v0.6.0
✨ Added
-
resoio launch/resoio terminate(start/stop Resonite via umu-launcher):
New commands and Python functions (resoio.launch/resoio.terminate) that
start and force-stop the Resonite client without gRPC.launchspawns the umu-launcher chain and PID-diffs the engine
(resonite_pid) and renderer (renderer_pid) host processes into
existence, returning both as aLaunchResult.terminatestagesSIGTERM→SIGKILLover those two PIDs (or auto-detects
the single running instance when given none, erroring if more than one is
found).RESONITE_EXE(default: the Steam install) andMOD_PATH(the Gale profile
with the mod deployed) select the install; the ResoniteIO mod must be
installed (via Gale / Thunderstore) orlauncherrors with guidance.- The cooperative gRPC quit stays available as
resoio shutdown. - Exposed as
resoio.launch/resoio.terminate/LaunchResult/
LauncherErrorand theresoio launch(-e/--exe/-p/--profile/
--vanilla/--format human|json) andresoio terminate
([resonite_pid] [renderer_pid]) CLI commands.
-
Run Resonite inside the dev container: the dev container can now launch
Resonite itself via the newresoio launch/resoio terminatecommands (and
the thinjust resonite-launch/just resonite-stopwrappers).- The container entrypoint rsyncs the read-only
/resonitebind into a writable
/opt/resonite, andresoio launchstarts Resonite throughumu-run/
Proton with the ResoniteIO mod loaded from the./galeGale profile (the
first run pulls GE-Proton and copies the ~2 GB install).resoio launch --vanillaruns vanilla Resonite with no mod. - Engine side loads via hookfxr, renderer side via a doorstop
winhttp.dll—
resoio launchsetsWINEDLLOVERRIDES="winhttp=n,b"automatically, so no
manual Steam-styleWINEDLLOVERRIDESsetup is needed on this path. - The whole mod loop runs inside the container: the mod (GrpcHost) creates its
gRPC socket under the container's~/.resonite-io/(it makes the directory
itself before binding) and the Python client connects there — no host
bind-share. - The mod's BepInEx log stays at
gale/BepInEx/LogOutput.log(just log);
umu/Proton launch noise is split intogale/BepInEx/umu-launch.log. - Rendering needs a host graphical session (X11 / Xwayland) and
PipeWire/PulseAudio for audio. NVIDIA / AMD / Intel GPUs are all supported
—initialize.shdetects the vendor and selects the matching per-vendor
compose overlay (.devcontainer/compose.{nvidia,amd,intel}.ymlvia the
compose.gpu.ymlsymlink). - Requires
kernel.apparmor_restrict_unprivileged_userns=0on the host
(pressure-vessel needs unprivileged user namespaces, which Ubuntu 24.04+
restricts by default); the container start hard-fails without it. - The dev image base also moved from
debian:bookworm-slimtodebian:13-slim
(trixie), andcompose.ymlmoved from the repo root to
.devcontainer/compose.yml.
- The container entrypoint rsyncs the read-only
-
Contactmodality: A new unary modality that drives the dash "Contacts"
tab by reading/writing the cloud contact list (Engine.Cloud.Contacts/
Engine.Cloud.Users) directly — no UI automation.ListContactsreturns the synced contacts with presence (online status +
current session name / access level) plus the contact / request counts and a
list-loaded flag, with client-sidesearch(username / alternate-username
substring) andfilter(accepted friends / incoming requests). Like the dash
tab it hidesShouldBeHiddencontacts (ignored / blocked / none) by default —
passinclude_hiddento include them, and every contact carries an
is_hiddenflag.GetContactfetches one by user id (absent →found=false).SearchUsers
queries the cloud for users to add (exact or substring, read-only).AddContact(resolving the username mod-side when omitted),AcceptRequest,
andRemoveContactmutate the list; remove declines a request / deletes a
friend, which the engine marksIgnoredso the entry drops out of the default
(hidden) list.- Unknown ids return
NotFound, cloud failuresInternal, and an unavailable
cloudFailedPrecondition. - Exposed as
ContactClientand the nestedresoio contactCLI (list(with
--include-hidden) /get/search/add/accept/remove, each
with--format human|json).
-
Authmodality: A new unary modality for Resonite cloud authentication —
sign in / out and read the auth status — drivingEngine.Cloud.Session
directly (Login/Logout/Status, all returning a unifiedAuthStatus
oflogged_in/user_id/user_name/session_expires_unix_nanos).logintakes a credential (username / email /U-id) and a password (plus
an optionaltotpfor 2FA) andremember_me(default true), which delegates
session persistence to the engine — resoio stores no credentials on disk.- Wrong credentials return
Unauthenticated; a 2FA-enabled account with
no/blank code returnsFailedPrecondition, and the CLI then prompts for the
code and retries once. - Security: the plaintext password is never persisted, logged, placed in an
exception / gRPC status detail, or--format jsonoutput, and there is no
--passwordCLI flag — the password comes only fromRESONITE_IO_PASSWORD,
piped stdin, or a hidden prompt. - All three leaves support
--format human|json; the humanstatusoutput
renders the session expiry as a UTC datetime, and the--format jsondocument
adds a derived ISO-8601session_expires_isonext to the exact
session_expires_unix_nanos. When the credential is omitted, the interactive
prompt readsUsername or Email— a username, email, or user id is accepted. - Exposed as
AuthClientand the nestedresoio auth login/logout/
statusCLI.
-
Sessionmodality: A new unary userspace modality that drives the dash
"Session" dialog — the connected session's Settings, Users, and Permissions
tabs — by reading/writingWorld.Configuration/World.AllUsers/
World.Permissionsdirectly (no UI automation).- Settings use a get + partial-apply model (
GetSettings/
ApplySettings): world name/description, max users, access level,
hide-from-listing, mobile-friendly, away-kick, auto-save, auto-cleanup, and
tags. Partial updates useproto3 optionalpresence, sofalse/0can be
set explicitly and unset fields are left untouched (tagsuse a
replace_tagsgate);ApplySettingsreturns nothing — callGetSettingsto
read the new state. - Users expose
ListUsersplus host-gatedKickUser/BanUser/
SilenceUser/RespawnUser/SetUserRole; targets resolve byuser_id
(preferred),user_name, orlocal(self), andrespawndefaults to self. - Permissions expose
ListRoles(with the default
anonymous/visitor/contact/host/owner roles) andGetUserRoleOverrides. - Host-gated operations return
PermissionDeniedwhen the local user lacks the
right, and out-of-rangemax_usersreturnsInvalidArgument. - Exposed as
SessionClientand the nestedresoio sessionCLI (settings get/set,users list,user kick/ban/silence/respawn/role,roles list,overrides list).
- Settings use a get + partial-apply model (
-
resoio shutdown/resoio.shutdown: The graceful-stop command and
convenience function are now namedshutdown, matching Resonite's terminology
and theLifecycle.ShutdownRPC. Behaviour is unchanged — it reads the engine
PID fromInfo(for reporting) and sendsLifecycle.Shutdown; the engine quits
itself and Steam/Proton reaps the renderer + launch wrappers. Prints / returns
the engine's host PID, or "resonite not running" /Nonewhen no engine is
reachable. -
resoio --format human|json: Commands that return structured data (ping,
info,display,cursor,grabber,context-menu,dash,world,mic,
session) gained a--formatflag.human(default) keeps the existing text
output unchanged;jsonprints one machine-readable document to stdout (proto
field names in snake_case, enums as their name, big ints exact, non-ASCII
preserved).--formatis not added to pid/path-only commands (shutdown/
terminate,screenshot/record/world thumbnail), interactive commands
(drive/grabber interactive/inventory), or the side-effect-only
session user kick/ban/respawnleaves. -
resoio wait/resoio.wait_for_ready: A new startup-readiness gate that
blocks until the Resonite IO server answersConnection.Ping.- The public async
wait_for_ready(socket_path=None, *, timeout=None, interval=0.1)polls until a ping round-trips and returns the resolved socket
path, retrying while the socket is absent, has no listener yet, or the engine
is still warming up (FAILED_PRECONDITION);AmbiguousSocketErrorand other
gRPC errors propagate, andtimeout(None= wait forever) raises
TimeoutError. - The
resoio waitCLI wraps it: it prints the resolved socket path on success,
takes an optionalpidto targetresonite-{pid}.sock, and-T/--timeout
(default 30s,<=0tries once) bounds the wait.--formatis not added
(path-only output).
- The public async
-
Grabber post-grab interactions (
Use/Unuse/Equip/Dequip): The
Grabber service gained four unary RPCs (all returningGrabberGrabState) for
operating what a hand holds.Usepresses a virtual button (primary= left-click /secondary=
right-click) and holds it down untilUnuse, driven by a per-tick
ExternalInputre-injection repeater (Locomotion-style) so the press survives
across RPCs.primaryinjects both the digitalInteractaction and
the analog press-strength action, which is what makes strength-driven tools
such as Pens / Geometry Line Brushes (theBrushToolfamily, which fire on
analogprimaryStrength, not on the digital press) draw — holdUse, sweep
the cursor withcursor setto move the tip, thenUnuse.Equipfinds anIToolon a grabbed object and equips it into the hand;
Dequipremoves the equipped tool (both no-ops when nothing applies).Usetakes an optionalstrength(analog primary press pressure,0..1,
default1.0, server-clamped, ignored forsecondaryand missing →1.0)
usable as e.g. brush pressure.GrabberGrabStategainedis_tool_equipped/
equipped_tool_name/held_buttons.- Exposed as
GrabberClient.use/unuse/click(a press+release
convenience) /equip/dequip(use/clicktakestrength: float = 1.0) and theresoio grabberactionsuse/unuse/click/equip/
dequipwith--button {primary,secondary}and--strength(default1.0).
🔧 Changed
resoio grabis renamed toresoio grabber, and the action is now
required: 💥 Breaking: the top-level Grabber command isresoio grabberand
the action must be named explicitly —resoio grabber grab/release/
state/interactive. Bareresoio grabbernow errors with the argparse
usage code; the old implicit-grabdefault (whereresoio grabran a grab with
no action) is removed. The oldresoio grabcommand name is also removed
(no alias), so argparse rejects it. This aligns the command with theGrabber
modality name (likecursor/display/world). The Python API
(GrabberClient) and the gRPC wire are unchanged.resoio terminate/resoio.terminatenow force-stops the processes: 💥
Breaking: it was a deprecated alias ofresoio shutdown(a graceful
Lifecycle.Shutdownover gRPC); it now kills the engine + renderer host
processes (SIGTERM→SIGKILL) and takes[resonite_pid] [renderer_pid](or
auto-detects the single running instance). Useresoio shutdown/
resoio.shutdownfor the cooperative gRPC quit. The old gRPC
resoio.terminate(socket_path=...)signature is removed.resoio shutdown/resoio.shutdowndocumented as best-effort: the
gracefulLifecycle.ShutdownACK only confirms the quit was requested, not
that the engine exited. On Linux (including the dev container) FrooxEngine
frequently hangs during teardown and the engine never exits on its own
(issue #49), so the docs and example now spell out the cooperative pattern —
shutdownto ask nicely, thenterminatefor a guaranteed stop. Behaviour is
unchanged; the live e2e now drives that real flow (graceful request, then forced
terminate) instead of asserting a graceful exit that does not happen
in-container.resoio recorddefault output is now a file: 💥 Breaking: with no-o,
recordsavesrecord_<timestamp>.mp4(.wavfor--audio) to the current
directory instead of streaming to stdout. Pass-o -for the previous stdout
behaviour, or-o PATHfor an explicit file.screenshot/record/world thumbnailprint the saved path: on a file
save these now print the saved absolute path to stdout (screenshotwas
previously silent;world thumbnailpreviously logged to stderr), so a caller
can capture stdout to locate the artifact.-o -still streams raw bytes with
no path line.world thumbnailalso gained the dated-default /-o -target
rules to matchscreenshot/record.resoio micsummary moves to stdout: the end-of-stream summary
(received_frames/received_samples/dropped_frames/unix_nanos) is
the command result and now prints to stdout in both formats (was stderr); errors
and status messages stay on stderr.- Socket resolution skips dead sockets: directory-based socket resolution
(resolve_socket_path, used by every modality client) now reads the engine PID
from eachresonite-{pid}.sockcandidate and skips ones whose process is gone
(psutil.pid_exists), so a stale socket left behind by a SIGKILL'd engine no
longer causes a spuriousAmbiguousSocketErroror a connect to a dead UDS; only
live sockets count toward the found / ambiguous decision. Names that do not
encode an integer PID are kept. Adds apsutilruntime dependency.
🗑️ Removed
-
Container ↔ host Resonite bridge removed (migrated to in-container mod
launch): now that the dev container launches the mod-loaded Resonite itself
(just resonite-start), the host-side daemon (scripts/host_agent.py), its
container client (scripts/resonite_cli.py), thejust host-agentrecipe, and
the debug socket~/.resonite-io-debug/host-agent.sockare all removed.- The production gRPC UDS is no longer bind-shared with the host — the mod
creates it inside the container under~/.resonite-io/. - The host desktop screenshot bridge (the
just resonite-screenshotrecipe
/ host-agent /pyscreenshot) is removed; screenshots now go through the
existing in-engineresoio screenshot(CameraClient.shot(), Camera v2
framebuffer — e.g.resoio screenshot -o foo.png). just resonite-upis renamed tojust resonite-vanilla. TheGaleProfile/
GaleBinenv vars are dropped (the Gale profile is read from./gale).
- The production gRPC UDS is no longer bind-shared with the host — the mod