Compare commits

...
18 Commits
Author SHA1 Message Date
vii 40698efb7d docs(changelog): release 0.2.0 2026-09-16 03:25:39 -05:00
vii 3d2a2b3d98 docs(changelog): record the input, storage, and packaging changes 2026-09-16 02:56:11 -05:00
vii 78a314b761 ci(packages): reduce to a branded nightly AppImage
The workflow built DEB, RPM and pacman packages on every push to main and
every pull request, then published them as "Mocktail continuous" with
assets named Mocktail-* and a continuous tag pushed as komaruworld.

Nightcap ships the AppImage and the Flatpak, so the three native package
jobs are removed along with the APT and RPM repository step, which could
not work here regardless: it downloads .deb and .rpm from this repository's
latest release, and the release workflow publishes neither.

What remains runs nightly and on demand rather than per commit, skips the
build when the continuous tag already points at HEAD, publishes
Nightcap-nightly-x86_64.AppImage with a checksum beside it, and tags as
github-actions[bot]. A manual run from another branch builds and
smoke-tests without touching the release.
2026-09-16 02:51:20 -05:00
vii 2d1dd3788b ci(release): pass GH_TOKEN to the AppImage dependency installer
install_anylinux_dependencies.sh now queries the GitHub API for the rolling
release's asset IDs and digests. Without a token it uses the unauthenticated
rate limit, which shared Actions runner IPs exhaust. packages.yml already
passes the token; the release workflow runs the same script in the same
container and did not.
2026-09-16 02:42:06 -05:00
vii 56d3160641 Merge upstream komaruworld/mocktail into main
Takes upstream's AnyLinux dependency rework: the checked-in digest list is
replaced by a name list, and the installer snapshots GitHub asset IDs and
SHA256 digests together at build time, verifying each archive before
installation. This removes the manual hash refresh a998754 had to do.

Keeps Nightcap's FAQ.md over the upstream copy; ours is the same document
with Nightcap paths, RPC on by default, and the Discord play time section.

Keeps TextureManager2 enabled rather than upstream's revert to TM1. Their
mip regression is described on the mobile MipPackStream path, and this fork
already reports real host memory and a PC device profile. Hours of play
show no texture problem, so the workaround is not adopted without evidence
it is needed here.
2026-09-16 02:37:08 -05:00
vii 5db99041bc docs: rewrite the input roadmap items and trim the donation list
Discord Rich Presence item 1 is custom presence fields; the private-server
join item is dropped. The profiling item names the Chrome trace format
instead of Perfetto.

Item 2 covered hot-plug and input release on disconnect, which already
work, so it is now only the button glyph gap. Raw mouse and the pointer
fix move to Shipped.

Drops the BCH donation address from the README.
2026-09-16 02:29:07 -05:00
vii 9ec56338c3 test: fix umask, dash, and scheduling assumptions in three suites
native_update_test wrote approval artifacts with the process umask, which
candidate_approval rejects when group writable. The helper now chmods 0600.

webview_helper_launcher_test drove its fake helper with `dd <&198` under
#!/bin/sh. dash rejects multi-digit file descriptors, so the script never
ran and the packet file stayed empty. It now reads the control socket in
python3, as the adjacent tests in the file already do.

audio_foundation_test asserted the poller thread had run before stopping
it, without waiting for it to be scheduled; it failed about one run in two.
It now waits for the first poll under a bounded deadline.
2026-09-16 02:28:09 -05:00
vii 894ebf8eb7 fix(services): create BrowserTracker storage directories private and usable
The service created its own credential directory with create_directories
and then refused it, because IsPrivateDirectory rejects a group-writable
path and mkdir masks its mode with the process umask. On a umask of 002
that failed every first run; a umask carrying owner bits left the
directory unwritable by its owner instead.

CreatePrivateDirectories creates each missing component and tightens it to
0700 through a descriptor opened O_DIRECTORY | O_NOFOLLOW. Components that
already exist keep their mode and are still judged by IsPrivateDirectory.
2026-09-16 02:28:02 -05:00
vii 6bec8a6c5c feat(input): add input.raw_mouse and keep the pointer inside the viewport
Relative-mode motion reports zero absolute coordinates, so the router
accumulates deltas. The accumulator was never clamped, only the copy sent
to the engine, so a long turn pushed it far outside the viewport and every
reversal was swallowed until the pointer travelled all the way back.

input.raw_mouse sends look deltas in host units instead of guest surface
units. Those differ only when the desktop display scale and the window's
pixel density disagree, as under fractional scaling. Default off.
2026-09-16 02:27:56 -05:00
vii b9e7bc761b docs(roadmap): swap RPC join items for custom presence fields
Name the Chrome trace format for --profile instead of Perfetto.
2026-09-15 22:53:19 -05:00
komaruworld b54c896498 ci hate me? 2026-09-15 19:52:24 +03:00
komaruworld 1cd7b7db8f Fuck ci 2026-09-15 19:42:56 +03:00
vii f87aaca2fb feat(discord): enable Rich Presence by default under Nightcap's own application
Adds Terms and Privacy pages to the site for the Discord application.
2026-09-15 10:34:50 -05:00
vii 901ba615ab fix(runtime): keep fflags.json values over the performance preset instead of failing launch 2026-09-15 10:34:50 -05:00
komaruworld cdbf9ac1eb revert commit fdce534 2026-09-15 18:26:13 +03:00
vii f098bc1b5d docs(faq): add FAQ from upstream with Nightcap paths and link it
Discord RPC config key, log locations, and the upstream vr branch.
2026-09-15 09:12:38 -05:00
vii e1f7c8c06c docs(changelog): add CHANGELOG.md and use it for GitHub release notes
Release and Flatpak workflows take the release body from the tag's
changelog section and fail early if it is missing.
2026-09-15 08:23:11 -05:00
komaruworld f383fbbff4 update faq.md 2026-09-15 12:08:18 +03:00
44 changed files with 1133 additions and 413 deletions

No files matched your search

