Skip to content

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 22 Jun 12:55
398fdcf

✨ 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.

    • launch spawns the umu-launcher chain and PID-diffs the engine
      (resonite_pid) and renderer (renderer_pid) host processes into
      existence, returning both as a LaunchResult.
    • terminate stages SIGTERMSIGKILL over 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) and MOD_PATH (the Gale profile
      with the mod deployed) select the install; the ResoniteIO mod must be
      installed (via Gale / Thunderstore) or launch errors with guidance.
    • The cooperative gRPC quit stays available as resoio shutdown.
    • Exposed as resoio.launch / resoio.terminate / LaunchResult /
      LauncherError and the resoio launch (-e/--exe / -p/--profile /
      --vanilla / --format human|json) and resoio terminate
      ([resonite_pid] [renderer_pid]) CLI commands.
  • Run Resonite inside the dev container: the dev container can now launch
    Resonite itself via the new resoio launch / resoio terminate commands (and
    the thin just resonite-launch / just resonite-stop wrappers).

    • The container entrypoint rsyncs the read-only /resonite bind into a writable
      /opt/resonite, and resoio launch starts Resonite through umu-run /
      Proton with the ResoniteIO mod loaded from the ./gale Gale profile (the
      first run pulls GE-Proton and copies the ~2 GB install). resoio launch --vanilla runs vanilla Resonite with no mod.
    • Engine side loads via hookfxr, renderer side via a doorstop winhttp.dll
      resoio launch sets WINEDLLOVERRIDES="winhttp=n,b" automatically, so no
      manual Steam-style WINEDLLOVERRIDES setup 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 into gale/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.sh detects the vendor and selects the matching per-vendor
      compose overlay (.devcontainer/compose.{nvidia,amd,intel}.yml via the
      compose.gpu.yml symlink).
    • Requires kernel.apparmor_restrict_unprivileged_userns=0 on 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-slim to debian:13-slim
      (trixie), and compose.yml moved from the repo root to
      .devcontainer/compose.yml.
  • Contact modality: 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.

    • ListContacts returns the synced contacts with presence (online status +
      current session name / access level) plus the contact / request counts and a
      list-loaded flag, with client-side search (username / alternate-username
      substring) and filter (accepted friends / incoming requests). Like the dash
      tab it hides ShouldBeHidden contacts (ignored / blocked / none) by default —
      pass include_hidden to include them, and every contact carries an
      is_hidden flag.
    • GetContact fetches 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,
      and RemoveContact mutate the list; remove declines a request / deletes a
      friend, which the engine marks Ignored so the entry drops out of the default
      (hidden) list.
    • Unknown ids return NotFound, cloud failures Internal, and an unavailable
      cloud FailedPrecondition.
    • Exposed as ContactClient and the nested resoio contact CLI (list (with
      --include-hidden) / get / search / add / accept / remove, each
      with --format human|json).
  • Auth modality: A new unary modality for Resonite cloud authentication —
    sign in / out and read the auth status — driving Engine.Cloud.Session
    directly (Login / Logout / Status, all returning a unified AuthStatus
    of logged_in / user_id / user_name / session_expires_unix_nanos).

    • login takes a credential (username / email / U- id) and a password (plus
      an optional totp for 2FA) and remember_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 returns FailedPrecondition, 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 json output, and there is no
      --password CLI flag
      — the password comes only from RESONITE_IO_PASSWORD,
      piped stdin, or a hidden prompt.
    • All three leaves support --format human|json; the human status output
      renders the session expiry as a UTC datetime, and the --format json document
      adds a derived ISO-8601 session_expires_iso next to the exact
      session_expires_unix_nanos. When the credential is omitted, the interactive
      prompt reads Username or Email — a username, email, or user id is accepted.
    • Exposed as AuthClient and the nested resoio auth login / logout /
      status CLI.
  • Session modality: A new unary userspace modality that drives the dash
    "Session" dialog — the connected session's Settings, Users, and Permissions
    tabs — by reading/writing World.Configuration / World.AllUsers /
    World.Permissions directly (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 use proto3 optional presence, so false / 0 can be
      set explicitly and unset fields are left untouched (tags use a
      replace_tags gate); ApplySettings returns nothing — call GetSettings to
      read the new state.
    • Users expose ListUsers plus host-gated KickUser / BanUser /
      SilenceUser / RespawnUser / SetUserRole; targets resolve by user_id
      (preferred), user_name, or local (self), and respawn defaults to self.
    • Permissions expose ListRoles (with the default
      anonymous/visitor/contact/host/owner roles) and GetUserRoleOverrides.
    • Host-gated operations return PermissionDenied when the local user lacks the
      right, and out-of-range max_users returns InvalidArgument.
    • Exposed as SessionClient and the nested resoio session CLI (settings get/set, users list, user kick/ban/silence/respawn/role, roles list, overrides list).
  • resoio shutdown / resoio.shutdown: The graceful-stop command and
    convenience function are now named shutdown, matching Resonite's terminology
    and the Lifecycle.Shutdown RPC. Behaviour is unchanged — it reads the engine
    PID from Info (for reporting) and sends Lifecycle.Shutdown; the engine quits
    itself and Steam/Proton reaps the renderer + launch wrappers. Prints / returns
    the engine's host PID, or "resonite not running" / None when 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 --format flag. human (default) keeps the existing text
    output unchanged; json prints one machine-readable document to stdout (proto
    field names in snake_case, enums as their name, big ints exact, non-ASCII
    preserved). --format is 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 / respawn leaves.

  • resoio wait / resoio.wait_for_ready: A new startup-readiness gate that
    blocks until the Resonite IO server answers Connection.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); AmbiguousSocketError and other
      gRPC errors propagate, and timeout (None = wait forever) raises
      TimeoutError.
    • The resoio wait CLI wraps it: it prints the resolved socket path on success,
      takes an optional pid to target resonite-{pid}.sock, and -T/--timeout
      (default 30s, <=0 tries once) bounds the wait. --format is not added
      (path-only output).
  • Grabber post-grab interactions (Use / Unuse / Equip / Dequip): The
    Grabber service gained four unary RPCs (all returning GrabberGrabState) for
    operating what a hand holds.

    • Use presses a virtual button (primary = left-click / secondary =
      right-click) and holds it down until Unuse, driven by a per-tick
      ExternalInput re-injection repeater (Locomotion-style) so the press survives
      across RPCs. primary injects both the digital Interact action and
      the analog press-strength action, which is what makes strength-driven tools
      such as Pens / Geometry Line Brushes (the BrushTool family, which fire on
      analog primaryStrength, not on the digital press) draw — hold Use, sweep
      the cursor with cursor set to move the tip, then Unuse.
    • Equip finds an ITool on a grabbed object and equips it into the hand;
      Dequip removes the equipped tool (both no-ops when nothing applies).
    • Use takes an optional strength (analog primary press pressure, 0..1,
      default 1.0, server-clamped, ignored for secondary and missing → 1.0)
      usable as e.g. brush pressure. GrabberGrabState gained is_tool_equipped /
      equipped_tool_name / held_buttons.
    • Exposed as GrabberClient.use / unuse / click (a press+release
      convenience) / equip / dequip (use / click take strength: float = 1.0) and the resoio grabber actions use / unuse / click / equip /
      dequip with --button {primary,secondary} and --strength (default 1.0).

