Compare commits

..
Author SHA1 Message Date
vii a3a2b76a06 feat(update): add an evidence-scoring solver for HostAbi addresses with a check command 2026-10-15 01:49:19 -05:00
vii f333399e18 test(update): add a 2908 to 3222 regression set for the HostAbi deriver
Share the input checks and FMOD slot tables so the solver reuses them.
2026-10-15 01:49:19 -05:00
vii 3062909209 refactor(jnivm): resolve method names once and log calls with no handler
GetMethodID maps each name to a JniMethod enum, so dispatch compares
numbers instead of strings. Calls that reach no handler print one
"no handler for" line per class, method and signature. Unknown
gameActivity_ callbacks are recorded on the CallVoidMethod path too.
The FMOD write path reuses a per-thread buffer instead of allocating.
2026-10-15 01:31:02 -05:00
vii 049784d0f8 fix(docs): restore the screenshots at full quality with transparency 2026-10-15 01:31:02 -05:00
vii ccf910f064 chore(ci): keep the signing and AUR keys in a release environment
The release environment only accepts main and v* tags, so a workflow pushed on another branch cannot read the keys. Flatpak builds that do not publish run without it.
2026-10-15 01:26:27 -05:00
vii 23e7831149 fix(site): keep the screenshots transparent
The first retake was flattened to RGB, which turned the transparent window edges black.
2026-10-13 21:34:07 -05:00
vii 5b2ee2c7f3 docs(site): rewrite the description and feature list, add SEO tags, refresh screenshots
New intro and feature list in the README and the site. The site gets a title with Roblox, link preview tags, structured data, a sitemap, robots.txt, a preview image and the Search Console tag. Screenshots are retaken, the old lobby shot is removed and the store listing points at the new ones.
2026-10-13 21:29:12 -05:00
vii 63ed62b4aa docs(faq): point the VR answer at Nightcap's vr branch 2026-10-13 20:21:33 -05:00
vii 81de528668 feat(packaging): publish nightcap and nightcap-bin to the AUR on release
Pushes nothing until the AUR_SSH_PRIVATE_KEY secret is set. Recipes move to 0.3.4.
2026-10-12 01:44:21 -05:00
vii ff81882cb0 docs: point Arch users to nightcap-git on the AUR
The package is maintained by @dim-ghub.
2026-10-12 01:44:21 -05:00
vii a87373d003 feat(assets): let the overlay replace files Roblox downloads
Roblox maps some rbxasset:// names to downloads and opens no local file for
them. For each file under content/ in the overlay folder, change its name in
the loaded libroblox so Roblox opens the local file.

