Docs: VR pet, its models, credits and changes outside Nix

docs/pet.md (usage, configuration, how it works), docs/pet-models.md
(the model spec), the README's feature line, options, changes outside
Nix and Credits (Toon Cat FREE and Tuxedo Cat, CC-BY 4.0; Quaternius
Shiba Inu and Fox, CC0), the cleanup artifact, the layout and runtime
names, and the template.
This commit is contained in:
Pierre Kisters committed 2026-10-01 04:33:39 +02:00
1 parent 4034fd4ded
commit 77085bad8a
7 files changed
+440 -11

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ is reported as "left alone" and never touched:
| `ui-state`: `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | the dashboard patches' saved choices; `--all` only, never `--orphans`; stray `*.json.tmp` files |
| `launchers`: `/run/user/1000/steam-frame-nix/applications` | the entries of [launchers](launchers.md) (tmpfs); `--all` only (switches remove those of removed launchers themselves) |
| `screenshots`: `/run/user/1000/steam-frame-nix/screenshots` | a link to `*/userdata/*/760/remote/250820/screenshots` ([SteamVR screenshots](screenshots.md)); kept by `--orphans --keep screenshots` (while `steamUserId` is unset) |
| `pet`: `/run/user/1000/steam-frame-nix/vr-pet` | `icon.png`, a link to `*-vr-pet-icons/*.png` in the store ([VR pet](pet.md#how-it-works)), its empty `.lock` and stray `.icon.tmp.*` links, then the directory if empty; kept by `--orphans --keep pet` (while `pet.enable`) |
| `dirs` | `~/.local/state/steam-frame-nix` and `/run/user/1000/steam-frame-nix` when empty |
### Left by older versions
+15 -1
View File
@@ -35,7 +35,7 @@ Where things run:
## Repository layout
```text
flake.nix homeManagerModules, packages/apps (cleanup), checks, template
flake.nix homeManagerModules, packages/apps (cleanup, pet-*), checks, template
install.sh install / uninstall / cleanup (curl | bash; also the
steam-frame-nix-cleanup package and the debugger arm step)
template/ `nix flake init -t` / installer config: flake.nix, home.nix
@@ -129,6 +129,19 @@ modules/
steam-close-button.nix, steam-close-button/ SteamVR: X on the Steam window
window-curvature.nix, window-curvature/ SteamVR: curvature wheel
frame-controls.nix, frame-controls/ SteamVR: window control bar
pet.nix the VR pet (pet): patch, CLI, "+" menu entry, icon units
pet/
package.nix build: bakes, catalog, patch, CLI, icons, preview, tests
core.js SteamVR+preview+test (in the patch): behaviour, interaction
systemui.js, unpatch.js SteamVR: draws the pet in the scene graph, grip bar, menu
cli.mjs app: `vr-pet` over DevTools (also the "+" menu entry)
icon.sh service steam-frame-nix-pet-icon (+ .path), switch:
<runtimeDir>/steam-frame-nix/vr-pet/icon.png -> model icon
bake.py, bake_gltf.py, rig.py build: the cat's / a glTF animal's frames (OBJ per frame)
index.py, skin.py, thumb.py build: catalog (models.json), coats, thumbnails, icons
models/<id>/model.json build: the built-in models (spec: docs/pet-models.md)
preview/ dev: desktop preview (`nix run .#pet-preview`)
check.nix, tests/ test: core, adapter, CLI, models, icons, private-model guard
```
## Runtime names
@@ -151,6 +164,7 @@ they are kept even where a file name says more:
| `steamvr-debugger` | `steamvrDebugger` | | `steamvr-webhelper-debugger` |
| `launchers` | `launchers` | | `steam-frame-nix-launchers` (`.service`, `.path`) |
| `screenshots` | `screenshots` | | `steam-frame-nix-screenshots` (`.service`, `.path`) |
| `pet` | `pet` | `vr-pet` (SteamVR, state) | `steam-ui-patches`, `steam-frame-nix-pet-icon` (`.service`, `.path`) |
| `docker` | `docker` | | `docker` |
Log of all patches:
+254
View File
@@ -0,0 +1,254 @@
# VR pet models
How to add a model to the [VR pet](pet.md): the spec's one description (the
scripts in `modules/pet/` point here). Credits of the built-in models:
[README, Credits](../README.md#credits).
Every model is a folder with a `model.json` (its spec) and any files the
spec names (a texture, a `.glb`, a thumbnail, a Blender script). The
built-in ones are `modules/pet/models/<id>/`, found by `builtins.readDir`
(nothing else lists them). Your own go into `steamFrame.pet.extraModels`:
either such a folder (`corgi = ./pets/corgi;`) or the spec as an attrset,
with files as Nix paths (`calico = { name = "Calico"; png = ./calico.png; };`).
Rebuild, and the model shows up in the pet's ⋯ menu and in
`vr-pet models`. The id (folder or attribute name, `vr-pet model <id>`):
letters, digits, `-` and `_`.
An invalid spec fails the build with a message naming the model and the field
(index.py `validate()`, bake_gltf.py for the animal fields).
## Fields (every kind)
| field | meaning |
|-----------|---------|
| `name` | the text in the menu (required) |
| `kind` | `base`, `recolor`, `texture` or `gltf` (required; an extraModels attrset with a `png` defaults to `texture`) |
| `order` | the menu's order (the coats 0-4, the animals 10-19; default 100), then the name |
| `default` | `true` on the one model shown until another is picked (the Ginger cat); `steamFrame.pet.defaultModel = "<id>"` overrides it |
| `group` | the menu puts a line between groups: `coats` (default for the cat's kinds) or `animals` (default for `gltf`) |
| `thumb` | a picture of the model's own (a file of the folder) for the menu, instead of a render (thumb.py: the sitting animal, else the standing one; 48 px) |
| `private` | `true`: personal use only, never published ([below](#private-models)); must be a boolean; models.json carries it for every model (`false` by default) |
| `rights` | who owns the character and why it is private (a text); a spec with `rights` must have `"private": true` |
| `extends` | another model's id: its spec with this one's fields on top ([below](#another-build-of-the-same-animal-extends-proportions-colors)) |
## The cat and its coats
- `base`: the Toon Cat, baked by bake.py (its sources are pinned in
package.nix). There is one.
- `recolor`: a coat of the cat, skin.py moving the ginger fur to another
ramp: `"fur": ["#rrggbb" (dark end), "#rrggbb" (light end)]`, optional
`"white": ["chest", "muzzle", "paws"]` markings.
- `texture`: a coat of the cat with a texture of its own: `"png": "coat.png"`,
a 64 x 64 PNG laid out like the cat's (start from an edited copy of
`nix build github:lhns/steam-frame-nix#pet-models`/cat.png: the body's texels are a
side view, columns along the length, each a dark-to-light ramp).
Coats share the cat's frames and behaviour; they switch in place.
## Other animals: kind `gltf`
A rigged, animated glTF (`.glb`, or `.gltf` with its files), baked by
bake_gltf.py into one OBJ per frame (as the cat) with its own behaviour set:
the core (core.js) gives an animal what its clips allow.
| field | meaning |
|-----------|---------|
| `src` | the glTF: a file of the folder (`"model.glb"`), `{ "url": ..., "hash": "sha256-..." }` (fetched, pinned) or `{ "path": ... }` (a file of the folder, e.g. downloaded by hand), optionally turned into a glTF (below) |
| `height` | m, the standing animal's height (top of the ears) in its first idle frame; the cat is 0.30 |
| `feet` | the four foot bones (their ends), for the walk speed: how fast the feet move back while on the floor |
| `zones` | bones for the interaction points: `scruff` (the top of the skin it moves: where it is held from), `head`, `back` (the top of the skin above the joint: petting), `chin`, `tailBase` (`{ "bone": ..., "at": "joint" }`: the joint itself) |
| `follow` | `{ bone: bone it moves with }`: bones outside the leg chains that the skin follows (IK targets, as the Quaternius feet); in hand-keyed poses they keep their place relative to that bone |
| `keys` | hand-keyed poses (below) |
| `clips` | the core's clip names -> how each is made (below); `idle` and `walk` are required |
| `species` | `{ "actions": { name: { "pose": "stand", "weight": 1, "cooldown": 30 } } }`: the animal's own idle actions (a clip of that name), chosen now and then in that pose like the cat's groom or stretch |
| `texture` | px, the width of a base colour texture in the atlas (default 256; 512 for a detailed face) |
| `proportions` | per bone `scale`, `rot`, `aim` on the rest pose: a reshaped animal (below) |
| `colors` | flat materials' colours: a recoloured animal (below) |
| `thumbFrame` | the baked frame the menu's render shows instead, e.g. `"idle_0"` (the Dachshund: its long body shows standing) |
| `credit` | the source and licence (a built-in model: also in [README, Credits](../README.md#credits)) |
### Sources that are not a glTF yet
`src` (`{url, hash}` or `{path}`) may also have (package.nix `gltfOf`); the
built-in models need none of them, a model from a `.blend` all three:
- `"unzip": "Dir/*"`: it is a zip; the members matching the glob are
unpacked. For a fetched one the `hash` is of those unpacked files (a
recursive hash), not of the zip (a service may zip it on the fly).
- `"blend": "Dir/model.blend"`: the .blend to open (its path in the zip).
- `"blender": "convert.py"`: a Blender script of the folder, run at build
time as `blender -b <blend> --python convert.py -- <out.glb>`; it exports
the glTF (and may clean it up: drop meshes, add bones, key clips,
decimate). The build fails when the script does or writes nothing.
Blender (`pkgs.blender`) is then a build input of that model only.
### Clips: how animations map
The core asks for clips by name. Each pose has a loop, transitions join
poses, actions are one-shot clips that start and end in their pose:
| pose | loop | into it (and back) |
|--------|-----------|-----------------------------------------------------|
| stand | `idle` | |
| walk | `walk` | `walkstart` (stand -> walk; played backwards to stop) |
| sit | `sit` | `sitdown` (stand -> sit), `standup` (sit -> stand) |
| lie | `lie` | `liedown` (stand -> lie; backwards to get up) |
| sleep | `sleep` | `curl` (lie -> sleep; backwards to wake) |
| petted | `purr`, `sitpurr`, `liepurr` | `purrin`, `sitpurrin`, `liepurrin` |
| held | `dangle` | (hangs from the scruff: `"hang": true`) |
| dropped| `fall` | `land` (an action: lands, ends standing) |
Actions: `startle` (a fast hand close by; then it backs off), `land`, the
cat's `groom` (sitting), `stretch`, `shake`, and the animal's own
(`species.actions`).
Whatever is missing falls back: a pose without its loop is not there
(commands and a saved pose take the nearest: sit, lie -> stand, sleep ->
lie); a transition without its clip is a cut; an action without its clip is
never chosen; petting without a purr loop does nothing; no `startle` / `land`
clip: it backs off / goes on at once; `dangle` / `fall` without their loops
show the nearest (fall, then stand).
Each clip is one of:
- `"Walk"`: that clip of the glTF, sampled at the baked fps (looping for a
pose loop).
- `{ "clip": "Jump_ToIdle", "start": 0.5, "end": 1.3, "fps": 12, "loop": false }`:
a part of it, at its own fps (12 for slow ones: fewer frames).
- `{ "clip": "Jump_ToIdle", "hold": 0.5 }` (or `"hold": "last"`): one frame
held (`"seconds"`, `"layers"` as below).
- `{ "key": "sit", "seconds": 3, "fps": 8, "layers": [...] }`: a hand-keyed
pose held; with layers a loop (at half the fps, or `fps`).
- `{ "blend": ["stand", "sit"], "seconds": 0.9 }`: a transition from the first
frame of one clip or key to another's.
- `{ "reverse": "sitdown" }`: another clip backwards.
- `"hang": true` on any: hung from the scruff (the dangle), not on the floor.
- `"lift": 0.12` on any: that many m above the floor (flying): a loop all
the time; a one-shot rises over its first quarter and settles over its
last (e.g. a flying animal's `hover` and `startle`).
A layer is a sine on top: `{ "bone": "Tail1", "rot": [0, 22, 0], "period": 0.4, "phase": 0.5 }`
(degrees about the body's axes: a wagging tail) or `"scale": [0.03, 0, 0.03]`
(breathing).
### Keys: hand-keyed poses
`{ "from": "Idle", "at": 0, "rot": { bone: [x, y, z] }, "aim": { bone: [x, y, z] } }`:
that frame of a glTF clip, with bones turned (degrees about the body's axes:
x its left, + = nose down; y up; z forward) and / or aimed (the bone, toward
its first child joint, pointed along a direction in the body's axes: `[0, -1, 0]`
straight down, `[0, -0.1, 1]` forward along the floor). Parents are done
first; a child turns with its parent. Every frame is put on the floor (its
lowest point), so fold the legs until the body rests where it should.
Check the poses by rendering frames (thumb.py renders an OBJ with the
texture; `nix run github:lhns/steam-frame-nix#pet-preview` plays them all).
Turn a head with the bone that carries it and its ears: in the Quaternius
rigs the ears hang from `Neck3`, not from `Head`, and the skull's top is
skinned to them, so turning `Head` alone twists the face under still ears.
The Shiba, Fox and Dachshund turn their heads at `Neck3` (with `Neck1` /
`Neck2` for the neck; the split keeps the head where a `Head` turn had put
it) and relax the ears on top at `Ear2` (sleep).
### Budget and texture
- **Faces:** at most 3000 triangles per frame (the cat has 2636): vrcompositor
draws every mounted frame, and a few hundred are mounted. The bake fails
above it: decimate the model first (e.g. Blender's Decimate modifier).
- **Texture:** a render model has one texture. bake_gltf.py makes it:
flat material colours become a palette (one 8-texel column per material,
a little darker toward the floor by the vertex's height when standing:
baked shading like the Toon Cat's), a material's base colour texture is
stacked below it (UVs in 0..1), `texture` px wide (default 256).
Everything is `cat.png` / `cat.mtl` next to the frames (the file names
are shared by all models).
- **Facing:** the glTF's +Z forward, +Y up (as Blender's glTF export).
### A full example (the Shiba Inu, shortened)
```json
{
"name": "Shiba Inu", "kind": "gltf", "order": 10,
"credit": "Shiba Inu, Ultimate Animated Animal Pack by Quaternius, CC0 1.0",
"src": { "url": "https://static.poly.pizza/ba6d0ee3-bcc0-4ef0-9d3c-a3e245b41c77.glb",
"hash": "sha256-nL1PHhL4UCQSbqtqLFcGwJkZbfGhDbdObItnusP/9Uo=" },
"height": 0.35,
"feet": ["FrontLowerLeg.L_end", "FrontLowerLeg.R_end", "BackLowerLeg.L_end", "BackLowerLeg.R_end"],
"follow": { "IKFrontLeg.L": "FrontLowerLeg.L", "IKFrontLeg.R": "FrontLowerLeg.R",
"IKBackLeg.L": "BackLowerLeg.L", "IKBackLeg.R": "BackLowerLeg.R" },
"zones": { "scruff": "Neck2", "head": "Head", "back": "Torso2",
"chin": { "bone": "Head", "at": "joint" }, "tailBase": { "bone": "Tail1", "at": "joint" } },
"keys": {
"stand": { "from": "Idle" },
"sit": { "from": "Idle",
"rot": { "Back": [-38, 0, 0], "Neck1": [8, 0, 0], "Neck2": [6, 0, 0], "Neck3": [18, 0, 0] },
"aim": { "FrontUpperLeg.L": [0.03, -1, 0.05], "BackLeg.L": [0.2, -0.3, 1],
"BackUpperLeg.L": [0.05, -0.35, -1], "BackLowerLeg.L": [0.05, -0.2, 1] } }
},
"clips": {
"idle": { "clip": "Idle", "fps": 12 },
"walk": "Walk",
"walkstart": { "blend": ["stand", "walk"], "seconds": 0.3 },
"sit": { "key": "sit", "seconds": 3, "layers": [{ "bone": "Torso2", "scale": [0.025, 0, 0.025], "period": 3 }] },
"sitdown": { "blend": ["stand", "sit"], "seconds": 0.9 },
"standup": { "reverse": "sitdown" },
"purr": { "key": "stand", "seconds": 1.2, "fps": 24,
"layers": [{ "bone": "Tail1", "rot": [0, 22, 0], "period": 0.4 }] },
"fall": { "clip": "Jump_ToIdle", "hold": 0.5 },
"land": { "clip": "Jump_ToIdle", "start": 0.5 },
"startle": { "clip": "Idle_HitReact_Left", "loop": false },
"sniff": { "clip": "Eating", "fps": 12, "loop": false }
},
"species": { "actions": { "sniff": { "pose": "stand", "weight": 1.2, "cooldown": 45 } } }
}
```
The complete ones in `modules/pet/models/`: `shiba/model.json`,
`fox/model.json` (same rig), `dachshund/model.json` (the Shiba reshaped:
below).
### Another build of the same animal: `extends`, `proportions`, `colors`
`"extends": "<id>"` starts from another model's spec: this one's fields go
on top (an object field entry by entry, so `"keys": { "sit": … }` replaces
only that key; the other's `default` and `thumb` are not taken). Files are
looked up in this folder, then in that model's. With these two fields
(bake_gltf.py) the same glTF and clips make a different animal:
- `proportions`: `{ bone: { "scale": [x, y, z], "rot": [x, y, z], "aim": [x, y, z] } }`,
kept in every clip. `scale`: the skin the bone moves, in the bone's own
axes (y along the bone, toward its child), from its joint; its children's
joints move with it but are not scaled (a longer back keeps its legs).
`rot` / `aim`: the bone's rest pose turned / aimed as in a key (body
axes), on top of the clip's own motion (ears hanging, a straight tail).
`follow` bones (IK feet) stay where they were relative to their bone,
that offset scaled with it, turned as in the clip (flat paws).
- `colors`: `{ material name: "#rrggbb" }`: a flat material's colour (the
glTF's material names; the Quaternius dogs: `Main`, `Main_Light`,
`Black`, `Eyes_*`).
The Dachshund (`dachshund/model.json`) is the Shiba Inu this way: back and
torso ×1.25-1.45 long, legs ×0.42-0.5, muzzle ×1.3, the ears longer and aimed
down and out, the tail straightened, black and tan; the Shiba's clips and
behaviour, with its own `sit` key (the Shiba's leans back too far for short
front legs to reach the floor). Short legs lift less, so a reshaped walk's
speed counts only the feet moving back. Check the face from the front too
when rendering frames (above): a bone's
`scale` / `rot` / `aim` also moves the skin weighted partly to it (the
Shiba's eyes and brow are about a fifth `Ear1`, so the Dachshund's ears
bend at `Ear2`, `Ear1` only tilted 20° out).
## Private models
A model of a character someone else owns (the licence of a fan model does
not cover the character) is for personal use only: its spec has
`"private": true` and a `rights` note saying whose it is. index.py checks
both (`private` a boolean; `rights` only with `"private": true`), and
models.json marks it (`"private": true`).
Such a model goes into `steamFrame.pet.extraModels` from your own
configuration (a folder in your repository), never into
`modules/pet/models/`: a built-in model with `"private": true` fails
evaluation (package.nix `builtinOf`; the `pet` check tests the refusal and
that a private extra model builds).
+109
View File
@@ -0,0 +1,109 @@
# VR pet
`pet.*`, module `pet` ([options](../README.md#options)).
A [SteamVR dashboard patch](ui-patches.md#steamvr-dashboard-patches).
## What you get
A 3D pet in SteamVR's scene, next to your windows: the Toon Cat in five
coats (Ginger, Tuxedo, Blue, Cream, Snow), a Shiba Inu, a Fox and a
Dachshund, plus your own models (`pet.extraModels`). It lives in the
dashboard's scene, so it is there in SteamVR Home and over games while the
dashboard runs.
- **On its own** it walks around you, sits, lies down, sleeps, follows you
beyond 2.5 m and reappears in front of you beyond 8 m. The cat also
grooms, stretches and shakes; the dogs and the fox sniff and look around.
- **Pick it up:** point the laser just above it (a grip bar shows up, like
a window's), press and drag like a window (the thumbstick pushes and
pulls). It dangles by the scruff, turns with the controller and falls
where you let go. No controller button is read; a falling pet is not
caught.
- **Pet it:** rest or move a controller slowly (< 0.4 m/s) on its head or
back (within 10 cm) for 0.6 s: it purrs (a dog wags its tail) while the
strokes go on. A fast swipe (> 1 m/s) close by startles it.
- **Its controls**, below the grip bar: X hides it (kept across SteamVR
restarts); ⋯ opens its menu: the models (a check on the current one, kept
across restarts), then Summon (1 m in front of you), Sit, Lie down,
Sleep. Buttons act on a press and a release on them, not during a drag or
0.3 s after. The menu closes on a pick, a press elsewhere, 1 s after the
laser left it, and when the pet hides.
- **Bring it back:** "Pet" in the dashboard's "+" menu (its icon is the
current model's), i.e. `vr-pet show`: at its spot while that is within
3 m and in view, else 1 m in front of you.
The `vr-pet` command:
```text
vr-pet show [--summon] show it (--summon: always 1 m in front of you)
vr-pet hide | status hide it; its state as JSON
vr-pet models the models (* the current one)
vr-pet model <id> switch (a hidden pet switches too)
```
## Configuration
```nix
steamFrame.pet.enable = true;
# steamFrame.pet.defaultModel = "shiba";
# steamFrame.pet.options = { walkSpeed = 0.3; follow = 3; }; # core.js DEFAULTS
# steamFrame.pet.extraModels.corgi = ./pets/corgi; # a model folder
```
The first switch bakes the models: a few minutes on the Frame and about
0.5 GB in the store (1.1 GB without `auto-optimise-store`, which hardlinks
the coats' copies of the cat's frames); the sources are fetched at build
time (no Blender needed for the built-in models). Adding your own model, the spec and the
animation mapping: [VR pet models](pet-models.md). A model of a character
someone else owns belongs in your own `extraModels` with `"private": true`
([private models](pet-models.md#private-models)), never in this repository.
Without a headset: `nix run github:lhns/steam-frame-nix#pet-preview` plays
the pet in a browser (mouse as the hands, a debug panel).
## Limitations
- vrcompositor draws every mounted frame: a few hundred static OBJs per
model, mounted gradually (`pet.mount`); switching to another animal takes
a few seconds to load.
- The pet is drawn by the dashboard's page: it is gone while SteamVR's
dashboard process restarts, and the patch reattaches within 15 s.
- It walks on the floor of the standing space and knows nothing of your
room's furniture.
- The "+" menu icon changes when the model does; a running Steam picks it
up when it rescans its icons (a few seconds).
## Credits
The models' authors and licences: [README, Credits](../README.md#credits).
## How it works
`vr-pet`, with [persistent state](ui-patches.md#persistent-state) (the
spot, pose, model and whether it is hidden). Debugging in the `systemui`
page: `window.__sfuiPet.cat.command('sit' | 'summon' | …)`,
`.cat.state()`, `.stats()`; the CLI calls only `window.__sfuiPet.api`.
Journal: `journalctl --user -t vr-pet -u steam-ui-patches`.
- **Build** (`modules/pet/package.nix`): bake.py retargets and hand-keys
the Toon Cat's clips and bakes one OBJ per animation frame; bake_gltf.py
does the same for a rigged glTF (the Quaternius animals); index.py makes
the catalog (coats as recoloured textures of the cat's frames, one
directory per model, 48 px thumbnails) and the "+" menu icons. Each
animal is a derivation of its own.
- **Behaviour** (`core.js`): poses, clips and transitions, wandering,
following, petting, the scruff drag and the fall, independent of where
it is drawn (also used by the preview and the tests).
- **Drawing** (`systemui.js`): every frame is a render model node under one
world-locked root in the dashboard's scene graph; the frames a clip needs
are mounted ahead, unused ones dropped. Each change resends the
dashboard's last scene graph message with only the pet's subtree
replaced. The grip bar is a panel like a window's handle; while dragged
the root is parented to the controller, so the compositor carries it.
- **"+" menu icon:** `~/.local/share/icons/hicolor/256x256/apps/vr-pet.png`
links to `/run/user/1000/steam-frame-nix/vr-pet/icon.png`, which the
oneshot user service `steam-frame-nix-pet-icon` points to the current
model's icon: on switch, at login and when the state file changes (path
unit); it bumps hicolor's mtime so a running Steam rescans. Lifetime and
cleanup: [Changes outside Nix](../README.md#changes-outside-nix-exceptions).
+6 -6
View File
@@ -47,15 +47,15 @@ and `sudo systemctl disable` them.
[Dashboard windows](dashboard-windows.md),
[Steam close button](steam-close-button.md),
[window curvature](window-curvature.md) and
[window control bar](window-control-bar.md) patch SteamVR's dashboard while
it runs. What they have in common:
[window curvature](window-curvature.md),
[window control bar](window-control-bar.md) and the [VR pet](pet.md) patch
SteamVR's dashboard while it runs. What they have in common:
- Each turns on the [SteamVR debugger](steamvr-debugger.md) (**the first
time, restart SteamVR once**).
- They depend on SteamVR UI internals: each is found by signature (its
entry in `modules/steam-ui-patches/lib/signatures.json` has the patch's
name); after an update that changes them the dashboard stays stock (see
name; the VR pet's are inline in `modules/pet/systemui.js`); after an update that changes them the dashboard stays stock (see
[after a Steam update](#after-a-steam-update)). Tested with SteamVR build
11008059.
- All are laser-only: gamepad navigation sees the stock dashboard.
@@ -102,8 +102,8 @@ per such patch in `~/.local/state/steam-frame-nix/ui-patches/<name>.json`
service writes the file atomically, only on change, only for that page's
`state` patches, at most 64 KiB.
Used by the [window control bar](window-control-bar.md) and the
[Steam close button](steam-close-button.md). The file is user data, not
Used by the [window control bar](window-control-bar.md), the
[Steam close button](steam-close-button.md) and the [VR pet](pet.md). The file is user data, not
generated by Nix: it is kept when the patch is disabled or removed and
removed only by `steam-frame-nix-cleanup --all` or `install.sh uninstall`;
see [changes outside Nix](../README.md#changes-outside-nix-exceptions).