+1
View File
@@ -64,3 +64,4 @@ jobs:
bash tests/managed_payload_build_contract_test.sh .
bash tests/flatpak_packaging_test.sh .
bash tests/native_packaging_test.sh .
bash tests/anylinux_dependencies_test.sh .
+7 -1
View File
@@ -160,6 +160,9 @@ jobs:
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:
@@ -174,13 +177,16 @@ jobs:
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" --generate-notes ||
--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
+58 -220
View File
@@ -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:
@@ -45,10 +53,13 @@ jobs:
- 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 \
@@ -135,155 +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
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 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 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 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, 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:
@@ -294,84 +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/*.AppImage release/mocktail-nightly.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.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
+15 -1
View File
@@ -24,12 +24,20 @@ jobs:
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 \
@@ -127,6 +135,9 @@ jobs:
permissions:
contents: write
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download AppImage
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -142,13 +153,16 @@ jobs:
tag="${GITHUB_REF_NAME}"
mv release/Mocktail-x86_64.AppImage release/Nightcap-x86_64.AppImage
(cd release && sha256sum Nightcap-x86_64.AppImage > Nightcap-x86_64.AppImage.sha256)
./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" --generate-notes ||
--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.AppImage \
release/Nightcap-x86_64.AppImage.sha256
+80
View File
@@ -0,0 +1,80 @@
# 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.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.2.0...HEAD
[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 -1
View File
@@ -61,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}"
+65
View File
@@ -0,0 +1,65 @@
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
# FAQ
## How do I turn Discord RPC off?
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/io.github.CoderDayton.nightcap/config/mocktail/config.yaml`
For native installs, a custom `$XDG_CONFIG_HOME` replaces `~/.config`.
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: false
```
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?
Nightcap saves logs automatically. Reproduce the issue, close Nightcap, and
attach `latest.log`:
- Native / AppImage: `~/.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 play in VR?
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 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
```
Start WiVRn or SteamVR/ALVR, connect your headset, then run:
```bash
./build/mocktail -vr
```
+3 -2
View File
@@ -35,7 +35,9 @@ side by side.
binary.
Everything else works the same as upstream. See the
[roadmap](docs/ROADMAP.md) for what is coming next.
[roadmap](docs/ROADMAP.md) for what is coming next, the
[changelog](CHANGELOG.md) for what each release changed, and the
[FAQ](FAQ.md) for Discord RPC, logs, and VR.
## Install with Flatpak
@@ -195,7 +197,6 @@ Support this fork by giving it a star or with cryptocurrency:
- LTC: `LdABR2ELRYrUESWZWrEk1uNx38gHHMuHUU`
- SOL: `tjeDnPoWeyW8zfTf8c5CVAm5QTV8vFPaPouaScAdwfT`
- BCH: `qpktw5jwut9ae8e6898qt3v7rj3km44utgk2r9mj8n`
Support the upstream project, which does most of the heavy lifting:
+10 -2
View File
@@ -77,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.
@@ -89,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.
+27 -17
View File
@@ -13,7 +13,7 @@ Goal: a small GTK4 settings window, reachable from the app menu and from a
`--settings` flag, that reads and writes the same `config.yaml`.
- Graphics tab: backend, frame rate limit, vsync, small texture upscale.
- Input tab: raw mouse and controller options (see item 2).
- Input tab: the `input.raw_mouse` toggle and controller options.
- Advanced tab: FFlag overrides with a plain text editor for `fflags.json`.
- Discord tab: the `discord_rpc` block (see item 3).
- Changes apply on next launch. A restart button restarts Nightcap.
@@ -21,28 +21,29 @@ Goal: a small GTK4 settings window, reachable from the app menu and from a
Done when every option in `config/mocktail.example.yaml` that a player would
touch can be set without a text editor.
## 2. Controller and raw mouse input
## 2. Controller button glyphs
Upstream added Xbox and PlayStation gamepad support through SDL. Desktop play
still has rough edges.
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.
- Raw mouse input in first-person games, with no acceleration or clamping.
- Better controller support: hot-plug without a restart, correct button
glyphs for the connected pad, and no stuck inputs after a disconnect.
- 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 mouse feels like a mouse and a plugged-in controller just works.
Done when a plugged-in pad shows its own buttons.
## 3. Discord Rich Presence
Upstream ships Discord RPC, off by default, behind `discord_rpc.enabled`. It
shows the experience name, elapsed time, and a join button when the server is
public.
Upstream ships Discord RPC. Nightcap turns it on by default; see the
[FAQ](../FAQ.md) to switch it off. It shows the experience name, elapsed
time, the experience icon, and a join button when the server is public.
- Turn it on by default. It never reads a Discord token, so the privacy cost
is only the experience name, which the player already shares in Roblox.
- Show the experience icon as the large image instead of a static logo.
- Show the current server region and player count when Roblox exposes them.
- Make the join button work for private servers the player has a link for.
- Custom presence: set the title, details line, state line, and large and
small icons from `config.yaml`, with placeholders like the experience name
and elapsed time.
- A Discord tab in the settings window to toggle each field.
Done when a friend can see what you are playing and join in one click, and
@@ -57,7 +58,8 @@ feeling.
GPU time, and present latency.
- Shader compile and pipeline cache tracing, so first-load stutter can be
measured and cut.
- A `--profile` flag that writes a trace file readable by Perfetto.
- A `--profile` flag that writes a Chrome-format trace file, readable in
`ui.perfetto.dev` and `chrome://tracing`.
- A short benchmark guide in `docs/` with a fixed set of experiences, so
before and after numbers can be compared across machines.
@@ -67,12 +69,20 @@ proves it.
## 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.
- 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
+12 -10
View File
@@ -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
@@ -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);
+4
View File
@@ -171,6 +171,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 {
@@ -237,6 +240,7 @@ class RobloxInputRouter final {
std::vector<ActiveGamepad> gamepads_;
float mouse_x_ = 0.0f;
float mouse_y_ = 0.0f;
bool raw_mouse_ = false;
};
const char* RobloxInputDispatchStateName(RobloxInputDispatchState state);
@@ -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,
+6 -1
View File
@@ -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 {
@@ -55,7 +60,7 @@ struct DiscordRpcTextConfig {
};
struct DiscordRpcConfig {
bool enabled = false;
bool enabled = true;
bool show_place_name = true;
bool show_elapsed_time = true;
bool join_enabled = true;
-12
View File
@@ -1,12 +0,0 @@
# Modified by vii from komaruworld/mocktail. See README "About this fork".
# pkgforge-dev/archlinux-pkgs-debloated, continuous release, 2026-09-15.
# These assets are rolling. Refresh names/hashes together; do not bypass a
# checksum mismatch. Only the disposable AppImage build container installs them.
eb87771dabc819b318e0ddf94e7ea175a662c1b6fbe748661126c520a0fd4627 ffmpeg-mini-x86_64.pkg.tar.zst
e62d30173cc87d3a717a218610c9291b9f647b29b99ee86a0a591226c39a0728 gdk-pixbuf2-mini-x86_64.pkg.tar.zst
ca18bc00a343650cde3d674135fa482b54faa639017f82792906db8c68ca9fb6 gtk4-mini-x86_64.pkg.tar.zst
8500f31bb0f793d159d58f2e428594baba63f0c1a0276394c5a3f1ac1da422ef icu-mini-x86_64.pkg.tar.zst
3d51ee49d0e49a065971703b3f61b610bd1c307cd5165ec16846d75f3efe4a08 librsvg-mini-x86_64.pkg.tar.zst
fd52222372202ac7d37ff3f6ef0a47b0decee224aab37ac2b24086fb623b8b86 libxml2-mini-x86_64.pkg.tar.zst
da5598b3f8e3a33ba0811c225752466a1abd3821053a19b480410937454feb04 mesa-mini-x86_64.pkg.tar.zst
ca23eed050345d8c3f45a21610f91aecc273569b09d8d6eed574014e2e560d89 opus-mini-x86_64.pkg.tar.zst
+12
View File
@@ -0,0 +1,12 @@
# Allowed reduced packages from pkgforge-dev/archlinux-pkgs-debloated.
# continuous is rolling: the installer snapshots GitHub asset IDs and SHA256
# digests together at build time, and verifies every archive before installation.
# Only the disposable AppImage build container installs these packages.
ffmpeg-mini-x86_64.pkg.tar.zst
gdk-pixbuf2-mini-x86_64.pkg.tar.zst
gtk4-mini-x86_64.pkg.tar.zst
icu-mini-x86_64.pkg.tar.zst
librsvg-mini-x86_64.pkg.tar.zst
libxml2-mini-x86_64.pkg.tar.zst
mesa-mini-x86_64.pkg.tar.zst
opus-mini-x86_64.pkg.tar.zst
+2
View File
@@ -70,3 +70,5 @@ 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"
+116 -30
View File
@@ -4,36 +4,122 @@
set -Eeuo pipefail
# The reduced packages are intentionally limited to the disposable builder.
# Never replace a developer's desktop / graphics packages on the host.
[[ -f /.dockerenv || -f /run/.containerenv ]] || {
printf 'AnyLinux dependency installation requires a disposable container\n' >&2
exit 1
}
[[ "$(id -u)" == 0 ]] || {
printf 'AnyLinux dependency installation requires container root\n' >&2
AnyLinuxDependencyDie() {
printf '[anylinux-dependencies] error: %s\n' "$*" >&2
exit 1
}
readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
readonly MANIFEST="${ROOT}/packaging/anylinux-dependencies.sha256"
readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")"
trap 'rm -rf -- "${DOWNLOADS}"' EXIT
readonly BASE_URL=https://github.com/pkgforge-dev/archlinux-pkgs-debloated/releases/download/continuous
declare -a packages=()
while read -r digest filename remainder; do
[[ -n "${digest}" && "${digest}" != \#* ]] || continue
[[ "${digest}" =~ ^[0-9a-f]{64}$ &&
"${filename}" =~ ^[a-z0-9-]+-x86_64\.pkg\.tar\.zst$ &&
-z "${remainder}" ]] || {
printf 'Invalid AnyLinux dependency manifest entry\n' >&2
exit 1
}
curl --fail --location --retry 3 --output "${DOWNLOADS}/${filename}" \
"${BASE_URL}/${filename}"
printf '%s %s\n' "${digest}" "${DOWNLOADS}/${filename}" |
sha256sum --check --strict
packages+=("${DOWNLOADS}/${filename}")
done <"${MANIFEST}"
(( ${#packages[@]} > 0 ))
pacman -U --noconfirm --needed "${packages[@]}"
AnyLinuxDependencyRequireContainer() {
# Never replace a developer's desktop / graphics packages on the host.
[[ -f /.dockerenv || -f /run/.containerenv ]] ||
AnyLinuxDependencyDie 'installation requires a disposable container'
[[ "$(id -u)" == 0 ]] ||
AnyLinuxDependencyDie 'installation requires container root'
}
AnyLinuxDependencySnapshot() {
local manifest="$1" release="$2" filename remainder names_json
local -a names=()
local -A seen=()
while read -r filename remainder || [[ -n "${filename}" ]]; do
[[ -n "${filename}" && "${filename}" != \#* ]] || continue
[[ "${filename}" =~ ^[a-z0-9-]+-mini-x86_64\.pkg\.tar\.zst$ &&
-z "${remainder}" && -z "${seen[${filename}]:-}" ]] ||
AnyLinuxDependencyDie 'invalid or duplicate dependency manifest entry'
seen["${filename}"]=1
names+=("${filename}")
done <"${manifest}"
(( ${#names[@]} > 0 )) || AnyLinuxDependencyDie 'empty dependency manifest'
names_json="$(printf '%s\n' "${names[@]}" | jq --raw-input --slurp \
'split("\n")[:-1]')"
# continuous assets are replaced on upstream rebuilds. Resolve all required
# names and GitHub-computed digests in one response, then download by asset ID
# rather than by rolling filename. These are per-build integrity checks, not
# immutable version pins. Never accept absent digests or compute our own trust.
jq --exit-status --raw-output --argjson names "${names_json}" \
--arg base 'https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases/assets/' '
(if .tag_name != "continuous" or (.assets | type) != "array" then
error("invalid continuous release metadata")
else .assets end) as $assets |
$names[] as $name |
[$assets[] | select(.name == $name)] as $matches |
if ($matches | length) != 1 then
error("missing or duplicate dependency asset: " + $name)
else $matches[0] end |
if .state != "uploaded" or (.id | type) != "number" or
.id <= 0 or .id != (.id | floor) or
(.digest | type) != "string" then
error("invalid dependency asset metadata: " + $name)
elif (.digest | test("^sha256:[0-9a-f]{64}$") | not) or
.url != ($base + (.id | tostring)) then
error("invalid dependency digest or URL: " + $name)
else
[(.digest | ltrimstr("sha256:")), $name, .url] | @tsv
end
' "${release}" || AnyLinuxDependencyDie 'cannot resolve verified dependency assets'
}
AnyLinuxDownloadDependencies() {
local manifest="$1" downloads="$2" attempt digest filename url failed
local -a curl_options=(
--fail --silent --show-error --location --retry 3
--proto '=https' --proto-redir '=https'
--connect-timeout 15 --max-time 180
--header 'X-GitHub-Api-Version: 2022-11-28'
)
if [[ -n "${GH_TOKEN:-}" ]]; then
curl_options+=(--header "Authorization: Bearer ${GH_TOKEN}")
fi
for attempt in 1 2 3; do
curl "${curl_options[@]}" --header 'Accept: application/vnd.github+json' \
--output "${downloads}/release.json" \
https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases/tags/continuous ||
AnyLinuxDependencyDie 'cannot fetch upstream release metadata'
AnyLinuxDependencySnapshot "${manifest}" "${downloads}/release.json" \
>"${downloads}/dependencies.tsv"
: >"${downloads}/SHA256SUMS"
failed=false
while IFS=$'\t' read -r digest filename url; do
printf '[anylinux-dependencies] downloading %s (SHA256 %s)\n' \
"${filename}" "${digest}" >&2
if ! curl "${curl_options[@]}" --header 'Accept: application/octet-stream' \
--output "${downloads}/${filename}" "${url}"; then
failed=true
break
fi
printf '%s %s\n' "${digest}" "${filename}" >>"${downloads}/SHA256SUMS"
done <"${downloads}/dependencies.tsv"
if [[ "${failed}" == true ]]; then
# An upstream upload can delete an ID between resolution and download.
# Retry a complete snapshot, never mix old checksums with new filenames.
printf '[anylinux-dependencies] download failed; refreshing snapshot (%s/3)\n' \
"${attempt}" >&2
continue
fi
(cd -- "${downloads}" && sha256sum --check --strict SHA256SUMS) ||
AnyLinuxDependencyDie 'dependency checksum verification failed'
return 0
done
AnyLinuxDependencyDie 'dependency downloads failed after 3 snapshots'
}
AnyLinuxDependenciesMain() (
AnyLinuxDependencyRequireContainer
(( $# == 0 )) || AnyLinuxDependencyDie 'no arguments are supported'
readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
readonly MANIFEST="${ROOT}/packaging/anylinux-dependencies.txt"
readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")"
trap 'rm -rf -- "${DOWNLOADS}"' EXIT
AnyLinuxDownloadDependencies "${MANIFEST}" "${DOWNLOADS}"
local digest filename url
local -a packages=()
while IFS=$'\t' read -r digest filename url; do
packages+=("${DOWNLOADS}/${filename}")
done <"${DOWNLOADS}/dependencies.tsv"
pacman -U --noconfirm --needed "${packages[@]}"
)
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
AnyLinuxDependenciesMain "$@"
fi
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Print the CHANGELOG.md section for one version, for use as GitHub release
# notes. Usage: scripts/release_notes.sh v0.1.0 [CHANGELOG.md]
# Exits 1 when the version has no section, so a release cannot ship without
# notes.
set -Eeuo pipefail
readonly tag="${1:?tag is required, e.g. v0.1.0}"
readonly changelog="${2:-$(dirname -- "${BASH_SOURCE[0]}")/../CHANGELOG.md}"
readonly version="${tag#v}"
notes="$(awk -v version="${version}" '
/^## \[/ {
if (found) exit
found = ($0 ~ "^## \\[" version "\\]")
next
}
/^\[[^]]+\]: / { if (found) exit }
found { print }
' "${changelog}")"
# Trim leading and trailing blank lines.
notes="$(printf '%s\n' "${notes}" | sed -e '/./,$!d' | sed -e ':a' -e '/^\n*$/{$d;N;ba' -e '}')"
[[ -n "${notes}" ]] || {
printf 'CHANGELOG.md has no section for version %s\n' "${version}" >&2
exit 1
}
printf '%s\n' "${notes}"
+2
View File
@@ -41,6 +41,8 @@ cd nightcap &amp;&amp; make build
<a href="https://github.com/CoderDayton/nightcap/releases/latest/download/Nightcap-x86_64.AppImage">AppImage</a>
<a href="Nightcap-x86_64.flatpak">Flatpak bundle</a>
<a href="nightcap-flatpak.gpg">Signing key</a>
<a href="terms.html">Terms</a>
<a href="privacy.html">Privacy</a>
</p>
<p><a class="source" href="https://github.com/CoderDayton/nightcap">Source code</a> ·
<a class="source" href="https://github.com/komaruworld/mocktail">Upstream Mocktail</a></p>
+43
View File
@@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Nightcap Privacy Policy</title>
<link rel="icon" href="nightcap.svg" type="image/svg+xml">
<link rel="stylesheet" href="styles.css">
</head>
<body><main>
<h1>Privacy Policy</h1>
<p>Nightcap is a Linux runtime for the Roblox Android client. This policy covers Nightcap itself and its Discord application. Roblox and Discord have their own policies for the data they handle.</p>
<h2>What Nightcap collects</h2>
<p>Nothing. Nightcap has no server. It does not collect, store, or send telemetry, analytics, crash reports, or account data to the Nightcap project.</p>
<h2>What stays on your machine</h2>
<ul>
<li>Configuration in <code>~/.config/mocktail/</code></li>
<li>Session logs and support bundles in <code>~/.local/state/mocktail/</code></li>
<li>Roblox login session under <code>~/.local/share/mocktail/</code></li>
<li>Roblox cache, downloaded payloads, and shader cache under <code>~/.cache/mocktail/</code></li>
</ul>
<p>Flatpak installs keep the same layout under <code>~/.var/app/io.github.CoderDayton.nightcap/</code>.</p>
<h2>Discord Rich Presence</h2>
<p>When Rich Presence is on, Nightcap sends the current experience name, its public icon URL, elapsed time, and an optional public server join code to the Discord desktop client on the same machine over a local socket. Discord displays this on your profile under its own privacy settings. Nightcap never signs in to Discord, never reads a Discord token, and never contacts Discord's servers.</p>
<p>Set <code>integrations.discord_rpc.enabled: false</code> in <code>config.yaml</code> to turn it off.</p>
<h2>Roblox</h2>
<p>Nightcap talks to Roblox's services exactly as the Roblox client would. That traffic is covered by the Roblox Privacy Policy.</p>
<h2>Contact</h2>
<p>Open an issue at <a class="source" href="https://github.com/CoderDayton/nightcap/issues">github.com/CoderDayton/nightcap/issues</a>.</p>
<p class="links">
<a href="index.html">Home</a>
<a href="terms.html">Terms of Service</a>
<a href="https://github.com/CoderDayton/nightcap">Source code</a>
</p>
</main></body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Nightcap Terms of Service</title>
<link rel="icon" href="nightcap.svg" type="image/svg+xml">
<link rel="stylesheet" href="styles.css">
</head>
<body><main>
<h1>Terms of Service</h1>
<p>Nightcap is free and open-source software licensed under the Apache License, Version 2.0. The license text shipped with the source governs your use of the software.</p>
<h2>No warranty</h2>
<p>Nightcap is provided "as is", without warranty of any kind. The authors are not liable for any loss, damage, or account action that results from using it. See sections 7 and 8 of the Apache License.</p>
<h2>Not affiliated</h2>
<p>Nightcap is an independent community project. It is not affiliated with, endorsed by, or supported by Roblox Corporation or Discord Inc. Roblox and Discord are trademarks of their respective owners.</p>
<h2>Your responsibility</h2>
<p>You are responsible for following the Roblox Terms of Use and the Discord Terms of Service while using Nightcap with those services. Nightcap does not modify Roblox game logic, does not bypass Roblox security, and is not a cheat.</p>
<h2>Discord application</h2>
<p>The Nightcap Discord application only provides Rich Presence for the Nightcap desktop runtime. It requests no permissions, has no bot, and does not access your Discord account.</p>
<h2>Changes</h2>
<p>These terms may change with new releases. The current version always lives at this address.</p>
<p class="links">
<a href="index.html">Home</a>
<a href="privacy.html">Privacy Policy</a>
<a href="https://github.com/CoderDayton/nightcap">Source code</a>
</p>
</main></body>
</html>
+1
View File
@@ -6801,6 +6801,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
window_input_runtime =
std::make_shared<mocktail::runtime::RobloxWindowInputRuntime>(
input_environment, roblox_capabilities.input);
window_input_runtime->SetRawMouseEnabled(input_capabilities.raw_mouse);
const mocktail::Status input_status = window_input_runtime->Initialize();
if (!input_status.ok()) {
std::cerr << "[FATAL] Typed production input did not initialize: "
+8 -1
View File
@@ -1,3 +1,4 @@
// Modified by vii from komaruworld/mocktail. See README "About this fork".
#include <algorithm>
#include <chrono>
#include <condition_variable>
@@ -779,14 +780,20 @@ int main(int argc, char* argv[]) {
<< " FFlag overrides from " << fflags_path << '\n';
}
std::string client_settings_overrides;
std::vector<std::string> kept_fflag_overrides;
if (!mocktail::runtime::MergeRuntimeClientSettingsOverrides(
runtime_config.config.frame_rate(),
runtime_config.config.performance(), fflags.json,
&client_settings_overrides, &command_line_error)) {
&client_settings_overrides, &command_line_error,
&kept_fflag_overrides)) {
std::cerr << "[FATAL] Cannot apply runtime client-settings policy: "
<< command_line_error << '\n';
return EXIT_FAILURE;
}
for (const std::string& name : kept_fflag_overrides) {
std::cout << " [runtime] keeping " << name << " from " << fflags_path
<< " over the performance preset\n";
}
std::string audio_capture_overrides;
if (!mocktail::runtime::MergeAudioCaptureClientSettingsOverrides(
runtime_config.config.microphone_enabled(),
+28 -41
View File
@@ -1,3 +1,4 @@
// Modified by vii from komaruworld/mocktail. See README "About this fork".
#include "runtime/performance_policy.h"
#include <sched.h>
@@ -27,22 +28,21 @@ struct ClientSetting {
std::string_view value;
};
bool SetCompatibleValue(nlohmann::json* object, const ClientSetting& setting,
std::string* error) {
// A caller-supplied value always wins over the preset; the key is reported
// through kept_overrides so startup can say which preset values were skipped.
void SetCompatibleValue(nlohmann::json* object, const ClientSetting& setting,
std::vector<std::string>* kept_overrides) {
const std::string name(setting.name);
const std::string value(setting.value);
const auto existing = object->find(name);
if (existing != object->end()) {
if (!existing->is_string() || existing->get<std::string>() != value) {
if (error != nullptr) {
*error = std::string("performance policy conflicts with ") + name;
}
return false;
if ((!existing->is_string() || existing->get<std::string>() != value) &&
kept_overrides != nullptr) {
kept_overrides->push_back(name);
}
return true;
return;
}
(*object)[name] = value;
return true;
}
bool ReadTopologyValue(int logical_cpu, std::string_view name, int* value) {
@@ -154,10 +154,10 @@ int DetectAvailablePhysicalCoreCount() {
: static_cast<int>(physical_cores.size());
}
bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
std::string_view base_json,
std::string* merged_json,
std::string* error) {
bool MergePerformanceClientSettingsOverrides(
const PerformancePolicy& policy, std::string_view base_json,
std::string* merged_json, std::string* error,
std::vector<std::string>* kept_overrides) {
if (merged_json == nullptr) {
if (error != nullptr) {
*error = "performance client-settings output is required";
@@ -184,13 +184,10 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
return true;
}
const auto apply_settings = [&overrides,
error](const auto& settings) -> bool {
kept_overrides](const auto& settings) {
for (const ClientSetting& setting : settings) {
if (!SetCompatibleValue(&overrides, setting, error)) {
return false;
}
SetCompatibleValue(&overrides, setting, kept_overrides);
}
return true;
};
const bool throughput_mode =
policy.physics_worker_mode == PhysicsWorkerMode::kThroughput;
@@ -198,9 +195,7 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
const std::array<ClientSetting, 1> physics_settings = {{
{"DFIntSimMidPhaseContactPipelineBatchSize", "128"},
}};
if (!apply_settings(physics_settings)) {
return false;
}
apply_settings(physics_settings);
}
int render_worker_count = 0;
if (throughput_mode ||
@@ -223,9 +218,7 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
{"FIntTaskSchedulerAutoThreadLimit", workers},
{"DFIntSimMidPhaseContactPipelineBatchSize", "128"},
}};
if (!apply_settings(physics_settings)) {
return false;
}
apply_settings(physics_settings);
} else if (policy.multithreaded_rendering &&
policy.physics_worker_mode != PhysicsWorkerMode::kLatency) {
const std::string workers = std::to_string(render_worker_count);
@@ -233,9 +226,7 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
{"FIntTaskSchedulerThreadMin", workers},
{"FIntTaskSchedulerAsyncTasksMinimumThreadCount", workers},
}};
if (!apply_settings(scheduler_settings)) {
return false;
}
apply_settings(scheduler_settings);
}
if ((policy.multithreaded_rendering || throughput_mode) &&
policy.physics_worker_mode != PhysicsWorkerMode::kLatency) {
@@ -322,26 +313,21 @@ bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
{"FIntDefaultAudioDecodeBufferSizeMs", "50"},
{"FIntMaxAudibleSoundChannels", "32"},
}};
if (!apply_settings(rendering_settings)) {
return false;
}
apply_settings(rendering_settings);
if (!manual_quality) {
if (!SetCompatibleValue(
&overrides, {"FIntDebugFRMQualityLevelOverride", quality_str},
error)) {
return false;
}
SetCompatibleValue(&overrides,
{"FIntDebugFRMQualityLevelOverride", quality_str},
kept_overrides);
}
}
*merged_json = overrides.dump();
return true;
}
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) {
std::string frame_rate_overrides;
if (!MergeFrameRateClientSettingsOverrides(frame_rate, base_json,
&frame_rate_overrides, error)) {
@@ -349,7 +335,8 @@ bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
}
std::string performance_overrides;
if (!MergePerformanceClientSettingsOverrides(
performance, frame_rate_overrides, &performance_overrides, error)) {
performance, frame_rate_overrides, &performance_overrides, error,
kept_overrides)) {
return false;
}
std::string http_client_overrides;
@@ -627,6 +627,10 @@ RobloxInputDispatchResult RobloxInputRuntime::HandleEvent(
return result;
}
void RobloxInputRuntime::SetRawMouseEnabled(bool enabled) {
router_.SetRawMouseEnabled(enabled);
}
RobloxInputSnapshot RobloxInputRuntime::Snapshot() const {
return router_.Snapshot();
}
+26 -11
View File
@@ -532,6 +532,11 @@ RobloxInputSnapshot RobloxInputRouter::Snapshot() const {
return snapshot;
}
void RobloxInputRouter::SetRawMouseEnabled(bool enabled) {
const std::lock_guard<std::mutex> lock(mutex_);
raw_mouse_ = enabled;
}
RobloxInputDispatchResult RobloxInputRouter::HandleMouseMotionLocked(
const platform::MouseMotionEvent& event) {
if (sink_.mouse_move == nullptr) {
@@ -544,21 +549,31 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseMotionLocked(
const float delta_x = transform.HostLogicalToGuestX(event.delta_x);
const float delta_y = transform.HostLogicalToGuestY(event.delta_y);
if (event.x == 0.0f && event.y == 0.0f &&
(delta_x != 0.0f || delta_y != 0.0f)) {
mouse_x_ += delta_x;
mouse_y_ += delta_y;
} else {
mouse_x_ = transform.HostLogicalToGuestX(event.x);
mouse_y_ = transform.HostLogicalToGuestY(event.y);
}
const float max_x = std::max(0.0F, transform.guest_width() - 1.0F);
const float max_y = std::max(0.0F, transform.guest_height() - 1.0F);
const float clamped_x = std::clamp(mouse_x_, 0.0f, max_x);
const float clamped_y = std::clamp(mouse_y_, 0.0f, max_y);
// mouse_x_/mouse_y_ stay inside the viewport. Relative-mode motion reports
// zero absolute coordinates, so the accumulated position is the only
// position there; letting it drift past the edge would swallow every
// reversal until the pointer walked all the way back.
if (event.x == 0.0f && event.y == 0.0f &&
(delta_x != 0.0f || delta_y != 0.0f)) {
mouse_x_ = std::clamp(mouse_x_ + delta_x, 0.0f, max_x);
mouse_y_ = std::clamp(mouse_y_ + delta_y, 0.0f, max_y);
} else {
mouse_x_ = std::clamp(transform.HostLogicalToGuestX(event.x), 0.0f, max_x);
mouse_y_ = std::clamp(transform.HostLogicalToGuestY(event.y), 0.0f, max_y);
}
// Raw mouse reports the host's own delta, skipping the guest-surface scale
// that otherwise changes look sensitivity when the desktop display scale and
// the window's pixel density disagree. The position stays in guest units.
const float reported_delta_x = raw_mouse_ ? event.delta_x : delta_x;
const float reported_delta_y = raw_mouse_ ? event.delta_y : delta_y;
return NativeResultLocked(
sink_.mouse_move(sink_.context, clamped_x, clamped_y, delta_x, delta_y),
sink_.mouse_move(sink_.context, mouse_x_, mouse_y_, reported_delta_x,
reported_delta_y),
RobloxInputEventKind::kMouseMotion);
}
@@ -195,6 +195,10 @@ Status RobloxWindowInputRuntime::UpdateTextFocusProperties(
return runtime_.UpdateTextFocusProperties(generation, properties);
}
void RobloxWindowInputRuntime::SetRawMouseEnabled(bool enabled) {
runtime_.SetRawMouseEnabled(enabled);
}
RobloxInputSnapshot RobloxWindowInputRuntime::Snapshot() const {
return runtime_.Snapshot();
}
+7 -1
View File
@@ -1,3 +1,4 @@
// Modified by vii from komaruworld/mocktail. See README "About this fork".
#include "runtime/runtime_config.h"
#include <algorithm>
@@ -229,6 +230,11 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
keyboard.has_value()) {
config.input_capabilities_.keyboard_enabled = *keyboard;
}
if (const std::optional<bool> raw_mouse =
InputEnabled(environment, "MOCKTAIL_RAW_MOUSE");
raw_mouse.has_value()) {
config.input_capabilities_.raw_mouse = *raw_mouse;
}
bool customized_device = false;
config.device_profile_valid_ =
config.device_profile_valid_ &&
@@ -325,7 +331,7 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
}
bool discord_booleans_valid = true;
config.discord_rpc_.enabled = ReadBoolean(
environment, "MOCKTAIL_DISCORD_RPC_ENABLED", false,
environment, "MOCKTAIL_DISCORD_RPC_ENABLED", true,
&discord_booleans_valid);
config.discord_rpc_.show_place_name = ReadBoolean(
environment, "MOCKTAIL_DISCORD_RPC_SHOW_PLACE_NAME", true,
+12 -3
View File
@@ -17,7 +17,8 @@ namespace runtime {
namespace {
constexpr std::string_view kDefaultRuntimeConfig =
R"yaml(# Mocktail configuration.
R"yaml(# 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.
# Integer: configuration schema version. Only version 1 is supported.
@@ -95,6 +96,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.
@@ -107,9 +116,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.
+12
View File
@@ -183,6 +183,7 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
"window.title",
"window.high_dpi",
"input.touch_enabled",
"input.raw_mouse",
"compatibility.desktop_playability",
"network.use_system_proxy",
"network.proxy_host",
@@ -433,6 +434,14 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
}
(*environment)["MOCKTAIL_TOUCH_MODE"] = parsed ? "on" : "off";
}
if (const auto raw_mouse = value("input.raw_mouse"); raw_mouse.has_value()) {
bool parsed = false;
if (!ParseBoolean(*raw_mouse, &parsed)) {
*error = "input.raw_mouse must be true or false";
return false;
}
(*environment)["MOCKTAIL_RAW_MOUSE"] = parsed ? "on" : "off";
}
if (const auto desktop_playability =
value("compatibility.desktop_playability");
desktop_playability.has_value()) {
@@ -877,6 +886,9 @@ bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
SetEnvironmentValue(
"MOCKTAIL_KEYBOARD_MODE",
config.input_capabilities().keyboard_enabled ? "on" : "off", error) &&
SetEnvironmentValue("MOCKTAIL_RAW_MOUSE",
config.input_capabilities().raw_mouse ? "on" : "off",
error) &&
SetEnvironmentValue("MOCKTAIL_DESKTOP_PLAYABILITY",
config.desktop_playability() ? "1" : "0", error) &&
SetEnvironmentValue("MOCKTAIL_FRAME_RATE_LIMIT",
+47 -4
View File
@@ -13,6 +13,7 @@
#include <nlohmann/json.hpp>
#include <random>
#include <system_error>
#include <vector>
namespace mocktail {
namespace services {
@@ -211,6 +212,48 @@ bool FindExistingDirectoryAncestor(const std::filesystem::path& directory,
}
}
// Creates every component of `directory` below `existing_ancestor` with mode
// 0700, whatever the process umask. mkdir masks its mode argument, so a umask
// of 002 would leave the directory group writable and rejected by the
// IsPrivateDirectory checks that guard this file's credentials, while a umask
// carrying owner bits would leave it unwritable by its own owner. Only
// components this call creates are tightened; one that already exists keeps
// its mode and is judged by IsPrivateDirectory like any other.
bool CreatePrivateDirectories(const std::filesystem::path& directory,
const std::filesystem::path& existing_ancestor) {
std::vector<std::filesystem::path> missing;
for (std::filesystem::path candidate = directory;
candidate != existing_ancestor;) {
if (candidate.empty()) return false;
missing.push_back(candidate);
// Mirror FindExistingDirectoryAncestor's walk, including the implicit "."
// parent of a bare relative name, so the two agree on which components are
// missing.
std::filesystem::path parent = candidate.parent_path();
if (parent.empty() && candidate != ".") parent = ".";
if (parent.empty() || parent == candidate) return false;
candidate = parent;
}
for (auto component = missing.rbegin(); component != missing.rend();
++component) {
if (mkdir(component->c_str(), 0700) != 0) {
if (errno != EEXIST) return false;
continue;
}
const int created = open(component->c_str(),
O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
if (created < 0) return false;
struct stat metadata = {};
const bool tightened = fstat(created, &metadata) == 0 &&
S_ISDIR(metadata.st_mode) &&
metadata.st_uid == geteuid() &&
fchmod(created, 0700) == 0;
close(created);
if (!tightened) return false;
}
return true;
}
bool FsyncDirectoryChain(const std::filesystem::path& first,
const std::filesystem::path& last) {
std::filesystem::path current = first;
@@ -322,8 +365,7 @@ bool AtomicWriteContents(const std::filesystem::path& path,
return false;
}
const bool parent_existed = existing_ancestor == parent;
std::filesystem::create_directories(parent, filesystem_error);
if (filesystem_error) {
if (!CreatePrivateDirectories(parent, existing_ancestor)) {
error->assign(directory_error);
return false;
}
@@ -449,10 +491,11 @@ BrowserTrackerResult BrowserTrackerService::EnsureInitialized(
return result;
}
const bool lock_parent_existed = existing_lock_ancestor == lock_parent;
std::filesystem::create_directories(lock_parent, filesystem_error);
const bool lock_parent_created =
CreatePrivateDirectories(lock_parent, existing_lock_ancestor);
struct stat lock_parent_status = {};
const bool safe_lock_parent =
!filesystem_error &&
lock_parent_created &&
lstat(lock_parent.c_str(), &lock_parent_status) == 0 &&
IsPrivateDirectory(lock_parent_status);
const int lock = safe_lock_parent
+6
View File
@@ -1049,6 +1049,12 @@ add_test(
${CMAKE_SOURCE_DIR}/packaging/AppRun
)
add_test(
NAME AnyLinuxDependenciesTest.RollingAssetsAreVerified
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/anylinux_dependencies_test.sh
${CMAKE_SOURCE_DIR}
)
add_test(
NAME AnyLinuxPackagingTest.RelocatedRuntimePaths
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/anylinux_packaging_test.sh
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env bash
# Copyright 2026 Mocktail Project Authors
# SPDX-License-Identifier: Apache-2.0
set -Eeuo pipefail
readonly ROOT="${1:?source root is required}"
readonly TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-dependency-test.XXXXXX")"
trap 'rm -rf -- "${TEMP_DIR}"' EXIT
source "${ROOT}/scripts/install_anylinux_dependencies.sh"
Fail() {
printf 'AnyLinux dependency test failed: %s\n' "$*" >&2
exit 1
}
readonly API=https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases
readonly FFMPEG=ffmpeg-mini-x86_64.pkg.tar.zst
readonly OPUS=opus-mini-x86_64.pkg.tar.zst
printf 'ffmpeg fixture\n' >"${TEMP_DIR}/ffmpeg"
printf 'updated ffmpeg fixture\n' >"${TEMP_DIR}/updated-ffmpeg"
printf 'opus fixture\n' >"${TEMP_DIR}/opus"
readonly FFMPEG_HASH="$(sha256sum "${TEMP_DIR}/ffmpeg" | cut -d ' ' -f 1)"
readonly UPDATED_FFMPEG_HASH="$(sha256sum "${TEMP_DIR}/updated-ffmpeg" | cut -d ' ' -f 1)"
readonly OPUS_HASH="$(sha256sum "${TEMP_DIR}/opus" | cut -d ' ' -f 1)"
printf '# Required fixture packages\n\n%s\n%s' "${FFMPEG}" "${OPUS}" \
>"${TEMP_DIR}/manifest.txt"
jq --null-input --arg api "${API}" --arg ffmpeg "${FFMPEG}" --arg opus "${OPUS}" \
--arg ffmpeg_hash "${FFMPEG_HASH}" --arg opus_hash "${OPUS_HASH}" '
{tag_name: "continuous", assets: [
{id: 10, name: $ffmpeg, state: "uploaded", digest: ("sha256:" + $ffmpeg_hash),
url: ($api + "/assets/10")},
{id: 20, name: $opus, state: "uploaded", digest: ("sha256:" + $opus_hash),
url: ($api + "/assets/20")},
{id: 30, name: "unrequested-mini-x86_64.pkg.tar.zst", state: "uploaded",
digest: null, url: ($api + "/assets/30")}
]}
' >"${TEMP_DIR}/release.json"
jq --arg hash "${UPDATED_FFMPEG_HASH}" '
.assets[0].id = 11 | .assets[0].url |= sub("/10$"; "/11") |
.assets[0].digest = ("sha256:" + $hash) |
.assets[1].id = 21 | .assets[1].url |= sub("/20$"; "/21")
' \
"${TEMP_DIR}/release.json" >"${TEMP_DIR}/refreshed-release.json"
mkdir -p "${TEMP_DIR}/project/scripts" "${TEMP_DIR}/project/packaging"
cp "${ROOT}/scripts/install_anylinux_dependencies.sh" "${TEMP_DIR}/project/scripts/"
cp "${TEMP_DIR}/manifest.txt" "${TEMP_DIR}/project/packaging/anylinux-dependencies.txt"
# Record requests without any network access or changes to host packages.
curl() {
local output="" accept="" authorization="" url=""
while (( $# > 0 )); do
case "$1" in
--output) output="$2"; shift 2 ;;
--header)
case "$2" in
Accept:*) accept="$2" ;;
Authorization:*) authorization="$2" ;;
esac
shift 2 ;;
--retry|--proto|--proto-redir|--connect-timeout|--max-time) shift 2 ;;
--fail|--silent|--show-error|--location) shift ;;
https://*) url="$1"; shift ;;
*) Fail "unexpected curl option: $1" ;;
esac
done
if [[ -n "${GH_TOKEN:-}" ]]; then
[[ "${authorization}" == "Authorization: Bearer ${GH_TOKEN}" ]] ||
Fail 'missing authenticated GitHub request'
fi
printf '%s\n' "${url}" >>"${TEMP_DIR}/calls"
case "${url}" in
"${API}/tags/continuous")
[[ "${accept}" == 'Accept: application/vnd.github+json' ]]
if [[ "${SCENARIO}" == metadata-failure ]]; then return 22; fi
if [[ "${SCENARIO}" == replaced && -f "${TEMP_DIR}/asset-replaced" ]]; then
cp "${TEMP_DIR}/refreshed-release.json" "${output}"
else
cp "${TEST_RELEASE}" "${output}"
fi ;;
"${API}/assets/10"|"${API}/assets/11"|"${API}/assets/20"|"${API}/assets/21")
[[ "${accept}" == 'Accept: application/octet-stream' ]]
if [[ "${SCENARIO}" == download-failure ]]; then return 22; fi
if [[ "${SCENARIO}" == replaced && "${url}" == "${API}/assets/20" ]]; then
touch "${TEMP_DIR}/asset-replaced"
return 22
fi
if [[ "${url}" == "${API}/assets/20" || "${url}" == "${API}/assets/21" ]]; then
cp "${TEMP_DIR}/opus" "${output}"
elif [[ "${url}" == "${API}/assets/11" ]]; then
cp "${TEMP_DIR}/updated-ffmpeg" "${output}"
else
cp "${TEMP_DIR}/ffmpeg" "${output}"
fi
if [[ "${SCENARIO}" == corrupted && "${url}" == "${API}/assets/20" ]]; then
printf 'corrupted\n' >>"${output}"
fi ;;
*) Fail "unrequested or rolling asset URL: ${url}" ;;
esac
}
pacman() {
[[ "$#" == 5 && "$1" == -U && "$2" == --noconfirm && "$3" == --needed ]]
if [[ "${SCENARIO}" == replaced ]]; then
cmp "${TEMP_DIR}/updated-ffmpeg" "$4"
else
cmp "${TEMP_DIR}/ffmpeg" "$4"
fi
cmp "${TEMP_DIR}/opus" "$5"
local downloads="${4%/*}"
(cd "${downloads}" && sha256sum --check --strict SHA256SUMS)
cp "$4" "$5" "${downloads}/SHA256SUMS" "${downloads}/dependencies.tsv" \
"${TEMP_DIR}/${SCENARIO}/"
touch "${TEMP_DIR}/installed"
}
export -f curl pacman Fail
export TEMP_DIR API FFMPEG OPUS SCENARIO TEST_RELEASE
ExpectRejectedSnapshot() {
local description="$1" filter="$2"
jq "${filter}" "${TEMP_DIR}/release.json" >"${TEMP_DIR}/invalid.json"
if (AnyLinuxDependencySnapshot "${TEMP_DIR}/manifest.txt" \
"${TEMP_DIR}/invalid.json") >"${TEMP_DIR}/invalid.log" 2>&1; then
Fail "accepted ${description}"
fi
}
(AnyLinuxDependencySnapshot "${TEMP_DIR}/manifest.txt" \
"${TEMP_DIR}/release.json") >"${TEMP_DIR}/snapshot.tsv" ||
Fail 'rejected valid dependency snapshot'
printf '%s\t%s\t%s\n' \
"${FFMPEG_HASH}" "${FFMPEG}" "${API}/assets/10" \
"${OPUS_HASH}" "${OPUS}" "${API}/assets/20" \
>"${TEMP_DIR}/expected-snapshot.tsv"
cmp "${TEMP_DIR}/expected-snapshot.tsv" "${TEMP_DIR}/snapshot.tsv" ||
Fail 'incorrect dependency snapshot'
ExpectRejectedSnapshot 'missing asset' '.assets |= .[1:]'
ExpectRejectedSnapshot 'duplicate asset' '.assets += [.assets[0]]'
ExpectRejectedSnapshot 'absent SHA256' '.assets[0].digest = null'
ExpectRejectedSnapshot 'malformed SHA256' '.assets[0].digest = "sha256:bad"'
ExpectRejectedSnapshot 'foreign asset URL' '.assets[0].url = "https://example.org/archive"'
ExpectRejectedSnapshot 'fractional asset ID' '.assets[0].id = 10.5'
ExpectRejectedSnapshot 'unuploaded asset' '.assets[0].state = "starter"'
ExpectRejectedSnapshot 'different release' '.tag_name = "other"'
for invalid_manifest in '../ffmpeg-mini-x86_64.pkg.tar.zst' \
"${FFMPEG} extra" "${FFMPEG}"$'\n'"${FFMPEG}" ''; do
printf '%s\n' "${invalid_manifest}" >"${TEMP_DIR}/invalid-manifest.txt"
if (AnyLinuxDependencySnapshot "${TEMP_DIR}/invalid-manifest.txt" \
"${TEMP_DIR}/release.json") >"${TEMP_DIR}/invalid.log" 2>&1; then
Fail 'accepted unsafe, duplicate, or empty package allowlist'
fi
done
for SCENARIO in success replaced corrupted download-failure metadata-failure invalid-metadata; do
TEST_RELEASE="${TEMP_DIR}/release.json"
if [[ "${SCENARIO}" == invalid-metadata ]]; then
# A valid first row must not allow downloads when a later row is invalid.
jq '.assets[1].digest = null' "${TEMP_DIR}/release.json" >"${TEMP_DIR}/invalid.json"
TEST_RELEASE="${TEMP_DIR}/invalid.json"
fi
mkdir "${TEMP_DIR}/${SCENARIO}"
: >"${TEMP_DIR}/calls"
if GH_TOKEN=fixture-token bash -Eeuo pipefail -c '
source "$1/project/scripts/install_anylinux_dependencies.sh"
AnyLinuxDependencyRequireContainer() { :; }
AnyLinuxDependenciesMain
' _ "${TEMP_DIR}" \
>"${TEMP_DIR}/${SCENARIO}.log" 2>&1; then
[[ "${SCENARIO}" == success || "${SCENARIO}" == replaced ]] ||
Fail "installed packages after ${SCENARIO}"
[[ -f "${TEMP_DIR}/installed" ]]
(cd "${TEMP_DIR}/${SCENARIO}" && sha256sum --check --strict SHA256SUMS)
rm -- "${TEMP_DIR}/installed"
else
if [[ "${SCENARIO}" == success || "${SCENARIO}" == replaced ]]; then
cat -- "${TEMP_DIR}/${SCENARIO}.log" >&2
Fail "valid download failed: ${SCENARIO}"
fi
[[ ! -e "${TEMP_DIR}/installed" ]] || Fail 'installed unverified archives'
fi
case "${SCENARIO}" in
success)
[[ "$(wc -l <"${TEMP_DIR}/calls")" == 3 ]]
grep -Fq "${API}/assets/10" "${TEMP_DIR}/calls" ;;
replaced)
[[ "$(wc -l <"${TEMP_DIR}/calls")" == 6 ]]
grep -Fq "${API}/assets/11" "${TEMP_DIR}/calls"
grep -Fq "${API}/assets/11" "${TEMP_DIR}/replaced/dependencies.tsv"
grep -Fq "${API}/assets/21" "${TEMP_DIR}/replaced/dependencies.tsv" ;;
corrupted)
grep -Fq 'dependency checksum verification failed' "${TEMP_DIR}/${SCENARIO}.log" ;;
download-failure)
[[ "$(wc -l <"${TEMP_DIR}/calls")" == 6 ]]
grep -Fq 'failed after 3 snapshots' "${TEMP_DIR}/${SCENARIO}.log" ;;
metadata-failure|invalid-metadata)
[[ "$(wc -l <"${TEMP_DIR}/calls")" == 1 ]] ;;
esac
done
if (id() { printf '1000\n'; }; AnyLinuxDependencyRequireContainer) \
>"${TEMP_DIR}/guard.log" 2>&1; then
Fail 'container/root installation guard was bypassed'
fi
printf 'AnyLinux dependency tests passed\n'
+7
View File
@@ -628,6 +628,13 @@ TEST(OpenSlQueueTest, GetStatePollsWhilePlaybackReleasesBuffers) {
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls == 1; }));
}
// A freshly spawned thread is not guaranteed to have been scheduled yet, so
// wait for the first concurrent poll before asking the poller to stop.
const auto poll_deadline = std::chrono::steady_clock::now() + 2s;
while (polls.load(std::memory_order_relaxed) == 0 &&
std::chrono::steady_clock::now() < poll_deadline) {
std::this_thread::yield();
}
stop_polling.store(true, std::memory_order_relaxed);
poller.join();
EXPECT_GT(polls.load(std::memory_order_relaxed), 0);
+61 -1
View File
@@ -41,18 +41,26 @@ class FakeHttpClient final : public HttpClient {
class BrowserTrackerServiceTest : public testing::Test {
protected:
void SetUp() override {
// The service refuses to hold credentials in a group- or world-writable
// directory, so the directories these tests create must be private
// whatever umask the developer or CI runner happens to use.
previous_umask_ = umask(077);
root_ = std::filesystem::temp_directory_path() /
("mocktail-browser-tracker-" + std::to_string(getpid()) + "-" +
::testing::UnitTest::GetInstance()->current_test_info()->name());
std::filesystem::remove_all(root_);
}
void TearDown() override { std::filesystem::remove_all(root_); }
void TearDown() override {
std::filesystem::remove_all(root_);
umask(previous_umask_);
}
std::filesystem::path Storage() const {
return root_ / "appData/LocalStorage/appStorage.json";
}
std::filesystem::path Cookies() const { return root_ / "roblox.cookie"; }
std::filesystem::path root_;
mode_t previous_umask_ = 0;
};
TEST_F(BrowserTrackerServiceTest, ReusesValidStoredIdentityWithoutNetwork) {
@@ -171,6 +179,58 @@ TEST_F(BrowserTrackerServiceTest,
EXPECT_FALSE(std::filesystem::exists(Storage()));
}
TEST_F(BrowserTrackerServiceTest, CreatesPrivateStorageDirectoryUnderAnyUmask) {
std::filesystem::create_directories(root_);
ASSERT_EQ(chmod(root_.c_str(), 0700), 0);
std::ofstream(Cookies()) << ".ROBLOSECURITY=secret";
FakeHttpClient http;
http.response = {true,
200,
R"({"browserTrackerId":987654321})",
"",
{"Set-Cookie: RBXEventTrackerV2="
"CreateDate=1&rbxid=2&browserid=987654321; Path=/"}};
// A permissive umask must not widen the directories the service creates for
// its own credentials; it refuses to use any that are group or world
// writable.
const mode_t previous_umask = umask(0);
const BrowserTrackerResult result =
BrowserTrackerService(http).EnsureInitialized(Storage(), Cookies(), true);
umask(previous_umask);
ASSERT_TRUE(result) << result.error;
struct stat storage_parent = {};
ASSERT_EQ(stat(Storage().parent_path().c_str(), &storage_parent), 0);
EXPECT_EQ(storage_parent.st_mode & 0777, 0700U);
}
TEST_F(BrowserTrackerServiceTest,
CreatesUsableStorageDirectoryUnderRestrictiveUmask) {
std::filesystem::create_directories(root_);
ASSERT_EQ(chmod(root_.c_str(), 0700), 0);
std::ofstream(Cookies()) << ".ROBLOSECURITY=secret";
FakeHttpClient http;
http.response = {true,
200,
R"({"browserTrackerId":987654321})",
"",
{"Set-Cookie: RBXEventTrackerV2="
"CreateDate=1&rbxid=2&browserid=987654321; Path=/"}};
// mkdir applies the umask to its mode argument, so a umask that strips owner
// bits would otherwise leave the service's own directory unwritable.
const mode_t previous_umask = umask(0200);
const BrowserTrackerResult result =
BrowserTrackerService(http).EnsureInitialized(Storage(), Cookies(), true);
umask(previous_umask);
ASSERT_TRUE(result) << result.error;
struct stat storage_parent = {};
ASSERT_EQ(stat(Storage().parent_path().c_str(), &storage_parent), 0);
EXPECT_EQ(storage_parent.st_mode & 0777, 0700U);
}
TEST_F(BrowserTrackerServiceTest, RejectsCorruptStoredIdentityFailClosed) {
std::filesystem::create_directories(Storage().parent_path());
std::ofstream(Storage()) << R"({"BrowserTrackerId":"0"})";
+19 -18
View File
@@ -86,34 +86,35 @@ grep -Fq 'mz_stream_tell(unzGetStream(archive))' "${ANDROID_STUB}" ||
Fail 'Android assets do not support the minizip-ng offset API'
for expected in \
'ubuntu:26.04' \
'fedora:44' \
'archlinux:base-devel' \
'minizip-ng-compat-devel' \
'-G DEB' \
'-G RPM' \
'makepkg --dir' \
'cron:' \
'APPIMAGE_FORMAT=anylinux' \
'quick-sharun' \
'./scripts/install_anylinux_dependencies.sh' \
'--env GH_TOKEN' \
'MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1' \
'--appimage-extract-and-run mocktail_updater status' \
'Mocktail-x86_64.AppImage' \
'Nightcap-nightly-x86_64.AppImage' \
'github-actions[bot]' \
'gh release upload continuous'; do
grep -Fq -- "${expected}" "${WORKFLOW}" ||
Fail "native package workflow is missing: ${expected}"
Fail "nightly AppImage workflow is missing: ${expected}"
done
grep -Fq 'paths-ignore:' "${WORKFLOW}" ||
Fail 'native package workflow has no path filters'
grep -Fq "'assets/screenshots/**'" "${WORKFLOW}" ||
Fail 'native packages rebuild for screenshot-only changes'
if grep -Fq "'packaging/flatpak/**'" "${WORKFLOW}"; then
Fail 'native packages ignore Flatpak changes required by Pages publication'
fi
if grep -Fq "'site/**'" "${WORKFLOW}"; then
Fail 'native packages ignore website changes required by Pages publication'
fi
# The nightly runs on a schedule and on demand. Per-commit triggers would
# rebuild a full AppImage for every push and pull request. The DEB, RPM and
# pacman packages are built by the Makefile but are not published from here;
# upstream ships those for unmodified Mocktail.
for unexpected in \
'push:' \
'pull_request:' \
'-G DEB' \
'-G RPM' \
'makepkg --dir'; do
if grep -Fq -- "${unexpected}" "${WORKFLOW}"; then
Fail "nightly AppImage workflow should not contain: ${unexpected}"
fi
done
grep -Fq 'Ubuntu 26.04+' "${README}" ||
Fail 'README has no Ubuntu source dependency guide'
+3
View File
@@ -51,6 +51,9 @@ class TemporaryDirectory final {
void Write(const std::filesystem::path& path, std::string_view contents) {
std::filesystem::create_directories(path.parent_path());
std::ofstream(path, std::ios::binary) << contents;
// Approval artifacts are rejected when group or world writable, which a
// permissive umask would otherwise leave them.
chmod(path.c_str(), 0600);
}
std::string ReadFile(const std::filesystem::path& path) {
+44 -24
View File
@@ -1,3 +1,4 @@
// Modified by vii from komaruworld/mocktail. See README "About this fork".
#include "runtime/performance_policy.h"
#include <gtest/gtest.h>
@@ -5,6 +6,7 @@
#define JSON_NOEXCEPTION 1
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace mocktail {
namespace runtime {
@@ -228,22 +230,50 @@ TEST(PerformancePolicyTest, ComposesFrameRateBeforePerformanceMode) {
EXPECT_EQ(parsed.at("FFlagLuaAppDefaultHttpRetry"), "False");
}
TEST(PerformancePolicyTest, RejectsConflictingOrMalformedOverrides) {
TEST(PerformancePolicyTest, KeepsUserOverridesThatConflictWithPreset) {
std::string merged;
std::string error;
std::vector<std::string> kept;
ASSERT_TRUE(MergePerformanceClientSettingsOverrides(
{true, 14},
R"({"FIntOcclusionWorkerThreadCount":"2","FFlagMovePrerenderV2":true,)"
R"("FIntDebugForceMSAASamples":4})",
&merged, &error, &kept))
<< error;
const nlohmann::json parsed = nlohmann::json::parse(merged);
EXPECT_EQ(parsed.at("FIntOcclusionWorkerThreadCount"), "2");
EXPECT_EQ(parsed.at("FFlagMovePrerenderV2"), true);
EXPECT_EQ(parsed.at("FIntDebugForceMSAASamples"), 4);
EXPECT_EQ(parsed.at("FFlagFastGPULightGrid"), "True");
EXPECT_EQ(kept, (std::vector<std::string>{"FIntOcclusionWorkerThreadCount",
"FFlagMovePrerenderV2",
"FIntDebugForceMSAASamples"}));
PerformancePolicy latency =
ParsePerformancePolicy("true", "0", "auto", "latency");
latency.physical_core_count = 14;
kept.clear();
ASSERT_TRUE(MergePerformanceClientSettingsOverrides(
latency, R"({"DFIntSimMidPhaseContactPipelineBatchSize":"64"})", &merged,
&error, &kept))
<< error;
EXPECT_EQ(nlohmann::json::parse(merged)
.at("DFIntSimMidPhaseContactPipelineBatchSize"),
"64");
EXPECT_EQ(kept, (std::vector<std::string>{
"DFIntSimMidPhaseContactPipelineBatchSize"}));
kept.clear();
ASSERT_TRUE(MergeRuntimeClientSettingsOverrides(
ParseFrameRatePolicy(""), {true, 14},
R"({"FIntOcclusionWorkerThreadCount":"2"})", &merged, &error, &kept))
<< error;
EXPECT_EQ(kept, (std::vector<std::string>{"FIntOcclusionWorkerThreadCount"}));
}
TEST(PerformancePolicyTest, RejectsMalformedOverrides) {
std::string merged = "unchanged";
std::string error;
EXPECT_FALSE(MergePerformanceClientSettingsOverrides(
{true, 14}, R"({"FIntOcclusionWorkerThreadCount":"2"})", &merged,
&error));
EXPECT_EQ(merged, "unchanged");
EXPECT_NE(error.find("FIntOcclusionWorkerThreadCount"), std::string::npos);
error.clear();
EXPECT_FALSE(MergePerformanceClientSettingsOverrides(
{true, 14}, R"({"FFlagMovePrerenderV2":true})", &merged, &error));
EXPECT_EQ(merged, "unchanged");
EXPECT_NE(error.find("FFlagMovePrerenderV2"), std::string::npos);
error.clear();
EXPECT_FALSE(MergePerformanceClientSettingsOverrides({true, 14}, "not-json",
&merged, &error));
EXPECT_EQ(merged, "unchanged");
@@ -254,16 +284,6 @@ TEST(PerformancePolicyTest, RejectsConflictingOrMalformedOverrides) {
&merged, &error));
EXPECT_EQ(merged, "unchanged");
EXPECT_NE(error.find("JSON object"), std::string::npos);
PerformancePolicy latency =
ParsePerformancePolicy("true", "0", "auto", "latency");
latency.physical_core_count = 14;
error.clear();
EXPECT_FALSE(MergePerformanceClientSettingsOverrides(
latency, R"({"DFIntSimMidPhaseContactPipelineBatchSize":"64"})", &merged,
&error));
EXPECT_NE(error.find("DFIntSimMidPhaseContactPipelineBatchSize"),
std::string::npos);
}
TEST(PerformancePolicyTest, HostAudioMenuUsesBridgedDeviceQueries) {
+65
View File
@@ -238,6 +238,71 @@ TEST_F(RobloxInputRouterTest, RoutesMouseMotionButtonAndVerticalWheel) {
EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].delta_y, -2.0f);
}
TEST_F(RobloxInputRouterTest,
KeepsRelativeMotionResponsiveAfterOvershootingViewportEdge) {
// Relative-mode motion reports zero absolute coordinates, so the router
// accumulates deltas. The accumulator stays inside the viewport: a reversal
// moves the pointer on the next event, however far the overshoot ran.
ASSERT_TRUE(
router_
.HandleEvent(Event(platform::MouseMotionEvent{640.0f, 360.0f, 0.0f,
0.0f, 0}))
.dispatched());
ASSERT_TRUE(
router_
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 2000.0f,
0.0f, 0}))
.dispatched());
ASSERT_TRUE(
router_
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, -100.0f,
0.0f, 0}))
.dispatched());
ASSERT_EQ(probe_.mouse_moves.size(), 3U);
EXPECT_FLOAT_EQ(probe_.mouse_moves[1].x, 1279.0f);
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].x, 1179.0f);
EXPECT_FLOAT_EQ(probe_.mouse_moves[2].delta_x, -100.0f);
}
// A fractionally scaled X11 desktop: the buffer is 1:1 with logical units
// while the display scale is not, which is the case where the guest-surface
// scale actually changes a look delta.
constexpr platform::WindowResizedEvent kFractionalScalingViewport{
1624, 811, 1624, 811, 1.145833F};
TEST_F(RobloxInputRouterTest, ScalesLookDeltasUnlessRawMouseIsEnabled) {
ASSERT_EQ(router_.HandleEvent(Event(kFractionalScalingViewport)).state,
RobloxInputDispatchState::kStateUpdated);
ASSERT_TRUE(
router_
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 10.0f,
-6.0f, 0}))
.dispatched());
ASSERT_EQ(probe_.mouse_moves.size(), 1U);
EXPECT_NEAR(probe_.mouse_moves[0].delta_x, 8.727f, 0.01f);
EXPECT_NEAR(probe_.mouse_moves[0].delta_y, -5.236f, 0.01f);
}
TEST_F(RobloxInputRouterTest, RawMouseSendsLookDeltasUnscaled) {
ASSERT_EQ(router_.HandleEvent(Event(kFractionalScalingViewport)).state,
RobloxInputDispatchState::kStateUpdated);
router_.SetRawMouseEnabled(true);
ASSERT_TRUE(
router_
.HandleEvent(Event(platform::MouseMotionEvent{0.0f, 0.0f, 10.0f,
-6.0f, 0}))
.dispatched());
ASSERT_EQ(probe_.mouse_moves.size(), 1U);
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_x, 10.0f);
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].delta_y, -6.0f);
// The reported position stays in guest units whatever the delta carries.
EXPECT_NEAR(probe_.mouse_moves[0].x, 8.727f, 0.01f);
EXPECT_FLOAT_EQ(probe_.mouse_moves[0].y, 0.0f);
}
TEST_F(RobloxInputRouterTest,
ScalesMouseCoordinatesAfterDisplayScaleChange) {
const RobloxInputDispatchResult resize = router_.HandleEvent(
+24 -2
View File
@@ -156,9 +156,9 @@ TEST(RuntimeConfigBootstrapTest,
"startup.\n output_device: default",
"# between boots; prefer the exact device name when it is unique.\n"
" input_device: default",
"# Boolean (default: false): publish Mocktail activity to Discord "
"# Boolean (default: true): publish Nightcap activity to Discord "
"Desktop.\n # This never signs in to Discord and never reads an "
"account token.\n enabled: false",
"account token.\n enabled: true",
"# Boolean (default: true): show the current Roblox experience "
"name.\n show_place_name: true",
"# Boolean (default: true): show how long the current session has "
@@ -288,6 +288,28 @@ TEST(RuntimeConfigFileTest, RejectsSymlinkAndOversizedConfiguration) {
EXPECT_NE(loaded.error.find("no larger than 1 MiB"), std::string::npos);
}
TEST(RuntimeConfigFileTest, RawMouseDefaultsOffAndLoadsFromYaml) {
TemporaryDirectory defaulted_directory;
const RuntimeConfigLoadResult defaulted = LoadRuntimeConfig(
MapEnvironment(), defaulted_directory.Write(R"yaml(
version: 1
input:
touch_enabled: false
)yaml"));
ASSERT_TRUE(defaulted) << defaulted.error;
EXPECT_FALSE(defaulted.config.input_capabilities().raw_mouse);
TemporaryDirectory enabled_directory;
const RuntimeConfigLoadResult enabled = LoadRuntimeConfig(
MapEnvironment(), enabled_directory.Write(R"yaml(
version: 1
input:
raw_mouse: true
)yaml"));
ASSERT_TRUE(enabled) << enabled.error;
EXPECT_TRUE(enabled.config.input_capabilities().raw_mouse);
}
TEST(RuntimeConfigFileTest, LoadsTypedDesktopAndGraphicsSettings) {
TemporaryDirectory temporary;
const std::filesystem::path file = temporary.Write(R"yaml(
+3 -2
View File
@@ -1,3 +1,4 @@
// Modified by vii from komaruworld/mocktail. See README "About this fork".
#include "runtime/runtime_config.h"
#include "runtime/system_proxy.h"
@@ -74,13 +75,13 @@ TEST(RuntimeConfigTest, UsesSupportedDefaults) {
EXPECT_FALSE(config.fleasion_enabled());
EXPECT_TRUE(config.fleasion_valid());
EXPECT_FALSE(config.network_proxy().has_value());
EXPECT_FALSE(config.discord_rpc().enabled);
EXPECT_TRUE(config.discord_rpc().enabled);
EXPECT_TRUE(config.discord_rpc().show_place_name);
EXPECT_TRUE(config.discord_rpc().show_elapsed_time);
EXPECT_TRUE(config.discord_rpc().join_enabled);
EXPECT_TRUE(config.discord_rpc().public_servers_only);
EXPECT_EQ(config.discord_rpc().join_button_label, "Join Server");
EXPECT_EQ(config.discord_rpc().application_id, "1537088975720812655");
EXPECT_EQ(config.discord_rpc().application_id, "1549428231927500843");
EXPECT_EQ(config.discord_rpc().text.browsing, "Browsing experiences");
EXPECT_EQ(config.discord_rpc().text.joining, "Joining an experience");
EXPECT_EQ(config.discord_rpc().text.playing, "{place_name}");
+12 -8
View File
@@ -349,14 +349,18 @@ TEST(WebViewHelperLauncherTest, OwnsAndClosesTheExactSpawnedProcess) {
const std::filesystem::path helper = temporary.path() / "fake-helper";
const std::filesystem::path close_packet = temporary.path() / "close-packet";
const std::filesystem::path ready = temporary.path() / "ready";
ASSERT_TRUE(WriteExecutable(helper,
"#!/bin/sh\n"
"cat >/dev/null\n"
"touch '" +
ready.string() +
"'\n"
"dd bs=65548 count=1 status=none <&198 >'" +
close_packet.string() + "'\n"));
// dash rejects multi-digit file descriptors, so the control socket is read
// in python3 rather than through a `<&198` redirection.
ASSERT_TRUE(WriteExecutable(
helper, "#!/bin/sh\n"
"cat >/dev/null\n"
"touch '" +
ready.string() +
"'\n"
"python3 -c 'import socket,sys; s=socket.socket(fileno=198); "
"s.settimeout(2); "
"open(sys.argv[1],\"wb\").write(s.recv(65548))' '" +
close_packet.string() + "'\n"));
const WebViewHelperLaunchResult result =
LaunchWebViewHelper(helper, "https://www.roblox.com/login");