Idea from @dim-ghub in #20.
2026-10-12 01:20:13 -05:00
vii b46098c211 feat(assets): trace asset lookups that find no file 2026-10-12 01:20:13 -05:00
vii 144350090e docs: describe the asset overlay folder 2026-10-11 23:58:26 -05:00
vii 3ed0418997 refactor(assets): share the asset lookup cache insert 2026-10-11 23:58:26 -05:00
vii 8410070eec Merge pull request #19 from dim-ghub/main
Add Sober-compatible asset overlay system
2026-10-11 23:56:58 -05:00
dim-ghub a1166fb6ba Add Sober-compatible asset overlay system 2026-10-11 23:11:54 -05:00
vii c9eb4fb7f0 docs(changelog): credit @qutad for the anti-aliasing fix 2026-10-10 22:02:40 -05:00
vii b59c070cc0 docs(changelog): release 0.3.4
Report Nightcap's own version instead of Mocktail's.
2026-10-10 21:27:00 -05:00
vii 4a9acf8c49 feat(site): redesign the landing page and legal pages 2026-10-10 21:07:33 -05:00
vii c342bab7b5 docs(changelog): note the voice chat fix after a Roblox update 2026-10-10 00:33:00 -05:00
vii a3b9c872a5 fix(update): derive the FMOD bridge from the shipped reference when the installed profile lacks it 2026-10-10 00:33:00 -05:00
vii 4f45baf3f7 docs(changelog): note the anti-aliasing and voice chat microphone fixes 2026-10-10 00:09:42 -05:00
vii 0ddb546ba8 chore(merge): merge upstream komaruworld/mocktail main 2026-10-10 00:09:42 -05:00
vii 7ba752ebb7 docs(changelog): bring the unreleased section up to date 2026-10-09 23:57:41 -05:00
vii 227895faa4 feat(window): use Wayland on NVIDIA Wayland sessions
The X11/XWayland fallback guarded against the explicit-sync disconnect
that is fixed, so the rule and its driver detection are removed.
2026-10-09 23:57:41 -05:00
vii dfc3c8d555 docs(changelog): note the Roblox 2.742 update and new interface fixes 2026-10-09 23:48:55 -05:00
vii e490dff97f feat(runtime): show the desktop side panel menu by default 2026-10-09 23:48:55 -05:00
vii b5b9990017 fix(services): let the BrowserTracker bootstrap use Roblox's storage directory
Tighten a group-writable directory this user owns, reject a storage file
this user cannot vouch for, and treat a stored ID of "0" as unset.
2026-10-09 23:48:55 -05:00
vii d278c0fe0a fix(runtime): run nativeAppBridgeAppStart by default
It starts Roblox's StartupController, which fetches the account's
flag-linked experiments and writes ClientSettings/IxpSettings.json.
2026-10-09 23:48:55 -05:00
vii 044f97e476 fix(update): accept payloads that reorder the leading constructors
Roblox 2.742 moves the process-load constructor behind the JNI singleton
initializer and renames registers in the registry initializer. The deriver
finds each leading constructor by its code, and the loader checks that host
bridges run the checkpoint and JNI singleton constructors and skip the two
wiring entries.
2026-10-09 23:48:55 -05:00
vii 321ba5b3a2 test(flatpak): keep a host flatpak-builder out of the missing-builder check 2026-10-09 14:19:01 -05:00
vii fd364a7bf2 docs: wrap the changelog at 110 columns and drop the glyphs roadmap item
Add a pre-commit check for added changelog lines over 110 characters. Roblox has no Nintendo glyph set, so the roadmap item cannot be met.
2026-10-09 13:50:46 -05:00
vii 5253563e3f fix(appimage): restore a distribution package when its reduced build is too old
A reduced gtk4 older than the one libadwaita requires failed the nightly configure step.
2026-10-09 13:49:38 -05:00
komaruworld 551d2a1829 Probably a vc fix 2026-10-09 15:49:35 +03:00
farwhile 37b88103f4 fix(runtime): stop forcing single-sample MSAA (#185)
* fix(runtime): stop forcing single-sample MSAA

* test removed

* Fix race in webview helper launcher test
2026-10-09 08:33:32 +03:00
vii 763faed0e3 fix(graphics): pick the Vulkan driver that matches the bound kernel driver
Skip a GPU whose kernel driver has no installed Vulkan driver and try the next one. Log why when none can run Vulkan.
2026-10-08 18:41:36 -05:00
komaruworld 74eedada0b improve evilbsd support
Netbsd hi
2026-10-09 00:45:05 +03:00
vii 6cb3dd00a9 fix(compat): bound Bionic strerror_r by errno table and gate atoi shim to non-glibc
strerror_r: Bionic's message table covers errno 0..133 except the
unassigned 41 and 58. Numbers outside that set now get "Unknown error N"
without consulting the host, so the text is the same on glibc and musl and
no longer relies on comparing against strerror_r(-1).

atoi: glibc's atoi is (int)strtol() like Bionic's, so the host symbol is
used there as before. The Bionic shim is registered only on libcs where
that is not true (musl). linker_registry_test expects the matching result.
2026-10-08 16:10:03 -05:00
vii fe2f641a9e chore(merge): merge upstream komaruworld/mocktail main
Brings in 3e6f63e (drop libexecinfo), aa31cdf (Chimera/musl portability),
bc10810 (Bionic atoi, canary signal exit codes) and cb2e9dd (CI fixes).

Conflicts:
- tests/native_update_test.cc: keep both <cerrno> and <csignal>.
- tests/webview_helper_launcher_test.cc: keep the existing python3 socket
  read; it already solves the dash multi-digit fd redirection problem that
  upstream fixed by switching the helper to bash.
2026-10-08 16:10:03 -05:00
komaruworld cb2e9dd598 Small ci fix 2026-10-08 21:12:29 +03:00
komaruworld bc108106db mog chimera 2026-10-08 18:14:23 +03:00
komaruworld aa31cdf395 Improve chimera support 2026-10-08 16:59:11 +03:00
komaruworld 3e6f63ea93 remove libexecinfo dependency 2026-10-08 16:13:41 +03:00
vii bfa6811417 docs(changelog): note the crash and hang fixes 2026-10-07 14:22:39 -05:00
vii 84502c0a1a fix(window): show the hidden window from the main thread
The render thread called SDL_ShowWindow outside the surface commit gate. It now sets a flag, and PumpEvents shows the window inside the gate.
2026-10-07 14:20:16 -05:00
vii 46d1d5ffbe fix(graphics): retry swapchain creation with the game's present mode
A driver can reject a present mode the device did not enable. Retry once with the mode the game asked for, except after errors another mode cannot fix.
2026-10-07 14:20:16 -05:00
vii 545fd8344f fix(session-log): stop waiting for the log writer after 2 seconds
A child that kept the log pipes open made Stop wait forever. Poll for the writer, then SIGKILL it after 2 seconds.
2026-10-07 14:20:16 -05:00
vii 4c40fcdc99 fix(jnivm): stub missing JNI functions and ignore repeat releases
Fill the 65 unimplemented JNI table slots with stubs that log once and return zero, so a call no longer jumps to address 0. A release of a handle with no reference count now returns, so a double release cannot queue one slot twice and give two objects the same handle.
2026-10-07 14:20:16 -05:00
vii d4c7a6f741 fix(app-shell): answer LAUNCH_CONVERSATION so chat requests open 2026-10-07 13:04:31 -05:00
vii 491925f59e test(graphics): register texture_override_test with ctest 2026-10-07 12:01:14 -05:00
vii 2917bc5423 chore(merge): merge upstream komaruworld/mocktail main 2026-10-07 12:01:14 -05:00
komaruworld 2451264696 ETC2 texture fixes
Refs #191
2026-10-07 19:02:35 +03:00
komaruworld a9a96c6861 Add zsync file
fixes #182
2026-10-07 17:35:33 +03:00
vii 5bf4eeb41b fix(window): keep main-thread surface commits out of host present
With explicit sync, a wl_surface commit from the SDL thread can land
between the driver's sync points and its buffer attach. The compositor
answers with a no_buffer protocol error and disconnects the client.
2026-10-06 19:24:27 -05:00
vii d937dd7ea6 fix(flatpak): ask for the X11 socket only as a fallback 2026-10-04 21:08:23 -05:00
vii 169e65bf60 docs(changelog): release 0.3.3 2026-10-04 20:44:33 -05:00
vii 69476f0f47 docs(changelog): add theme crash and Flatpak NVIDIA fixes 2026-10-04 20:14:25 -05:00
vii 79d0cdfb40 chore(merge): merge upstream komaruworld/mocktail main
Keep our captured-pointer handling in place of upstream's relative_mode
version. Keep upstream's motion coalescing guard on our relative flag.
2026-10-04 20:14:13 -05:00
komaruworld 273209b9ea Fix angle platform selection 2026-10-03 21:19:26 +03:00
komaruworld bbfd603e17 Fix captured mouse input
Refs; #135
2026-10-03 14:28:12 +03:00
komaruworld 1ba2941e7f Fix theme crash
Fixes #144
2026-10-03 14:03:25 +03:00
komaruworld 68c90ce428 flatpak nvidia xwayland fallback
#186
2026-10-03 13:48:39 +03:00
vii 068f07e125 chore(merge): merge upstream komaruworld/mocktail main 2026-10-02 20:02:32 -05:00
vii a469689bbb fix(webview): use GTK's software renderer when the helper composites in software
GTK's GL renderer aborted in libepoxy while snapshotting a page switch, which closed the web view. GSK_RENDERER set by the user still wins.
2026-10-02 20:02:32 -05:00
vii 51836a980d docs(changelog): add follow-user, Nix desktop entry, text focus and sky fixes 2026-10-02 20:02:32 -05:00
vii ad546a7781 Merge pull request #16 from kimlherme/docs/recovery-marker-cleanup
docs: document leftover recovery marker cleanup
2026-10-02 19:37:49 -05:00
kimlherme 3c45450dfc docs: document recovery marker cleanup 2026-10-02 20:21:34 -03:00
vii 185c11b57f test(update): expect a missing abi to count only on x86_64 hosts 2026-10-02 12:52:16 -05:00
vii 2c3d7b485e test(update): declare the guest ABI in test catalogs so aarch64 matches
Profiles without abi default to x86_64, so every payload store test skipped its catalog on arm64.
2026-10-02 12:44:47 -05:00
komaruworld 6f8344a28b Sky fixes 2026-10-02 16:52:27 +03:00
vii b60c43e8f0 chore(merge): merge upstream komaruworld/mocktail main
Keep both the CallProtocol and AccountProtocol bridges. Keep our pointer
hold and catalog validation. Re-apply our JNI local-frame counting,
singleton handles, class handle cache, handle-table report and thread
detach release on upstream's split jnivm files.
2026-10-01 21:41:43 -05:00
vii b5e4273277 docs(changelog): add updater, audio and platform fixes 2026-10-01 19:26:21 -05:00
vii d2858fedc8 Merge pull request #13 from kimlherme/fix/payload-update-correctness
fix(update): preserve rollback state and validate payload activation
2026-10-01 19:25:58 -05:00
vii 326a0659ea Merge pull request #15 from kimlherme/fix/platform-lifecycle-compatibility
fix(platform): correct Vulkan teardown, pointer capture and DNS flags
2026-10-01 19:25:51 -05:00
vii dd45467056 Merge pull request #14 from kimlherme/fix/audio-jni-ownership
fix(runtime): preserve OpenSL tickets and JNI array references
2026-10-01 19:25:40 -05:00
vii 94b1036296 docs(changelog): add ETC2 decode speedup 2026-10-01 19:14:37 -05:00
vii 4c61daad0c Merge pull request #10 from glook9001/simd-etc2-decoder
perf(graphics): SIMD PSHUFB ETC2/EAC decode with bit-exact scalar verification
2026-10-01 19:14:33 -05:00
kimlherme f5f224875a fix(platform): correct Vulkan teardown, pointer capture and DNS flags 2026-10-01 21:05:31 -03:00
kimlherme 90d4891a4e fix(update): preserve rollback state and validate payload activation 2026-10-01 21:05:30 -03:00
kimlherme 46dd99266c fix(runtime): preserve OpenSL tickets and JNI array references 2026-10-01 21:05:30 -03:00
77fce18a95 po co chodzic, mozna latac (#183)
* po co chodzic, mozna latac

* Fix jni ownership, split helpers

---------

Co-authored-by: Mia <uwie@thinkwise.ro>
Co-authored-by: komaruworld <rtysus228@gmail.com>
2026-10-01 20:31:52 +03:00
komaruworld d6f0f2c755 Fix some vc
Fokora
2026-10-01 18:03:02 +03:00
glook9001 c6afc5af24 perf(graphics): eliminate crop page faults with strided resampling and streaming WC loads 2026-10-01 12:08:54 +03:00
glook9001 74e7455ee1 perf(graphics): SIMD PSHUFB ETC2/EAC decode with bit-exact scalar verification 2026-10-01 12:08:35 +03:00
vii 000d021452 docs(changelog): add payload import fix 2026-09-30 21:34:06 -05:00
vii 2eef2c3821 Merge pull request #11 from kimlherme/fix/payload-import-rollback
fix(payload): preserve the previous payload on failed imports
2026-09-30 21:33:43 -05:00
Guilhermepycharm 4928443f90 fix(payload): preserve previous files across failed imports
Enable rollback before the first backup move, preserve originals during incomplete backups, and retain recovery files if restoration fails. Add offline full-importer regression coverage and register it with CTest.
2026-09-30 22:54:33 -03:00
native b37497db7b fix text overlay again (#179) 2026-09-30 17:55:30 +03:00
Lupey 6bbdc39d7e feat(nix): add desktop application (#177) 2026-09-30 17:48:05 +03:00
komaruworld 1c11336ba1 Fix text issues
bla bla bla
2026-09-30 16:56:48 +03:00
vii b19bd8406f docs(changelog): release 0.3.2 2026-09-29 23:33:26 -05:00
vii cd46538eb8 docs(readme): document Flatpak build job settings 2026-09-29 23:27:33 -05:00
vii eca5f395eb update(flatpak): default local builds to 2 jobs 2026-09-29 23:25:03 -05:00
vii 26767e925c fix(webview): keep Roblox's WebViewProtocol alive so login challenges finish
Roblox's WebViewProtocol is a counted subsystem that Android's app start
holds for the process lifetime. Nightcap held no handle, so the protocol
and its handleJavascriptCallback subscriber were deleted after each use.
Page messages such as challengeCompleted then waited for the next Lua
WebView call, which came only at the 60 s challenge timeout, and login
failed.

Find the unexported getter through its call site in
initializeAndroidWebViewProtocol, check both functions against x86-64
and AArch64 code contracts, and hold one handle before native init.

The helper no longer closes a solved challenge before the host reads its
events. Roblox closes WebViewProtocol challenges itself, with a 3 s host
fallback; OPEN_CAPTCHA_VIEW windows close once success is forwarded.
Solved-challenge feature names now live in one shared header.
2026-09-29 22:09:27 -05:00
vii 01a0e40463 docs(changelog): add unreleased login, input and ARM64 entries 2026-09-29 18:24:25 -05:00
glook9001 a11e833e06 fix(webview): handle challengeCompleted event to acknowledge callback and close captcha window 2026-09-29 18:21:45 -05:00
vii b9bce36824 docs(roadmap): move first-load stutter to shipped 2026-09-29 17:36:41 -05:00
vii 14c8a55211 docs(github): adapt issue and PR templates for Nightcap 2026-09-29 17:36:41 -05:00
vii 5c5422fa02 fix(login): answer AccountProtocol device-integrity requests
Roblox login challenges asked for a Play Integrity token that nothing
answered, so the challenge sat out LuaApp's native timeout and looped.
A new optional bridge reports integrity as unsupported and answers token
requests at once with an empty token (TOKEN_PROVIDER_UNINITIALIZED).
2026-09-29 16:37:12 -05:00
vii 9b041ec2ac fix(webview): allow 5 s for helper startup and log its timing
Slow starts (cold Flatpak) could exceed the 3 s ready deadline and kill
login verification windows. Launch and timeout logs now report elapsed ms.
2026-09-29 16:05:01 -05:00
vii 5f83c4a64f docs(readme): center the logo, title, and badges 2026-09-26 04:07:01 -05:00
vii a9d8e3185e chore(merge): merge upstream komaruworld/mocktail main 2026-09-26 03:59:06 -05:00
vii 67a9f4db97 fix(input): advance the TextBox caret past trailing whitespace (#148)
The final cluster rect SDL_ttf reports stops at the last inked glyph, so a
caret at the end of a single-line field did not move when a trailing space
was typed. Use the laid-out width there; wrapped layouts keep the clamp
because their width is the widest line rather than the last one.
2026-09-25 18:40:23 +03:00
farwhile f07daccc82 fix(input): map middle click to MouseButton3 (#171) 2026-09-25 18:39:24 +03:00
珍惜andHttpAnimations 2eef4f966f Reject partial or overflowing user ids (#168)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-25 18:37:33 +03:00
vii 41003efb09 docs(changelog): record the private server join fix 2026-09-24 23:35:46 -05:00
vii 29a0f03a90 fix(webview): launch web Play buttons without waiting for navigation
Roblox defers a launchGame sent from the WebView until the Lua app next
navigates, so joining a private server stalled until a sidebar tab was
clicked. Parse launchGame in the runtime, queue it through the
experience launch pipeline, and close the WebView. Other commands still
reach Roblox.

Fixes #169
2026-09-24 23:02:48 -05:00
vii 5a27aa0366 fix(runtime): build engine pump rest on aarch64 hosts 2026-09-24 18:01:41 -05:00
vii c99a55dc54 chore(merge): merge upstream komaruworld/mocktail main
Brings in aarch64 host support (#152, #162, #167), natural scrolling (#163)
and the arm64 CI build job. Packaging, Pages and README keep the Nightcap
x86_64 side; upstream's aarch64 bundles are not published.
2026-09-24 17:44:28 -05:00
珍惜andHttpAnimations 30d50f15d1 Fix catalog unable to tell arm64 from x86_64 profiles for the same version. (#167)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-24 20:12:18 +03:00
komaruworld ab940acfb5 add arm64 ci with build
fog, update faq
2026-09-24 19:03:22 +03:00
farwhile 719b1fdd22 fix/respect host natural scrolling direction (#163) 2026-09-23 18:31:41 +03:00
珍惜andHttpAnimations 1fa2e116b3 Fix crashing on x86 hosts (#162)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-23 12:04:53 +03:00
vii 35359f3575 perf(etc2): carve upload staging out of shared blocks
Each ETC2 upload created and destroyed its own staging buffer on the render
thread. Uploads now take ranges of 16 MiB blocks, reused once every range
is released; four empty blocks are kept. Profile traces gain per-frame CPU
time counters and slices for staging and driver memory calls.
2026-09-22 21:31:58 -05:00
珍惜andHttpAnimations 40237fae72 arm 64 (#152)
Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
2026-09-21 20:09:37 +03:00
vii ad6a3c3147 docs(readme): match the DEB filename that CPack produces 2026-09-20 13:06:45 -05:00
vii d982cd9506 Merge pull request #2 from CoderDayton/release/0.3.1
Release 0.3.1: packaging, input and shutdown fixes
2026-09-20 12:53:44 -05:00
vii 65adb3bddc fix(packaging): fail the release when a package glob matches nothing
An unmatched glob reached sha256sum and gh as literal text. The publish job
now collects the assets into an array and stops if a package is missing.
2026-09-20 12:33:26 -05:00
vii 47e23df2e0 docs(changelog): release 0.3.1 2026-09-20 12:31:39 -05:00
vii c0ff1842ee docs(changelog): record the packaging, input and shutdown fixes 2026-09-20 12:30:00 -05:00
vii e073947b91 fix(window): keep the pointer capture owner alive across shutdown
Cross-thread callers take a strong reference under a mutex, so a shutdown
racing SetGameSessionActive no longer frees the owner mid-call.
2026-09-20 12:26:39 -05:00
vii 66de662e5b fix(input): decide pointer capture from state, not coordinates
The router inferred relative mode from SDL reporting zero absolute
coordinates. X11 does; Wayland reports a position that accumulates the
deltas and leaves the window, so every camera-drag event took the
absolute branch and walked Roblox's cursor to the viewport edge.

Motion, button and wheel events now carry the window's relative mouse
mode, and the RMB camera-drag fallback only captures inside an
experience so the app surface keeps a free cursor.
2026-09-20 12:05:48 -05:00
vii 6c3631058c chore(docs): ignore the working plans directory 2026-09-20 12:05:47 -05:00
vii 6829fd00ed perf(graphics): decode ETC2 uploads off the submitting thread
Uploads decode on a background dispatcher and the submit carries a
timeline semaphore wait, so the GPU waits for the texture rather than
the render thread. The gather of the application's compressed bytes
moves there too when they are already mapped; unmapping or freeing that
memory drains the decodes still reading it.

Staged copies gain an explicit host-write barrier. A queue submission
only makes host writes from before it visible to the device, and these
writes land after the submit.

Devices without timelineSemaphore, and submits whose pNext already sizes
arrays by their semaphore counts, decode inline as before.

Blade Ball, median of three: worst vkQueueSubmit 140ms -> 1ms, p99
4.9ms -> 0.05ms, total 2534ms -> 309ms. Frame interval unchanged; its
tail is the engine's own main-thread step.
2026-09-19 18:02:16 -05:00
vii 8be3618cd5 feat(packaging): ship DEB, RPM and pacman packages with each release
The release workflow now builds the three native packages beside the
AppImage and attaches them to the GitHub release with a checksum each.
Nothing is pushed to a distribution repository.

Packages are named nightcap and take their version from the release tag
instead of upstream's project version, so they no longer claim to be
Mocktail 1.0.4. Each variant conflicts with the others because they all own
/usr/bin/mocktail.

The AUR workflow no longer clones or pushes aur.archlinux.org; it refreshes
nightcap-git metadata on demand and uploads it as an artifact. The recipes
move to packaging/aur/nightcap{,-bin,-git}, point at this repository, and
version from the newest release tag.
2026-09-19 01:12:12 -05:00
vii 265b476440 docs(packaging): list Nightcap releases in the AppStream metadata
The releases block carried upstream's 1.0.x history, so a software centre
read the app as newer than every Nightcap tag and would never offer 0.3.x as
an update. Describe the 0.x line instead, and name Nightcap in the project
description.
2026-09-19 01:11:16 -05:00
vii ff905fe5b7 test(input): pin the caret position before a trailing space
SDL_ttf marks only the zero-length substring past the end of the text with
TTF_SUBSTRING_TEXT_END, so a caret left of a trailing space keeps the cluster
position. Cover both carets so the end-of-text branch cannot swallow the one
beside it.
2026-09-19 01:10:54 -05:00
vii 02108a9bb1 perf(input): reuse the TextBox overlay fonts across rasters
Every keystroke re-ran TTF_Init, a fontconfig sort, six TTF_OpenFont calls
and TTF_Quit on the render thread. Hold the opened fonts on the overlay and
reopen them only when the Roblox font or the point size changes, keeping the
current set when a reopen fails. Measured over 100 rasters, 1.59 ms to
0.12 ms per keystroke.
2026-09-19 00:32:31 -05:00
vii 7dd32335ca fix(input): advance the TextBox caret past trailing whitespace
The final cluster rect SDL_ttf reports stops at the last inked glyph, so a
caret at the end of a single-line field did not move when a trailing space
was typed. Use the laid-out width there; wrapped layouts keep the clamp
because their width is the widest line rather than the last one.
2026-09-19 00:32:31 -05:00
vii 5ed6ed5e84 chore(packaging): declare the libpng dependency
Texture overrides decode PNG through libpng, which stubs/CMakeLists.txt
already requires. Lists it in the Arch, AUR, and Nix packages, the thin
dependency installer, and the README build instructions.
2026-09-18 19:30:58 -05:00
vii cef1b3ec4a Merge upstream komaruworld/mocktail into main 2026-09-18 19:29:24 -05:00
vii 8bde1a1c0d Merge pull request #1 from CoderDayton/release/0.3.0
Release 0.3.0
2026-09-18 19:19:48 -05:00
vii 60e9c5c3ef docs(changelog): release 0.3.0 2026-09-18 19:06:08 -05:00
vii d76e1b6f54 docs(roadmap): move profiling and Discord presence to shipped, add first-load stutter 2026-09-18 19:06:02 -05:00
vii 33c06c063e docs(changelog): note the Sober-style Discord card defaults 2026-09-18 18:54:57 -05:00
vii 9e8afb233a feat(discord-rpc): show the Roblox badge as the default small image
Defaults images.small to the roblox_small asset on the bundled Discord
application, with "Roblox" as its hover text.
2026-09-18 18:54:12 -05:00
vii 7fda1eb239 feat(discord-rpc): show the experience creator and title the card Roblox
Default title is Roblox, details "Playing {place_name}", and state
"by {creator_name}". The creator comes from the games API response
already fetched for the place name. A text field that expands an
empty placeholder is left out, so the state never shows a bare "by".
2026-09-18 18:45:11 -05:00
vii f128e28b83 docs(perf): describe the main-thread pump rest modes and GameMode's governor permission 2026-09-18 18:26:32 -05:00
vii 718e67f9e1 perf(runtime): rest the engine pump between polls instead of spinning
The main thread polls nativeCallMessagesFromMainThread 146k times a second
with nothing to do. Between polls it now rests 100 us: nanosleep when the
governor is performance, TPAUSE (C0.2) when the CPU has WAITPKG, a spin
otherwise. Sleeping under a power-saving governor lets the render thread's
core clock down and drops frames, which is why TPAUSE is the default there.
MOCKTAIL_ENGINE_PUMP_REST=sleep|tpause|spin overrides. The profile trace
gains a slice for pumps that ran a message.
2026-09-18 15:36:54 -05:00
vii c6c354fdb1 perf(graphics): emit ETC2 uploads on the decode pool and stop the trace writer after a failed write
EmitPass reuses the decoder's persistent workers through RunOnDecodeWorkers
instead of creating and joining threads per batch. A failed trace write stops
the writer thread rather than leaving it ticking every 250 ms.
2026-09-18 12:42:36 -05:00
vii 25930d14ff fix(graphics, runtime): honor ETC2 source stride and cut main-loop overhead
ETC2 uploads read bufferRowLength/bufferImageHeight instead of assuming
packed rows. MakeFileObject retains the path field. The main-thread pump
caches its JNI attach, but not the GetJNIEnv fallback, so a VM swap retries.
An idle launch-broker drain skips both locks.
2026-09-18 12:21:29 -05:00
vii 65030d8df1 fix(input): hold the pointer position during relative mouse capture
Relative-mode motion carries zero absolute coordinates. The router used
to accumulate the deltas into its pointer position, so a long camera turn
parked the reported position at the viewport edge and Roblox drew its
cursor there when the capture ended. The position now stays where it was
captured while deltas pass through. Zero-coordinate wheel and button
events keep it too, and an unknown position defaults to the viewport
center.
2026-09-18 02:51:03 -05:00
vii 0be5e808c5 fix(runtime): restore the affinity of every thread, not just the caller
sched_setaffinity binds one thread, so the fallback that undid a GameMode pin
only freed the thread that called Start(); the worker threads the fix exists to
protect stayed on the daemon's cores. Walk /proc/self/task instead, and treat a
readdir failure partway through as a failed restore rather than a finished one.

Also stop chrome_trace_writer emitting a valueless "ts" on to_chars failure, or
a bare inf/nan, either of which costs the whole trace instead of one field.
2026-09-18 02:00:58 -05:00
vii 1b030a57eb perf(runtime, graphics): release core-pinning gamemode and parallelize texture uploads
Release the GameMode request when the daemon narrows the CPU mask; the pin
concentrates Roblox's worker pool and is re-applied every few seconds, so
ending the request is the only thing that lifts it. `gamemode: on` keeps it.

Emit texture uploads on worker threads instead of serially inside
vkQueueSubmit, in two passes so mip 0 lands before the levels scaled from it.

Decode ETC2 through a persistent pool and lower the parallel floor to one
128x128 image, so small batches stop running single-threaded.

Document all three in docs/PERFORMANCE.md.
2026-09-18 00:47:57 -05:00
vii 2f5527b8b5 perf(libc_shim): cut path translation cost per file call
Mappings are kept sorted longest prefix first, unmapped paths skip allocation, and the path trace switch is read once.
2026-09-16 21:22:31 -05:00
vii 579cedbf1d perf(textures): pool ETC2 staging buffers and speed up resampling
Staging buffers stay mapped and are reused instead of allocated per upload. Box resampling precomputes column spans and copies repeated rows, with identical output.
2026-09-16 21:22:31 -05:00
vii a7b90e5919 fix(jnivm): stop leaking JNI handles until the table fills
FindClass and GetObjectClass reuse one handle per class, detached threads release their local refs, and local ref deletes and singleton checks no longer scan. A full table logs the classes holding handles. Stale handles above slot 4096 no longer read as C strings.
2026-09-16 21:22:31 -05:00
vii 3bba795ea3 feat(profile): add --profile Chrome trace of Vulkan adapter work
Records presents, GPU waits, submits, ETC2 decode, and pipeline and shader creation for ui.perfetto.dev. Adds a benchmark guide and a trace summary script.
2026-09-16 21:22:30 -05:00
vii 5ce4487d4f feat(discord): add custom presence title, state, and icons
text.title sets the activity name, images.* set the large and small icons and hover text, and text.state may be empty to hide it. Text and image fields expand {place_name} and {place_icon}; fields that render empty are omitted. show_place_name: false now also hides the place thumbnail.
2026-09-16 11:45:30 -05:00
vii c544389dd0 fix(textures): make the crosshair arms symmetric
Move the right and bottom arms out 1px so every arm reaches 12px from the center pixel with a 4px gap.
2026-09-16 11:11:06 -05:00
vii bdb84142d7 docs(textures): ship a crosshair override people can copy in
Adds textures/ with the crosshair PNG named by its ETC2 hash and a README
covering install, naming and how to dump hashes for other textures. Nothing
installs or loads it; copying into ~/.config/mocktail/textures is opt-in.
2026-09-16 04:53:53 -05:00
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
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
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
vii a9987546d5 fix(packaging): refresh AnyLinux debloated package hashes to 2026-09-15
pkgforge rebuilt six of the eight rolling packages, so the AppImage build
failed on the ffmpeg-mini checksum.
2026-09-15 08:10:55 -05:00
vii 6a39887e8f docs(roadmap): add Nightcap roadmap and link it from the README 2026-09-15 08:04:17 -05:00
vii 349e314850 feat(packaging): rename app ID to io.github.CoderDayton.nightcap and publish Flatpak
Rename the desktop entry, metainfo, icons, Flatpak manifest, and SDL app
identity so Nightcap installs side by side with upstream. Build a signed
Flatpak on v* tags, publish the OSTree repo and landing page to GitHub
Pages, and attach the bundle to the release. Release creation tolerates
the AppImage and Flatpak workflows racing on the same tag.
2026-09-15 07:10:26 -05:00
vii b1112d8cdc feat(graphics): default small texture upscale to 4
Unset or non-numeric MOCKTAIL_SMALL_TEXTURE_UPSCALE now means 4, so the
upscale applies on every launch path. Drop the variable from the desktop
entry Exec line.
2026-09-15 03:49:13 -05:00
vii 27c383a5d9 feat(branding): recolor app icon to the Nightcap purple palette
Indigo #2F2A6B, violet #6C4FD1, lavender #A78BFA. PNGs rendered with resvg.
2026-09-14 22:26:10 -05:00
vii 7cf24feef6 feat(branding): rename fork to Nightcap and add AppImage release workflow
Show Nightcap as the app name in the desktop entry, window title,
dialogs, and metainfo. Binary, config paths, and desktop ID keep the
mocktail name. Publish Nightcap-x86_64.AppImage to GitHub Releases on
v* tags. README points at the fork's releases instead of upstream
packages.
2026-09-14 22:19:19 -05:00
vii 4443766d39 chore(hooks): add lefthook config mirroring CI repository checks 2026-09-14 18:19:15 -05:00
vii ea2166b09a docs(license): mark files modified from upstream per Apache 2.0
Add a modified-by notice to every upstream file this fork changes.
Ignore build-test/.
2026-09-14 18:08:12 -05:00
vii 9e4e69be1c docs(readme): describe fork changes and replace dead FAQ link with settings 2026-09-14 16:48:00 -05:00
vii dd0655bfde Merge branch 'pc-improvements' into main
Resolve src/update/host_abi_deriver.cc: keep upstream's FMOD vtable layout
versions and input-method derivation inside the optional output-device
bridge, so a mismatch omits the bridge instead of failing derivation.
2026-09-14 16:09:33 -05:00
vii d2d275706b perf(graphics): present through fifo-latest-ready by default to cut input latency
Roblox requests FIFO with 3 swapchain images, which queues up to two frames
before display. The default vsync policy (auto, unmanaged frame cap) now
resolves to the vsync policy so the lowest-latency synchronized present mode
the host offers is used.
2026-09-14 15:51:35 -05:00
vii b7b63dd30f feat(packaging): launch with MOCKTAIL_SMALL_TEXTURE_UPSCALE=4 from the desktop entry 2026-09-14 10:10:40 -05:00
vii aacab4036c feat(graphics): replace and upscale small ETC2 textures
Level-0 uploads are hashed against the override store; MOCKTAIL_SMALL_TEXTURE_UPSCALE
creates colour images up to 64px N times larger, snaps block-rounded regions to the
host mip, fills every host mip from level 0 and turns image copies touching scaled
images into blits. MOCKTAIL_ADVERTISE_ETC2=0 hides the feature bit.
2026-09-14 10:10:40 -05:00
vii e104b1d936 feat(graphics): add PNG texture override and dump store keyed by upload hash
Adds libpng, MOCKTAIL_TEXTURE_OVERRIDE_DIR (default ~/.config/mocktail/textures)
and MOCKTAIL_TEXTURE_DUMP_DIR.
2026-09-14 10:10:40 -05:00
vii bf6a6b417d fix(packaging): write the install path into the installed desktop entry 2026-09-14 04:53:50 -05:00
vii dfd35b8cc8 feat(packaging): launch on Wayland from the desktop entry 2026-09-14 04:44:25 -05:00
vii 1632681e6c fix(jnivm): answer NativeQuoteInterface.requestResponse so servers stop kicking with 319
The client asks Java for a keystore attestation quote after joining. With no
handler the call returned null, the server never got an answer and dropped
the session with error 319. Reply with the no-keystore form [1, 0, message].
2026-09-14 04:44:25 -05:00
vii 88d10d53f2 perf(graphics): decode ETC2 uploads off mapped memory on worker threads
Compressed bytes are copied to heap scratch, decoded heap to heap across
up to 8 threads in block-row bands, and streamed into the staging buffer.
Decoding straight from uncached Vulkan memory caused 200-460 ms hitches.
The FPS trace now reports the longest frame gap per window.
2026-09-14 04:18:43 -05:00
vii eae9fa7030 feat(graphics): emulate ETC2/EAC textures on Vulkan
Hosts without native ETC2 now report ETC2/EAC support. Uploads into emulated images are decoded to RGBA8/R16 staging buffers at submit. MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns it off.
2026-09-13 11:45:28 -05:00
vii ad76f68224 fix(runtime): report real host memory and display size
DeviceParams now carry host RAM instead of a low-RAM device profile, and screen, window and configuration sizes follow the host display and window with a 1920x1080 fallback.
2026-09-13 11:45:28 -05:00
vii d783793b4e fix(update): make the FMOD output-device bridge optional
Candidate payloads without a matching FMOD output-device signature now derive without the bridge instead of failing activation.
2026-09-13 11:45:28 -05:00
306 changed files with 27785 additions and 4813 deletions

No files matched your search

+8 -7
View File
@@ -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`.
+5 -4
View File
@@ -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
+2 -1
View File
@@ -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 #
+119
View File
@@ -0,0 +1,119 @@
name: Publish AUR packages
on:
workflow_dispatch:
inputs:
tag:
description: Release tag to publish, such as v0.3.4
required: true
type: string
workflow_call:
inputs:
tag:
required: true
type: string
secrets:
AUR_SSH_PRIVATE_KEY:
required: false
permissions:
contents: read
concurrency:
group: aur-publish
cancel-in-progress: false
jobs:
publish:
name: Publish nightcap and nightcap-bin
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: release
steps:
- name: Check out the release
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.tag }}
persist-credentials: false
- name: Render the recipes
shell: bash
env:
TAG: ${{ inputs.tag }}
run: |
set -Eeuo pipefail
[[ "$TAG" =~ ^v[0-9]+(\.[0-9]+)*$ ]]
version="${TAG#v}"
archive="nightcap-${version}-1-x86_64.pkg.tar.zst"
# nightcap-bin installs the pacman package from the release page, so
# its checksum is the one published beside that asset.
checksum="$(curl -fsSL \
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${archive}.sha256" |
awk '{print $1}')"
[[ "$checksum" =~ ^[0-9a-f]{64}$ ]]
sed -i "s/^pkgver=.*/pkgver=${version}/" \
packaging/aur/nightcap/PKGBUILD packaging/aur/nightcap-bin/PKGBUILD
sed -i "s/^sha256sums=('SKIP')$/sha256sums=('${checksum}')/" \
packaging/aur/nightcap-bin/PKGBUILD
grep -Fqx "sha256sums=('${checksum}')" packaging/aur/nightcap-bin/PKGBUILD
docker run --rm \
--volume "$GITHUB_WORKSPACE/packaging/aur:/aur" \
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
useradd --create-home builder
for package in nightcap nightcap-bin; do
install -d -o builder -g builder "/tmp/${package}"
install -o builder -g builder -m 0644 \
"/aur/${package}/PKGBUILD" "/tmp/${package}/PKGBUILD"
runuser -u builder -- bash -c \
"cd /tmp/${package} && makepkg --printsrcinfo" \
> "/aur/${package}/.SRCINFO"
done
'
for package in nightcap nightcap-bin; do
grep -Fqx "pkgver=${version}" "packaging/aur/${package}/PKGBUILD"
grep -Fqx $'\tpkgver = '"${version}" "packaging/aur/${package}/.SRCINFO"
done
echo "VERSION=${version}" >> "$GITHUB_ENV"
- name: Push to the AUR
shell: bash
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
run: |
set -Eeuo pipefail
if [[ -z "${AUR_SSH_PRIVATE_KEY}" ]]; then
echo "::notice::AUR_SSH_PRIVATE_KEY is not set; nothing was pushed to the AUR"
exit 0
fi
ssh_dir="$(mktemp -d)"
trap 'rm -rf "$ssh_dir"' EXIT
install -m 0600 /dev/null "$ssh_dir/key"
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$ssh_dir/key"
# The AUR lists this key's fingerprint on its front page:
# SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4
printf '%s\n' \
'aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN' \
> "$ssh_dir/known_hosts"
export GIT_SSH_COMMAND="ssh -i $ssh_dir/key -o IdentitiesOnly=yes -o UserKnownHostsFile=$ssh_dir/known_hosts -o StrictHostKeyChecking=yes"
for package in nightcap nightcap-bin; do
checkout="$ssh_dir/$package"
git clone "ssh://aur@aur.archlinux.org/${package}.git" "$checkout"
cp "packaging/aur/${package}/PKGBUILD" "packaging/aur/${package}/.SRCINFO" \
"$checkout/"
git -C "$checkout" add PKGBUILD .SRCINFO
if git -C "$checkout" diff --cached --quiet; then
echo "::notice::${package} is already at ${VERSION}"
continue
fi
git -C "$checkout" \
-c user.name='Nightcap release' \
-c user.email='CoderDayton@users.noreply.github.com' \
commit --quiet -m "Update to ${VERSION}"
# The AUR only accepts the master branch.
git -C "$checkout" push origin HEAD:master
done
+16 -74
View File
@@ -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
+61
View File
@@ -8,22 +8,32 @@ on:
- 'CMakeLists.txt'
- 'Makefile'
- 'README.md'
- '.gitmodules'
- 'cmake/**'
- 'config/**'
- 'include/**'
- 'packaging/**'
- 'scripts/**'
- 'src/**'
- 'stubs/**'
- 'tests/**'
- 'third_party/**'
pull_request:
paths:
- '.github/workflows/**'
- 'CMakeLists.txt'
- 'Makefile'
- 'README.md'
- '.gitmodules'
- 'cmake/**'
- 'config/**'
- 'include/**'
- 'packaging/**'
- 'scripts/**'
- 'src/**'
- 'stubs/**'
- 'tests/**'
- 'third_party/**'
workflow_dispatch:
permissions:
@@ -65,3 +75,54 @@ jobs:
bash tests/flatpak_packaging_test.sh .
bash tests/native_packaging_test.sh .
bash tests/anylinux_dependencies_test.sh .
native-build:
name: Native build (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: ubuntu-26.04
- arch: aarch64
runner: ubuntu-26.04-arm
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: recursive
- name: Install build dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
build-essential binutils cmake git ninja-build pkg-config lld \
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \
nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \
libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \
libutf8proc-dev libfontconfig1-dev libegl-dev libvulkan-dev \
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=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
+70 -68
View File
@@ -1,9 +1,9 @@
# Modified by vii from komaruworld/mocktail. See README "About this fork".
name: Flatpak
on:
workflow_run:
workflows: [Native packages]
types: [completed]
push:
tags: ['v*']
pull_request:
paths:
- '.github/workflows/flatpak.yml'
@@ -15,13 +15,11 @@ on:
- 'config/**'
- 'include/**'
- 'packaging/flatpak/**'
- 'packaging/discord-join.html'
- 'packaging/icons/**'
- 'packaging/space.bigrat.mocktail.desktop'
- 'packaging/space.bigrat.mocktail.metainfo.xml'
- 'packaging/space.bigrat.mocktail.svg'
- 'packaging/io.github.CoderDayton.nightcap.desktop'
- 'packaging/io.github.CoderDayton.nightcap.metainfo.xml'
- 'packaging/io.github.CoderDayton.nightcap.svg'
- 'scripts/assemble_flatpak_pages.sh'
- 'scripts/assemble_native_repositories.sh'
- 'site/**'
- 'src/**'
- 'stubs/**'
@@ -29,61 +27,31 @@ on:
workflow_dispatch:
permissions:
actions: read
contents: read
concurrency:
group: flatpak-${{ github.event.workflow_run.head_sha || github.ref }}
cancel-in-progress: true
group: flatpak-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
flatpak:
name: Flatpak build
if: >-
github.event_name != 'workflow_run' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request' &&
github.event.workflow_run.head_branch == 'main')
runs-on: ubuntu-24.04
timeout-minutes: 120
# The signing key lives in the release environment, which only main and
# v* tags may use. Builds that do not publish run without it.
environment: ${{ (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')) && 'release' || '' }}
env:
MOCKTAIL_PUBLISH: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'pull_request' && github.event.workflow_run.head_branch == 'main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
MOCKTAIL_SOURCE_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
# Publish on release tags and on manual runs from main. Pull requests
# only verify that the manifest still builds.
MOCKTAIL_PUBLISH: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
submodules: recursive
- name: Download package repositories
if: env.MOCKTAIL_PUBLISH == 'true'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -Eeuo pipefail
run_id="${{ github.event.workflow_run.id }}"
if [[ -z "$run_id" ]]; then
run_id="$(gh run list \
--workflow packages.yml \
--branch main \
--event push \
--status success \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId')"
fi
[[ -n "$run_id" ]]
gh run download "$run_id" \
--name Mocktail-native-repositories \
--dir native-repositories
[[ -d native-repositories/apt && \
-d native-repositories/rpm && \
-d native-repositories/downloads && \
-s native-repositories/mocktail-packages.gpg ]]
- name: Install Flatpak builder
shell: bash
run: |
@@ -99,22 +67,23 @@ jobs:
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
run: |
set -Eeuo pipefail
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_SHA"
# The manifest builds the local checkout from refs/heads/main.
git update-ref refs/heads/main "$GITHUB_SHA"
builder_signing=()
bundle_keys=()
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
-n "$FLATPAK_GPG_KEY_ID" ]]
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
install -d -m 0700 "$RUNNER_TEMP/nightcap-gnupg"
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
gpg --batch --homedir "$RUNNER_TEMP/nightcap-gnupg" --import
gpg --batch --homedir "$RUNNER_TEMP/nightcap-gnupg" \
--export "$FLATPAK_GPG_KEY_ID" >nightcap-flatpak.gpg
builder_signing=(
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
"--gpg-homedir=$RUNNER_TEMP/nightcap-gnupg"
)
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
bundle_keys=(--gpg-keys=nightcap-flatpak.gpg)
fi
flatpak-builder \
--arch=x86_64 \
@@ -127,34 +96,33 @@ jobs:
--repo=repo \
"${builder_signing[@]}" \
build-flatpak-ci \
packaging/flatpak/space.bigrat.mocktail.json
packaging/flatpak/io.github.CoderDayton.nightcap.json
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
flatpak build-update-repo \
--generate-static-deltas \
--static-delta-jobs=2 \
--prune \
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
--gpg-homedir="$RUNNER_TEMP/nightcap-gnupg" \
repo
fi
flatpak build-bundle \
--arch=x86_64 \
"${bundle_keys[@]}" \
repo \
Mocktail-x86_64.flatpak \
space.bigrat.mocktail \
Nightcap-x86_64.flatpak \
io.github.CoderDayton.nightcap \
stable
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
./scripts/assemble_flatpak_pages.sh \
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg \
public native-repositories
repo Nightcap-x86_64.flatpak nightcap-flatpak.gpg public
fi
- name: Upload Flatpak
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Mocktail-x86_64-flatpak
path: Mocktail-x86_64.flatpak
name: Nightcap-x86_64-flatpak
path: Nightcap-x86_64.flatpak
if-no-files-found: error
retention-days: 14
@@ -170,13 +138,7 @@ jobs:
deploy-pages:
name: Publish Flatpak repository
if: >-
(github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request' &&
github.event.workflow_run.head_branch == 'main') ||
(github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main')
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
needs: flatpak
runs-on: ubuntu-24.04
timeout-minutes: 10
@@ -191,3 +153,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
+69 -226
View File
@@ -1,41 +1,51 @@
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
env:
UPINFO: gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|continuous|Nightcap-nightly-x86_64.AppImage.zsync
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -52,12 +62,13 @@ jobs:
--volume "$GITHUB_WORKSPACE:/workspace" \
--workdir /workspace \
--env GH_TOKEN \
--env UPINFO \
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 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.
@@ -115,11 +126,12 @@ jobs:
export RUNTIME=/tmp/uruntime
export DWARFS_CMD=/tmp/mkdwarfs
make appimage BUILD_DIR=build-appimage JOBS=4 \
APPIMAGE=dist/Mocktail-x86_64.AppImage \
APPIMAGE=dist/Nightcap-nightly-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
test -x dist/Nightcap-nightly-x86_64.AppImage
test -s dist/Nightcap-nightly-x86_64.AppImage.zsync
'
- name: Smoke-test AppImage without FUSE
@@ -132,161 +144,26 @@ jobs:
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
test "$(dist/Nightcap-nightly-x86_64.AppImage --appimage-updateinformation)" = "$UPINFO"
timeout 60s dist/Nightcap-nightly-x86_64.AppImage --appimage-extract-and-run --help
timeout 60s dist/Nightcap-nightly-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: |
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
name: Nightcap-AppImage
path: |
dist/Nightcap-nightly-x86_64.AppImage
dist/Nightcap-nightly-x86_64.AppImage.zsync
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:
@@ -297,84 +174,50 @@ 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: 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 .zsync file names the AppImage it updates, so the build already
# carries the published name.
test -s release/Nightcap-nightly-x86_64.AppImage
test -s release/Nightcap-nightly-x86_64.AppImage.zsync
(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.zsync|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
+350
View File
@@ -0,0 +1,350 @@
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[@]}"
# nightcap-bin downloads the pacman package from the release page, so this
# waits for the upload.
aur:
name: Publish AUR packages
if: startsWith(github.ref, 'refs/tags/v')
needs: publish
uses: ./.github/workflows/aur-publish.yml
with:
tag: ${{ github.ref_name }}
secrets:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
+4
View File
@@ -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/
+301
View File
@@ -0,0 +1,301 @@
# 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]
### Added
- An asset overlay folder, `~/.local/share/mocktail/asset_overlay`. A file there replaces the file Roblox
ships at the same path, such as a cursor, a font or a sky texture, and it stays in place when Roblox
updates. The layout matches Sober's `asset_overlay` folder. `MOCKTAIL_ASSET_OVERLAY_DIR` moves it. Thanks to
@dim-ghub.
- The asset overlay folder now also replaces files Roblox downloads and no longer ships, such as the shift
lock and text cursors. Put the file under `content/`, at the path Roblox uses. Thanks to @dim-ghub for the
idea.
- `MOCKTAIL_ASSET_TRACE=1` now also prints the files Roblox asks for and does not get, as `[asset] miss`.
- Arch Linux: `nightcap-git` is on the AUR and builds the current `main` branch. Thanks to @dim-ghub, who
maintains it.
- The log now names each Roblox Java call that Nightcap has no handler for, once per call, as
`[JNI] no handler for <class>.<method><signature>`. This shows what a new Roblox version needs.
### Changed
- Nightcap answers Roblox's Java calls with less work per call, and the audio thread no longer allocates
memory for each block of sound it plays.
## [0.3.4] - 2026-10-11
### Added
- The AppImage build now makes a `.zsync` file, so tools like `appimageupdatetool` can update the AppImage.
- The desktop side panel menu now shows by default on PC profiles. Set `FIntFlyoutTestVariantRollout12` in
`fflags.json` to change it.
### Changed
- The Flatpak build asks for the X11 socket only when there is no Wayland session, as before 0.3.3.
- Nightcap now uses Wayland on a Wayland session with NVIDIA drivers too, however it is started. It used to
fall back to X11 there when started from a terminal.
- Building Nightcap no longer needs libexecinfo.
### Fixed
- Fixed Roblox 2.742 being rejected as an update, which left the game on the older version. Nightcap now
accepts Roblox versions that change the order of their start-up steps.
- Fixed Nightcap reporting Mocktail's version number, 1.0.4, in session logs and support bundles. It now
reports its own version.
- Fixed the old Roblox home screen showing instead of the new one with the Me tab and the "For you" and
"Charts" tabs. Nightcap now runs the Roblox start-up step that loads the new interface for your account.
- Fixed anti-aliasing being forced off at every graphics quality level. Roblox now picks it, and a
`FIntDebugForceMSAASamples` value in `fflags.json` still applies. Thanks to @qutad.
- Fixed the voice chat microphone only working on Roblox 2.738. It no longer depends on one Roblox version.
- Fixed other players not hearing you in voice chat after a Roblox update. The updater now rebuilds the
audio device data when the previous version's profile has none.
- Fixed frame hitches while textures load on graphics cards that need ETC2 textures decoded on the CPU.
- Fixed problems on Chimera Linux and other musl systems, including error messages that differed from the
ones Roblox expects.
- Fixed the nightly AppImage build failing when a bundled GTK 4 was older than the one libadwaita needs.
- Fixed a random crash on Wayland with NVIDIA drivers, where the game window closed after you let go of the
camera or left mouse lock. The log showed `wp_linux_drm_syncobj_surface_v1` error 3.
- Fixed some menu actions being ignored. The Chat button on a profile now opens the Party tab with that
player, and clicking a search suggestion now runs the search instead of closing the search menu.
- Fixed possible crashes when the game calls an Android function Nightcap did not provide, or releases the
same reference twice.
- Fixed a possible hang on close when a web view process stayed open. Nightcap now stops waiting for the log
writer after 2 seconds.
- Fixed a possible missing picture on graphics drivers that reject the low-latency present mode. Nightcap now
retries with the game's own mode.
- Fixed a possible Wayland crash when the window first appears on the OpenGL graphics path.
- Fixed a missing picture on machines where the graphics card runs a different driver than the one Nightcap
picked, such as an NVIDIA card on nouveau or a laptop with an Intel chip and an older second card. Nightcap
now picks the Vulkan driver that matches the running kernel driver, and says so when no card can run Vulkan.
## [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.
## [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.4...HEAD
[0.3.4]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.4
[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
+63 -75
View File
@@ -1,3 +1,4 @@
# Modified by vii from komaruworld/mocktail. See README "About this fork".
# Copyright 2026 Mocktail Project Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -8,8 +9,8 @@
cmake_minimum_required(VERSION 3.20)
project(Mocktail
VERSION 1.0.4
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
VERSION 0.3.4
DESCRIPTION "Android Roblox compatibility runtime for Linux"
LANGUAGES CXX
)
@@ -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,10 @@ 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
src/jnivm/jni_method_names.cc
)
add_library(Mocktail::LegacyJni ALIAS mocktail_legacy_jnivm)
target_include_directories(mocktail_legacy_jnivm PUBLIC include)
@@ -264,8 +269,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,12 +289,17 @@ 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
src/runtime/asset_overlay_names.cc
src/runtime/runtime_config.cc
src/runtime/runtime_config_bootstrap.cc
src/runtime/runtime_config_file.cc
@@ -301,6 +311,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 +394,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 +482,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
@@ -551,50 +563,6 @@ target_link_libraries(mocktail_bionic_abi_exports PRIVATE
Threads::Threads
)
include(CheckCXXSourceCompiles)
include(CheckIncludeFileCXX)
include(CMakePushCheckState)
check_include_file_cxx(execinfo.h MOCKTAIL_HAVE_EXECINFO_H)
if(NOT MOCKTAIL_HAVE_EXECINFO_H)
message(FATAL_ERROR
"execinfo.h is required for crash diagnostics; on musl install "
"libexecinfo-devel"
)
endif()
set(MOCKTAIL_EXECINFO_LINK_LIBRARY "")
set(_mocktail_execinfo_link_probe [=[
#include <execinfo.h>
int main() {
void* frames[1] = {};
const int count = backtrace(frames, 1);
return backtrace_symbols(frames, count) == nullptr;
}
]=])
check_cxx_source_compiles(
"${_mocktail_execinfo_link_probe}"
MOCKTAIL_EXECINFO_IN_HOST_LIBC
)
if(NOT MOCKTAIL_EXECINFO_IN_HOST_LIBC)
find_library(MOCKTAIL_EXECINFO_LIBRARY NAMES execinfo REQUIRED)
cmake_push_check_state(RESET)
set(CMAKE_REQUIRED_LIBRARIES "${MOCKTAIL_EXECINFO_LIBRARY}")
check_cxx_source_compiles(
"${_mocktail_execinfo_link_probe}"
MOCKTAIL_EXECINFO_IN_EXTERNAL_LIBRARY
)
cmake_pop_check_state()
if(NOT MOCKTAIL_EXECINFO_IN_EXTERNAL_LIBRARY)
message(FATAL_ERROR
"execinfo.h is available, but backtrace and backtrace_symbols cannot "
"be linked"
)
endif()
set(MOCKTAIL_EXECINFO_LINK_LIBRARY "${MOCKTAIL_EXECINFO_LIBRARY}")
endif()
unset(_mocktail_execinfo_link_probe)
add_library(mocktail_legacy_runtime STATIC
src/legacy/legacy_runtime.cc
src/runtime/roblox_text_input_jni_bridge.cc
@@ -619,7 +587,6 @@ target_link_libraries(mocktail_legacy_runtime PRIVATE
libc_shim
mocktail_window
mocktail_graphics_foundation
${MOCKTAIL_EXECINFO_LINK_LIBRARY}
)
target_compile_definitions(mocktail_legacy_runtime PRIVATE
"MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=\"${MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST}\""
@@ -648,6 +615,11 @@ target_link_options(mocktail PRIVATE
)
# Native FreeBSD helper for Linuxulator's unsupported DF socket option.
# The helper is x86-64 assembly; other host architectures have nothing to
# assemble.
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
set(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER OFF)
endif()
option(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER
"Build the native FreeBSD Linuxulator socket helper" ON)
if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER)
@@ -725,13 +697,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
@@ -740,7 +723,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"
)
@@ -773,33 +756,39 @@ 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 x86-64 Roblox client on Linux."
"Runs the Android Roblox client on Linux."
)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
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)
@@ -809,15 +798,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)
+42 -13
View File
@@ -1,41 +1,65 @@
<!-- 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. 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.
Bloxstrap brought the option back in [v2.9.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.9.0)
with a "use at your own risk" warning, then removed it in
[v2.10.0](https://github.com/bloxstraplabs/bloxstrap/releases/tag/v2.10.0)
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:
Yes, as an experiment. VR lives on Nightcap's `vr` branch and is not in the
releases yet. It works on Roblox builds 2998, 3092 and 3222. Install the
[build dependencies](README.md#building), then switch to that branch and
build:
```bash
git fetch origin
@@ -48,5 +72,10 @@ cmake --build build -j4
Start WiVRn or SteamVR/ALVR, connect your headset, then run:
```bash
./build/mocktail -vr
./build/mocktail --vr
```
That branch's
[README](https://github.com/CoderDayton/nightcap/blob/vr/README.md#experimental-vr)
and [docs/VR.md](https://github.com/CoderDayton/nightcap/blob/vr/docs/VR.md)
have the details, including how to try it without a headset.
+7 -6
View File
@@ -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
@@ -129,8 +130,8 @@ auto-run: ## Build/run loop with crash summaries
run-gles: build ## Run strict system EGL/OpenGL ES 3 without Vulkan fallback
@$(BINARY) --graphics opengl
run-angle: ## Run with ANGLE/Vulkan compatibility backend
@MOCKTAIL_GRAPHICS_BACKEND=angle-vulkan ./scripts/run_sober.sh
run-angle: build ## Run with ANGLE/Vulkan compatibility backend
@$(BINARY) --graphics angle-vulkan
update-roblox: ## Validate and import the current x86_64 Roblox bundle cached by Sober
@./scripts/update_roblox_payload.sh
+152 -95
View File
@@ -1,19 +1,104 @@
# 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>
<a href="https://discord.gg/DbAUG9hBFU"><img alt="Discord" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Fv10%2Finvites%2FDbAUG9hBFU%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&label=Discord&logo=discord&logoColor=white&color=5865F2&suffix=%20members"></a>
</h1>
</div>
[![CI](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml/badge.svg)](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
[![Stars](https://img.shields.io/github/stars/komaruworld/mocktail?style=flat&logo=github)](https://github.com/komaruworld/mocktail/stargazers)
[![Downloads](https://img.shields.io/github/downloads/komaruworld/mocktail/total?logo=github)](https://github.com/komaruworld/mocktail/releases/latest)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Discord](https://img.shields.io/discord/1543955607869063269?label=discord)](https://discord.gg/rhgpfcmFSD)
Mocktail runs the Android `x86_64` Roblox client on Linux, including a Linux
Nightcap is Roblox on Linux, tuned for desktop play. It is a Mocktail fork
that keeps up with Roblox updates, with extra bug fixes, custom assets and
more. It runs the Android `x86_64` Roblox client on Linux, including a Linux
userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI
pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on
the Linux side.
Mocktail is an independent community project. It is not affiliated with Roblox
Nightcap is an independent community project. It is not affiliated with Roblox
Corporation or VinegarHQ and does not distribute the Roblox client.
## About this fork
Nightcap is a fork of [komaruworld/mocktail](https://github.com/komaruworld/mocktail)
with extra fixes for playing on a PC. The program and config paths keep the
`mocktail` name so upstream fixes still apply cleanly. The app ID is
`io.github.CoderDayton.nightcap`, so Nightcap and upstream can be installed
side by side.
- **Keeps up with Roblox.** New Roblox versions work soon after they come out.
- **Plays more games.** Nightcap answers a server check that some experiences
use, so you can join and stay in those too.
- **Voice chat that keeps working.** Your microphone still works after a
Roblox update.
- **Logging in works.** Verification checks and captchas finish instead of
hanging.
- **The current Roblox look.** The new home screen and side menu, like on
Windows.
- **Your own cursors, fonts and skies.** Drop files in
`~/.local/share/mocktail/asset_overlay` and Nightcap uses them. The layout
matches Sober's `asset_overlay` folder.
- **Smoother mouse and a cooler PC.** Optional raw mouse input, and less CPU
use while idle.
Everything else works the same as upstream. See
[`textures/`](textures/README.md) for custom textures, a ready-made crosshair
and the asset overlay folder, 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.
On Arch Linux, [`nightcap-git`](https://aur.archlinux.org/packages/nightcap-git)
is on the AUR. It builds the current `main` branch and is maintained by
[@dim-ghub](https://github.com/dim-ghub):
```bash
yay -S nightcap-git
```
Nightcap is in no other distribution repository.
Or build and install from source using the steps under
[Building](#building).
## How it works
```
@@ -21,115 +106,59 @@ 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>
![Roblox home in Mocktail](assets/screenshots/flatpak-home.png)
![Roblox home in Nightcap](assets/screenshots/flatpak-home.png)
![Roblox gameplay in Mocktail](assets/screenshots/flatpak-gameplay-tower.png)
![Roblox experience in Mocktail](assets/screenshots/flatpak-gameplay-lobby.png)
![Roblox gameplay in Nightcap](assets/screenshots/flatpak-gameplay-tower.png)
</details>
## FFlag overrides
Put a JSON object in `$XDG_CONFIG_HOME/mocktail/fflags.json` (usually
`~/.config/mocktail/fflags.json`). Mocktail applies it on the next launch.
`~/.config/mocktail/fflags.json`). Nightcap applies it on the next launch.
PC profiles use the legacy Charts page by default to avoid the blank screen
caused by `Color3` errors in the newer SDUI page. An explicit
`FFlagLuaAppChartsAppPage` override takes precedence over this default.
PC profiles also set `FIntFlyoutTestVariantRollout12` to `2`, which shows the
desktop side panel menu. An explicit override takes precedence.
```json
{
"FFlagExample": "True",
"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. Experimental FreeBSD 15.1 Linuxulator support has
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Mocktail runs
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Nightcap runs
inside Linuxulator; it is not a native FreeBSD binary. Building requires CMake
3.20+, Git, pkg-config, LLD, binutils, a C++17 compiler, SDL 3.4+, SDL3_ttf,
Vulkan, EGL, libplacebo, fontconfig, libcurl, OpenSSL, libelf, libyaml, minizip,
Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and WebKitGTK 6.0.
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.
@@ -140,7 +169,7 @@ 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 zlib1g-dev
@@ -152,7 +181,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
```
@@ -164,7 +193,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
@@ -172,17 +202,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
@@ -190,7 +242,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`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1009 KiB

+2 -1
View File
@@ -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}"
+28 -5
View File
@@ -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
+17
View File
@@ -4,6 +4,7 @@
{
"version_name": "2.721.1108",
"version_code": 2350,
"abi": "x86_64",
"elf_build_id": "50e1b0abd123350e794226062fe3a1ef360c5f0d",
"status": "legacy-researched",
"default_allowed": false,
@@ -15,6 +16,7 @@
{
"version_name": "2.725.1142",
"version_code": 2546,
"abi": "x86_64",
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
"status": "supported",
"default_allowed": true,
@@ -35,6 +37,7 @@
{
"version_name": "2.727.1199",
"version_code": 2628,
"abi": "x86_64",
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
"status": "supported",
"default_allowed": true,
@@ -46,6 +49,7 @@
{
"version_name": "2.734.917",
"version_code": 2908,
"abi": "x86_64",
"elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326",
"status": "supported",
"default_allowed": true,
@@ -66,6 +70,19 @@
{
"version_name": "2.736.1408",
"version_code": 2998,
"abi": "arm64-v8a",
"elf_build_id": "ed2b035768b1513561d0e6ea7f9bbec35b0e60e5",
"status": "supported",
"default_allowed": true,
"allow_legacy_binary_patches": false,
"allow_host_abi_bridges": true,
"allow_host_constructor_replay": true,
"reason": "AArch64 arm64-v8a payload of the same 2.736.1408 release as the verified x86-64 profile; allocator, constructor replay, and pre-JNI bootstrap RVAs were derived by cross-architecture semantic matching."
},
{
"version_name": "2.736.1408",
"version_code": 2998,
"abi": "x86_64",
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
"status": "supported",
"default_allowed": true,
+1 -1
View File
@@ -32,7 +32,7 @@
"mode": "vendored-patched-snapshot",
"url": "https://github.com/minecraft-linux/mcpelauncher-linker.git",
"upstream_commit": "1ac3ea6c1cf4f73a84d73f0bff510fba94a7d0f2",
"vendor_tree": "fca44d85e3773c135ccfc603b14abdf6aeb8efd1",
"vendor_tree": "0637221e48f382ec8d0bc701385f4b10bcd07a65",
"patch": "third_party/patches/mcpelauncher-linker.patch",
"nested_sources": {
"android_bionic": {
+123
View File
@@ -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.
+193
View File
@@ -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).
+67
View File
@@ -0,0 +1,67 @@
# 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.
## 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.
+11
View File
@@ -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}"
+30
View File
@@ -33,6 +33,23 @@ void SetLegacyBionicDiagnosticsEnabled(bool enabled);
// guest layout explicit instead of exposing the incompatible host libc type.
using MocktailBionicMutexAttr = int64_t;
// Bionic LP64 pthread_attr_t: 56 bytes with explicit field offsets. The host
// glibc layout is opaque and wider on aarch64, so guest buffers must be read
// and written through this struct only.
struct MocktailBionicPthreadAttr {
std::uint32_t flags;
void* stack_base;
std::size_t stack_size;
std::size_t guard_size;
std::int32_t sched_policy;
std::int32_t sched_priority;
char reserved[16];
};
static_assert(sizeof(MocktailBionicPthreadAttr) == 56);
inline constexpr std::uint32_t kMocktailBionicAttrFlagDetached = 0x1;
inline constexpr std::uint32_t kMocktailBionicAttrFlagUserStack = 0x2;
extern "C" {
void mocktail_set_current_jni_env(void* env);
@@ -84,6 +101,19 @@ int mocktail_pthread_barrier_init(pthread_barrier_t* barrier,
int mocktail_pthread_barrier_destroy(pthread_barrier_t* barrier);
int mocktail_pthread_barrier_wait(pthread_barrier_t* barrier);
int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr,
size_t stack_size);
int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr,
int detach_state);
int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr,
const struct sched_param* parameters);
int mocktail_pthread_getattr_np(pthread_t thread,
MocktailBionicPthreadAttr* attr);
int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr,
void** stack_base, size_t* stack_size);
extern void* mocktail_gameactivity_on_start_native;
extern void* mocktail_gameactivity_on_resume_native;
extern void* mocktail_gameactivity_on_surface_created_native;
+19 -10
View File
@@ -11,19 +11,27 @@
namespace mocktail::compat {
// glibc's atoi is (int)strtol(), exactly like Bionic's, so the host symbol is
// used as-is there. Other libcs (musl) differ on overflow and need the shim.
#ifdef __GLIBC__
inline constexpr bool kHostAtoiMatchesBionic = true;
#else
inline constexpr bool kHostAtoiMatchesBionic = false;
#endif
// Android uses size_t fields here; glibc uses int and musl has no equivalent.
// An all-zero fallback means unknown, not zero usage.
struct BionicMallinfoSnapshot {
size_t arena = 0;
size_t ordblks = 0;
size_t smblks = 0;
size_t hblks = 0;
size_t hblkhd = 0;
size_t usmblks = 0;
size_t fsmblks = 0;
size_t uordblks = 0;
size_t fordblks = 0;
size_t keepcost = 0;
size_t arena;
size_t ordblks;
size_t smblks;
size_t hblks;
size_t hblkhd;
size_t usmblks;
size_t fsmblks;
size_t uordblks;
size_t fordblks;
size_t keepcost;
};
struct BionicLocaleState;
@@ -90,6 +98,7 @@ int mocktail_bionic_cxa_thread_atexit_impl(
mocktail::compat::BionicThreadDestructor destructor, void* argument,
void* dso_handle);
void mocktail_bionic_arc4random_buf(void* buffer, size_t size);
int mocktail_bionic_atoi(const char* text);
mocktail::compat::BionicMallinfoSnapshot mocktail_bionic_mallinfo();
int mocktail_bionic_sysinfo(struct sysinfo* info);
int mocktail_bionic_uname(struct utsname* name);
@@ -25,11 +25,12 @@ void ConfigureBionicPthreadThreadInitializer(
} // namespace mocktail::compat
struct MocktailBionicPthreadAttr;
// Rebuilds attributes instead of passing Android pthread_attr_t to glibc.
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
const pthread_attr_t* attr,
void* (*start_routine)(void*),
void* argument);
extern "C" int mocktail_bionic_pthread_create(
pthread_t* thread, const MocktailBionicPthreadAttr* attr,
void* (*start_routine)(void*), void* argument);
// Guest real-time scheduling is denied to avoid the host RLIMIT_RTTIME watchdog.
extern "C" int mocktail_bionic_pthread_setschedparam(
+5
View File
@@ -36,6 +36,11 @@ struct FmodOutputDeviceBridgeProfile {
? std::array<std::size_t, 4>{9, 10, 12, 18}
: std::array<std::size_t, 4>{8, 9, 10, 16};
}
std::array<std::size_t, 6> capture_vtable_indexes() const {
return vtable_layout_version == 2
? std::array<std::size_t, 6>{4, 13, 14, 15, 16, 17}
: std::array<std::size_t, 6>{4, 11, 12, 13, 14, 15};
}
bool valid_vtable_layout() const {
return vtable_layout_version == 1 || vtable_layout_version == 2;
+38
View File
@@ -0,0 +1,38 @@
#ifndef MOCKTAIL_COMPAT_GUEST_ABI_H_
#define MOCKTAIL_COMPAT_GUEST_ABI_H_
#include <elf.h>
#include <string_view>
namespace mocktail::compat {
#if defined(__aarch64__)
// Android ABI name as it appears in APK lib/ directories and build metadata.
inline constexpr std::string_view kGuestAbi = "arm64-v8a";
// APK split-config name carrying the guest native libraries.
inline constexpr std::string_view kGuestApkSplitName = "config.arm64_v8a";
// File name the payload store uses for the split APK.
inline constexpr std::string_view kGuestSplitApkFile =
"split_config.arm64_v8a.apk";
// sha256 map key holding the split APK digest in payload metadata.
inline constexpr std::string_view kGuestSplitApkHashKey = "arm64_v8a_split_apk";
// Short CPU label for device metadata and user-facing strings.
inline constexpr std::string_view kGuestCpuName = "aarch64";
inline constexpr int kGuestElfMachine = EM_AARCH64;
#elif defined(__x86_64__)
inline constexpr std::string_view kGuestAbi = "x86_64";
inline constexpr std::string_view kGuestApkSplitName = "config.x86_64";
inline constexpr std::string_view kGuestSplitApkFile = "split_config.x86_64.apk";
inline constexpr std::string_view kGuestSplitApkHashKey = "x86_64_split_apk";
inline constexpr std::string_view kGuestCpuName = "x86_64";
inline constexpr int kGuestElfMachine = EM_X86_64;
#else
#error "mocktail only supports aarch64 and x86_64 hosts"
#endif
// APK zip entry holding the guest libroblox.so.
inline constexpr std::string_view kGuestLibApkEntryPrefix = "lib/";
} // namespace mocktail::compat
#endif // MOCKTAIL_COMPAT_GUEST_ABI_H_
+9
View File
@@ -109,6 +109,15 @@ struct HostAbiProfile {
size_t NativeMimallocConstructorRangeEndExclusive() const noexcept;
bool ShouldInitializeNativeMimallocThreadAfterConstructor(
size_t index) const noexcept;
// True when both replays have the one verified shape. The native replay is
// one range that runs to the end of .init_array. Its first four
// constructors are the process-load constructor at the thread-initializer
// checkpoint, the JNI singleton initializer, and two wiring entries. The
// host-bridge replay runs the same constructors without the two wiring
// entries. `jni_singleton_constructor` is the .init_array index of the JNI
// singleton initializer.
bool HasVerifiedConstructorPolicy(
size_t jni_singleton_constructor) const noexcept;
bool HasValidNativePreJniBootstrap() const noexcept;
HostAllocatorStrategy ResolveAllocatorStrategy(
bool host_bridges_override_present,
+190
View File
@@ -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_
+6
View File
@@ -48,9 +48,15 @@ struct PlatformIdentity {
std::string manufacturer = "Mocktail";
std::string model = "Mocktail Headless";
std::string brand = "Mocktail";
#if defined(__aarch64__)
std::string device_code = "linux-arm64";
std::string device_sku = "mocktail-arm64";
std::string soc_model = "aarch64";
#else
std::string device_code = "linux-x86_64";
std::string device_sku = "mocktail-x86_64";
std::string soc_model = "x86_64";
#endif
};
struct RobloxCredentialView {
@@ -6,6 +6,9 @@
#if defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if defined(__aarch64__)
#include <cstdint>
#endif
namespace mocktail::audio {
@@ -22,16 +25,23 @@ class FmodThreadFloatingPointMode final {
delete;
bool Enable(const char* thread_name) noexcept {
#if defined(__x86_64__)
#if defined(__x86_64__) || defined(__aarch64__)
if (active_ || thread_name == nullptr) return false;
const std::string_view name(thread_name);
if (name != "FMOD mixer thread" && name != "FMOD feeder thread" &&
name != "FMOD Convolution thread" && name != "FMOD Worker Thread") {
return false;
}
#if defined(__x86_64__)
const unsigned int control = _mm_getcsr();
original_mode_ = control & kModeMask;
_mm_setcsr(control | kModeMask);
#else
std::uint64_t control;
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
original_mode_ = control & kModeMask;
__asm__ volatile("msr fpcr, %0" ::"r"(control | kModeMask));
#endif
active_ = true;
return true;
#else
@@ -41,11 +51,18 @@ class FmodThreadFloatingPointMode final {
}
void Restore() noexcept {
#if defined(__x86_64__)
#if defined(__x86_64__) || defined(__aarch64__)
if (active_) {
// Restore only our two bits, preserving rounding, exception masks and
// status flags set by the guest while the thread was attached.
#if defined(__x86_64__)
_mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_);
#else
std::uint64_t control;
__asm__ volatile("mrs %0, fpcr" : "=r"(control));
__asm__ volatile("msr fpcr, %0" ::"r"((control & ~kModeMask) |
original_mode_));
#endif
active_ = false;
}
#endif
@@ -56,6 +73,13 @@ class FmodThreadFloatingPointMode final {
static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6);
unsigned int original_mode_ = 0;
bool active_ = false;
#elif defined(__aarch64__)
// FPCR FZ flushes single/double subnormals; FZ16 covers the half-precision
// path used by some voice DSP filters.
static constexpr std::uint64_t kModeMask =
(std::uint64_t{1} << 24) | (std::uint64_t{1} << 19);
std::uint64_t original_mode_ = 0;
bool active_ = false;
#endif
};
@@ -61,8 +61,7 @@ class RobloxOutputDeviceBridge final {
void SelectDevice(int index, bool input = false);
void ConstructGuestString(void* destination, std::string_view value) const;
std::unique_ptr<NativeInputCapture> capture_;
bool capture_profile_supported_ = false;
std::shared_ptr<NativeInputCapture> capture_;
std::array<std::uintptr_t, 6> original_capture_methods_{};
mutable std::mutex mutex_;
compat::FmodOutputDeviceBridgeProfile profile_;
+3
View File
@@ -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_
+27 -1
View File
@@ -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_
@@ -2,6 +2,7 @@
#define MOCKTAIL_GRAPHICS_PRESENT_MODE_POLICY_H_
#include <cstdint>
#include <cstdio>
#include <string_view>
#include <vector>
@@ -29,6 +30,36 @@ FilterPresentModes(PresentModePolicy policy,
const char *PresentModePolicyName(PresentModePolicy policy);
const char *PresentModeKhrName(VkPresentModeKHR mode);
// Creates the swapchain with the mode in `info`. A driver can reject a mode
// that the device did not enable, so a failed create is retried once with the
// mode the game asked for. `create` takes the info and returns a VkResult.
template <typename Create>
VkResult CreateSwapchainWithPresentModeFallback(VkSwapchainCreateInfoKHR &info,
VkPresentModeKHR requested,
Create create) {
const VkResult result = create(info);
if (result == VK_SUCCESS || info.presentMode == requested) {
return result;
}
switch (result) {
case VK_ERROR_OUT_OF_HOST_MEMORY:
case VK_ERROR_OUT_OF_DEVICE_MEMORY:
case VK_ERROR_DEVICE_LOST:
case VK_ERROR_SURFACE_LOST_KHR:
case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR:
return result; // Another present mode cannot fix these.
default:
break;
}
std::fprintf(stderr,
" [vulkan] swapchain presentMode %s failed (%d); retrying "
"with %s\n",
PresentModeKhrName(info.presentMode), static_cast<int>(result),
PresentModeKhrName(requested));
info.presentMode = requested;
return create(info);
}
// Extra images so acquire does not wait on the compositor-held buffer.
std::uint32_t PreferSwapchainMinImageCount(PresentModePolicy policy,
std::uint32_t requested,
@@ -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
+27
View File
@@ -0,0 +1,27 @@
#ifndef MOCKTAIL_RUNTIME_ASSET_OVERLAY_NAMES_H_
#define MOCKTAIL_RUNTIME_ASSET_OVERLAY_NAMES_H_
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
namespace mocktail {
namespace runtime {
// The rbxasset:// name of every regular file under <overlay_dir>/content, such
// as rbxasset://textures/MouseLockedCursor.png. Sorted.
std::vector<std::string> OverlayRbxAssetNames(
const std::filesystem::path& overlay_dir);
// Roblox opens no local file for an rbxasset:// name it maps to a download.
// Writes '!' over the first byte of each whole NUL-terminated copy of a name
// in `data`, which unmaps it. Returns the number of copies changed.
std::size_t DisableAssetNameMappings(std::uint8_t* data, std::size_t size,
const std::vector<std::string>& names);
}
}
#endif
+2
View File
@@ -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 {
+5
View File
@@ -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
+5 -1
View File
@@ -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
+8 -1
View File
@@ -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);
+65
View File
@@ -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_
+34
View File
@@ -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();
+20
View File
@@ -18,6 +18,26 @@ std::string SelectVulkanIcdManifest(
const std::vector<std::filesystem::path>& directories,
std::string_view vendor);
// One GPU as the kernel reports it.
struct HostGpu {
unsigned int vendor = 0; // PCI vendor ID.
std::string kernel_driver; // Bound kernel driver; empty when unknown.
};
struct VulkanIcdSelection {
std::string manifest; // Empty when no GPU has a usable driver.
std::vector<std::string> skipped; // Why each passed-over GPU was skipped.
};
// ICD manifest for the first GPU whose bound kernel driver has an installed
// Vulkan driver. NVIDIA and AMD cards are tried before Intel when
// `prefer_discrete` is set, and after it otherwise. A GPU with an unknown
// kernel driver is matched by vendor alone.
VulkanIcdSelection SelectHostVulkanIcd(
const std::vector<HostGpu>& gpus,
const std::vector<std::filesystem::path>& directories,
bool prefer_discrete);
// Publishes the resolved graphics backend before the managed payload updater
// starts. OpenGL is a strict system EGL/GLES path; it never silently retries
// through ANGLE/Vulkan or accepts a window without a real graphics context.
+12 -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
+2 -2
View File
@@ -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);
+94
View File
@@ -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);
+17 -3
View File
@@ -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);
+7
View File
@@ -19,6 +19,13 @@ inline constexpr std::size_t kMaximumRobloxLaunchUriBytes = 64 * 1024;
Status ParseRobloxLaunchUri(std::string_view uri,
RobloxExperienceLaunchRequest* request);
// Converts a WebView launchGame command
// ({"functionName":"launchGame","params":{"request":{...}}}) into the same
// contract. Returns kUnsupported for other commands and kInvalidArgument for
// a malformed launchGame request.
Status ParseRobloxWebViewLaunchGame(std::string_view command,
RobloxExperienceLaunchRequest* request);
} // namespace runtime
} // namespace mocktail
@@ -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
+5 -65
View File
@@ -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;
};
+61
View File
@@ -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"
@@ -30,6 +32,9 @@ inline constexpr char kRobloxOpenCaptchaViewNotification[] =
"OPEN_CAPTCHA_VIEW";
inline constexpr char kRobloxViewProfileNotification[] = "VIEW_PROFILE";
inline constexpr char kRobloxPurchaseRobuxNotification[] = "PURCHASE_ROBUX";
// Sent with a decimal user id when the app asks for a chat with that user.
inline constexpr char kRobloxLaunchConversationNotification[] =
"LAUNCH_CONVERSATION";
// Owned subset of WebViewProtocol's openWindow payload. The URL is copied
// before the MessageBus RawCallback returns, so no JNI local reference escapes.
@@ -42,6 +47,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 +70,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,
@@ -106,8 +118,43 @@ using PublishWebViewRawFn = void (*)(JNIEnv *, jobject, jstring, jstring);
using BroadcastWebViewDataModelFocusFn = void (*)(JNIEnv *, jclass, jstring,
jstring, jstring);
using SignalWebViewJavascriptCallbackFn = void (*)(JNIEnv*, jclass, jstring);
// NativeGLInterface.nativeAppBridgeV2SendAppEventOnGameLoaded(namespace,
// detail, detailType).
using SendRobloxAppEventOnGameLoadedFn = void (*)(JNIEnv*, jclass, jstring,
jstring, jstring);
// NativeGLInterface.nativeAppBridgeV2SendAppEventOnAppReady(namespace, detail,
// detailType, appName). The engine holds the event until appName is ready.
using SendRobloxAppEventOnAppReadyFn = void (*)(JNIEnv*, jclass, jstring,
jstring, jstring, 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 +176,11 @@ 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;
// Optional: without them LAUNCH_CONVERSATION is reported as unavailable.
SendRobloxAppEventOnGameLoadedFn send_app_event_on_game_loaded = nullptr;
SendRobloxAppEventOnAppReadyFn send_app_event_on_app_ready = nullptr;
bool complete() const {
return get_open_window_id != nullptr &&
@@ -232,11 +284,14 @@ private:
kDataModelUnfocused,
kDataModelFocused,
kMessageBusWindowClose,
kStartConversationWithUser,
};
struct PendingHostWindowEvent {
HostWindowEventType type = HostWindowEventType::kDataModelUnfocused;
uint64_t generation = 0;
// Decimal user id of a kStartConversationWithUser event.
std::string user_id;
};
Status CheckJniException(JNIEnv *env, const char *operation) const;
@@ -245,6 +300,8 @@ private:
Status DispatchOpenRequest(RobloxWebViewOpenRequest request,
OpenSource source);
void QueueHostWindowEvent(HostWindowEventType type, uint64_t generation);
void QueueStartConversation(std::string user_id);
Status SendStartConversationEvents(JNIEnv* env, const std::string& user_id);
Status BeginDispatch();
void EndDispatch();
static void RawMessageCallback(void *context, JNIEnv *env, jstring message);
@@ -287,6 +344,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,
+22 -3
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 {
@@ -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(
+5 -1
View File
@@ -14,7 +14,7 @@ struct ActivePayloadPaths {
std::filesystem::path root;
std::filesystem::path roblox_library;
std::filesystem::path base_apk;
std::filesystem::path x86_64_split_apk;
std::filesystem::path arch_split_apk;
std::filesystem::path assets_content;
std::filesystem::path compatibility_manifest;
std::filesystem::path host_abi_profile;
@@ -67,6 +67,9 @@ class RuntimePaths {
return active_payload_manifest_;
}
const std::filesystem::path& cookie_file() const { return cookie_file_; }
const std::filesystem::path& asset_overlay_dir() const {
return asset_overlay_dir_;
}
bool use_real_sober_paths() const { return use_real_sober_paths_; }
std::filesystem::path DefaultAssetPath() const;
@@ -99,6 +102,7 @@ class RuntimePaths {
std::filesystem::path config_file_;
std::filesystem::path active_payload_manifest_;
std::filesystem::path cookie_file_;
std::filesystem::path asset_overlay_dir_;
bool use_real_sober_paths_ = false;
};
+4
View File
@@ -11,6 +11,10 @@ struct HostAbiDerivationOptions {
std::filesystem::path reference_library;
std::filesystem::path reference_profile;
std::vector<std::filesystem::path> reference_compatibility_manifests;
// Optional libroblox.so of another build that the compatibility manifests
// describe. It supplies the FMOD output-device anchors when the manifests
// have none for the reference build.
std::filesystem::path runtime_fallback_library;
std::filesystem::path candidate_payload_directory;
std::filesystem::path output_directory;
};
+10 -1
View File
@@ -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
+13 -2
View File
@@ -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 {
+3
View File
@@ -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_
+28
View File
@@ -0,0 +1,28 @@
# 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}
changelog-width:
glob: "CHANGELOG.md"
run: >-
git diff --cached -U0 -- CHANGELOG.md |
awk '/^\+[^+]/ && length($0) > 111 { print "CHANGELOG.md: added line over 110 characters: " substr($0, 2); bad = 1 } END { exit bad }'
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 .
@@ -0,0 +1,10 @@
# Allowed reduced aarch64 packages from pkgforge-dev/archlinux-pkgs-debloated.
# The installer verifies the rolling release asset IDs and SHA256 digests.
ffmpeg-mini-aarch64.pkg.tar.zst
gdk-pixbuf2-mini-aarch64.pkg.tar.xz
gtk4-mini-aarch64.pkg.tar.zst
icu-mini-aarch64.pkg.tar.zst
librsvg-mini-aarch64.pkg.tar.zst
libxml2-mini-aarch64.pkg.tar.zst
mesa-mini-aarch64.pkg.tar.zst
opus-mini-aarch64.pkg.tar.zst
+1
View File
@@ -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
+7 -5
View File
@@ -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.4
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.4
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.4-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.4-1-x86_64.pkg.tar.zst::https://github.com/CoderDayton/nightcap/releases/download/v0.3.4/nightcap-0.3.4-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.4
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.4
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.4
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.4
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",
+8 -8
View File
@@ -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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Loaded 100 of 306 files, more files were not shown because too many files have changed in this diff. Show more