🔧 Changed

  • resoio grab is renamed to resoio grabber, and the action is now
    required
    : 💥 Breaking: the top-level Grabber command is resoio grabber and
    the action must be named explicitly — resoio grabber grab / release /
    state / interactive. Bare resoio grabber now errors with the argparse
    usage code; the old implicit-grab default (where resoio grab ran a grab with
    no action) is removed. The old resoio grab command name is also removed
    (no alias), so argparse rejects it. This aligns the command with the Grabber
    modality name (like cursor / display / world). The Python API
    (GrabberClient) and the gRPC wire are unchanged.
  • resoio terminate / resoio.terminate now force-stops the processes: 💥
    Breaking: it was a deprecated alias of resoio shutdown (a graceful
    Lifecycle.Shutdown over gRPC); it now kills the engine + renderer host
    processes (SIGTERMSIGKILL) and takes [resonite_pid] [renderer_pid] (or
    auto-detects the single running instance). Use resoio shutdown /
    resoio.shutdown for the cooperative gRPC quit. The old gRPC
    resoio.terminate(socket_path=...) signature is removed.
  • resoio shutdown / resoio.shutdown documented as best-effort: the
    graceful Lifecycle.Shutdown ACK 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 —
    shutdown to ask nicely, then terminate for 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 record default output is now a file: 💥 Breaking: with no -o,
    record saves record_<timestamp>.mp4 (.wav for --audio) to the current
    directory instead of streaming to stdout. Pass -o - for the previous stdout
    behaviour, or -o PATH for an explicit file.
  • screenshot / record / world thumbnail print the saved path: on a file
    save these now print the saved absolute path to stdout (screenshot was
    previously silent; world thumbnail previously logged to stderr), so a caller
    can capture stdout to locate the artifact. -o - still streams raw bytes with
    no path line. world thumbnail also gained the dated-default / -o - target
    rules to match screenshot / record.
  • resoio mic summary 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 each resonite-{pid}.sock candidate 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 spurious AmbiguousSocketError or 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 a psutil runtime 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), the just host-agent recipe, and
    the debug socket ~/.resonite-io-debug/host-agent.sock are 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-screenshot recipe
      / host-agent / pyscreenshot) is removed; screenshots now go through the
      existing in-engine resoio screenshot (CameraClient.shot(), Camera v2
      framebuffer — e.g. resoio screenshot -o foo.png).
    • just resonite-up is renamed to just resonite-vanilla. The GaleProfile /
      GaleBin env vars are dropped (the Gale profile is read from ./gale).