Add mouselook eye-height offset and vehicle-tilt decouple option - #356
Add mouselook eye-height offset and vehicle-tilt decouple option#356Shadowolf7 wants to merge 24 commits into
Conversation
Renames the user-facing/OS-facing product identity across build config, macOS bundle IDs (viewer + CEF plugin), Windows resource metadata, in-app strings, installer/Linux packaging scripts, and docs. Adds placeholder icon/logo art under indra/newview/branding/ (replacing the external alchemy-branding vcpkg dependency for now) and doc/vayu_logo.png. Internal code identifiers (settings keys, shader dir names, vcpkg triplets/ports, the "alchemy" skin theme) and .github/ CI workflows are intentionally left untouched — see the plan notes for why. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- gGLManager.mHasBPTCTextureCompression detects GL 4.2 core / GL_ARB_texture_compression_bptc
- setManualImage's runtime compression path now prefers explicit BC7 over
generic driver-chosen formats when available, sharing one lookup table
(compressed_internal_format) so the swap and its detection can't drift apart
- Fixes an isCompressed() regression: widening it to also detect the
generic-compression-on-upload case broke setImage()'s data_hasmips
branch, which relies on isCompressed() to tell raw pixels from
pre-compressed blocks. Split into isCompressed() (source-precompressed,
e.g. DXT containers) and willCompressOnUpload() (will be compressed by
setManualImage), each used only where they're actually the right question.
- setSubImage()/setSubImageFromFrameBuffer() now refuse partial updates on
a compressed-or-will-compress texture instead of issuing a GL call
(glTexSubImage2D/glCopyTexSubImage2D) that's illegal on compressed
internal formats -- previously silent corruption, now a clear LL_ERRS
- dataFormatBits/dataFormatBytes/dataFormatVRAMBytes/dataFormatComponents
didn't know the BC7 enums, so any BC7-compressed texture's memory
accounting hit LL_ERRS("Unknown format") the moment it was allocated
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyvmxbNsTvqm79FvJRUP3J
FMOD Studio on Linux: - FMODSTUDIO.cmake looked for libs under api/core/lib/x64, but the Linux SDK ships them under x86_64 -- find_library never matched - lllistener_fmodstudio.cpp/llaudioengine_fmodstudio.cpp reinterpret-cast LLVector3's float array to FMOD_VECTOR* and dereferenced it, which is strict-aliasing UB and a hard error under -Werror=strict-aliasing on current GCC; build by value instead (to_fmod_vector helper) - viewer_manifest.py never bundled libfmod.so* into the package (FMOD isn't a vcpkg dependency, so nothing copied it) -- binary linked against it but the .so was never shipped, so the packaged viewer couldn't start. New --fmod= manifest arg / FMOD_MANIFEST_LIB_DIR CMake var fix this. Packaging: the "Viewer Manifest Copy" post-build step passed --actions=copy alone. is_packaging_viewer() (gates whether shaders, fonts, skins, app_settings XML etc. are even considered) requires "package" in the actions list, but only copy_action() (needs "copy") actually writes files -- package_action() alone is a no-op. So this step silently produced a package missing nearly everything needed to run, without failing the build. Now passes "copy package" together, matching the LOCAL_DIST_DIR block's existing pattern. strip_binaries() (package_finish) is a no-op for non-Release buildtypes, so normal dev builds keep their debug info. Launcher: refresh_desktop_app_entry.sh only reinstalled the .desktop entry when its content actually changed (previously unconditional on every launch), added PrefersNonDefaultGPU=true, and exposes a "Launch with Zink (OpenGL-over-Vulkan)" right-click action via a new vayu-zink wrapper (gamemoderun + switcherooctl launch, degrading gracefully if either tool is absent). wrapper.sh carries over AMD performance tuning (AMD_DEBUG=lowprecision, mimalloc) from the Firestorm high-performance launcher. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TyvmxbNsTvqm79FvJRUP3J
Auto-detects ccache and sets it as the C/C++ compiler launcher before project()/enable_language() run (where CMake starts honoring CMAKE_<LANG>_COMPILER_LAUNCHER). Silently a no-op if ccache isn't installed, so this is safe to leave on by default. Speeds up rebuilds after a git pull/rebase that touch many files but leave most preprocessed output unchanged -- Ninja alone recompiles anything with a newer mtime even if the content is identical to something already compiled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TyvmxbNsTvqm79FvJRUP3J
LLViewerDynamicTexture (snapshot previews, floater thumbnails, etc.) and the terrain paintmap baking texture both refresh via setSubImageFromFrameBuffer(), which LLImageGL now refuses on a compressed-or-will-compress texture (partial/copy updates are illegal on compressed internal formats). Neither disabled compression on its texture, so enabling RenderCompressTextures hit that guard immediately. Fix mirrors the existing setAllowCompression(false) pattern already used for media textures and UI images. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TyvmxbNsTvqm79FvJRUP3J
Completes the drop of the vendored, patched curl 7.54.1 (which existed solely for an old HTTP/1.1 pipelining fix that no longer applies -- modern curl dropped real HTTP/1.1 pipelining years ago, making CURLOPT_PIPELINING a no-op regardless) in favor of stock vcpkg curl. That drop exposed a real, previously-masked bug: Linden Lab's asset CDN sends "Content-Encoding: binary/octet-stream" on essentially every asset response (a MIME type, not a transfer coding -- almost certainly swapped with Content-Type in the S3 object metadata upstream). Old curl silently tolerated an unrecognized Content-Encoding value; curl 8.21.0 treats it as a hard failure (CURLE_BAD_CONTENT_ENCODING / "Easy_61"), which was surfacing as near-100% mesh header and texture fetch failures -- explaining why meshes and sculpts stopped loading, especially right after a teleport into a busy region. Add a per-policy-class PO_CONTENT_DECODING option to llcorehttp and disable it for the binary asset-fetch classes (texture, mesh1, mesh2, large mesh) that talk to the CDN, while leaving it enabled for capability calls (inventory, agent, etc.) that legitimately use gzip. Both CURLOPT_ENCODING and CURLOPT_HTTP_CONTENT_DECODING are set explicitly on every request rather than left unset when disabled, since libcurl easy handles are recycled from an unreset pool (HttpLibcurl::HandleCache) and would otherwise carry a prior request's decoding state forward. Also restore the QAModeHttpTrace debug setting (referenced in _httpoprequest.cpp but missing from settings.xml, so libcurl verbose tracing had no way to be enabled) -- it's what surfaced the bogus response header in the first place. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7R2Za4br1N947HEDgf8zr
LLMeshRepoThread::run()'s header-fetch loop silently dropped a request on final retry exhaustion (just a debug log), unlike the LOD and skin loops right next to it, which push into mUnavailableQ on the identical exhaustion condition so waiting objects get unstuck. Any LLVOVolume waiting on that mesh's header never received notifyMeshLoaded or a forced LOD fallback, leaving it at its placeholder shape permanently with no further retry. fetchMeshHeader() returns false when the local HTTP layer refuses to even enqueue the request (congestion), which is rare in ordinary play but close to guaranteed right after a teleport into a busy region, when a whole region's object list arrives at once and hits the fixed concurrent-request cap -- matching the reported symptom of mesh objects never rezzing after teleporting into crowded regions. Mirrors LLMeshHeaderHandler::processFailure(): push all LOD levels for the mesh onto mUnavailableQ so dependent objects fall back instead of waiting forever. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7R2Za4br1N947HEDgf8zr
…world alpha Depth-splices rigged attachment alpha into the distance-sorted world alpha walk instead of rendering rigged-then-world in two passes, so transparent geometry composites correctly both in front of and behind avatars (e.g. hair vs. background trees). Gated by RenderInterleavedAlpha (default on); legacy two-pass order remains available by disabling it. Ported from secondlife/viewer#5927 (Viscerous/viewer feature/interleaved-alpha), adapted for local divergence: - lldrawpoolalpha.cpp: dropped the GLTF-scene depth-buffer block from upstream's diff since LL::GLTFSceneManager was already removed here (9a6ace9, "Remove dead PMFP render pipeline and scene manager"); kept this fork's simplified sRenderingHUDs-free debug-alpha condition from 47b8fb7/c62735adc8 and only applied the EAlphaStream rename. - llspatialpartition.h: added <functional> alongside this fork's boost::unordered_map include rather than upstream's std <queue>/ <unordered_map>, and hand-added mAvatarDepth to LLSpatialGroup to match this fork's member layout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d windlight quick-select Four Firestorm-inspired features, adapted to this fork's existing systems rather than ported verbatim where the underlying APIs differ: - Pose stand: fsfloaterposestand.cpp/h ported from Phoenix Firestorm, reusing fspose.cpp/h (already present but never wired into CMakeLists.txt since the April 2026 Poser merge). Uses LLAgent::stopCurrentAnimations(force_keep_script_perms) -- ported Firestorm's FIRE-12148 fix alongside it, since dropping that flag would silently revoke script animation permissions (e.g. XPOSE) every time the pose stand opens. New "not_sitting" Agent.IsActionAllowed case gates the menu item and toolbar command. - FPS limiter: new ALLimitFramerate/ALFrameRateLimit settings gate a sleep-based frame cap in LLAppViewer's main loop, ported from Firestorm's FSLimitFramerate. Vayu had no equivalent -- the existing sleeps in that loop are all background-yield/IO-throttle, not a foreground cap. - VRAM draw-distance toggle: new ALDrawDistanceVRAMOptimization setting (default on, preserving existing behavior) gates the sDesiredDiscardBias-driven draw-distance reduction in llviewerdisplay.cpp, matching Firestorm's FIRE-35748 -- this behavior already ran unconditionally here, so the change is additive only. - Windlight quick-select: extends the existing ALPanelQuickSettings panel (status-bar quick-settings button) with sky/water/day-cycle preset combos and a reset-to-region-default button, built against this fork's actual inventory-item-based LLEnvironment API rather than porting Firestorm's older flat-file preset scan. RLV-gated on RLV_BHVR_SETENV to respect @setenv=n restrictions. Also fixes a pre-existing build bug found while testing this: the POST_BUILD viewer_manifest.py invocation passed --actions as an escaped-quote CMake string that didn't survive Ninja's re-escaping, arriving as ['copy\', 'package'] instead of ['copy', 'package']. Since copy_action() is gated on the exact "copy" action being present, every non-packaging dev build was silently skipping is_packaging_viewer()'s file staging (app_settings, skins, shaders, etc.) -- builds "succeeded" but ran against arbitrarily stale staged assets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nfig workaround wrapper-zink.sh now just sets the Zink override and hands off to wrapper.sh instead of duplicating the gamemoderun/switcherooctl launch logic, so Zink-mode launches get the same GameMode/GPU-selection behavior as normal launches without a second copy of the logic to keep in sync. Also skip both under LL_WRAPPER so gdb/valgrind troubleshooting gets a plain process tree. OPENSSL_CONF=/dev/null works around the statically-linked OpenSSL trying to read the system's crypto-policy config on distros (openSUSE/Fedora) whose config properties it doesn't recognize, which fails config parsing on the first TLS handshake and gets misreported by curl as CURLE_OUT_OF_MEMORY. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Git worktrees do not inherit submodule checkouts from the main clone, so `git worktree add` produces a tree with indra/dullahan and vcpkg both empty. Neither resulting failure names the cause: dullahan surfaces as a bare "does not contain a CMakeLists.txt" from add_subdirectory() roughly 1100 lines into the log, after a full vcpkg install has already run, and vcpkg surfaces as a missing CMAKE_TOOLCHAIN_FILE. Detect both up front -- ahead of the first project() call, so it runs before the toolchain is loaded -- and run `git submodule update --init --recursive` when either marker is absent. If git is missing, or the markers are still absent afterwards, fail immediately with the exact command to run rather than letting the confusing downstream error happen anyway. On a normal checkout this is two EXISTS tests and nothing else; the git call only ever runs when a submodule is genuinely missing. Verified by reproducing the snag in a throwaway worktree: both submodules were absent, the guard detected them, cloned both, and configure continued through vcpkg bootstrap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ported from Cool VL Viewer's gUsePBRShaders/RenderUsePBR concept, adapted to Vayu's architecture. Unlike CoolVL (which swaps its entire ee/ vs pbr/ shader tree and tears down render pools), Vayu already renders legacy Blinn-Phong and GLTF PBR materials side by side in one unified deferred pipeline, so this only needs to gate the few places that route a face to the PBR draw pool and shading path: - LLPipeline::getPoolTypeFromTE(): stop routing GLTF-materialed faces to POOL_GLTF_PBR when the setting is off, so they fall through to the same pool selection an ordinary legacy face would get. - LLVolumeGeometryManager::registerFace(): stop nulling the face's legacy diffuse texture when the setting is off. That nulling assumed the PBR pool's own per-channel textures would be bound instead; skipping it lets the face's legacy fallback diffuse texture (the same texture non-PBR viewers already see, per SL's existing backward-compat convention for GLTF materials) get bound normally. - LLFace::canRenderAsMask(): let a de-PBR'd face qualify for alpha-masking like any other legacy face instead of being unconditionally excluded. Reflection probes, hero probes, SSR, and the GBuffer ORM slot are intentionally left untouched -- they're already independently controlled via their own settings and aren't part of what this toggle is for. Known gap: toggling the setting doesn't force already-rendered geometry to immediately re-evaluate its pool assignment, so visible objects only pick up the change on their next natural rebuild (LOD change, region change, relog) rather than instantly. Left alone rather than guessing at a rebuild-all mechanism without a way to test it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Notecards had no way to load content from disk (only copy/paste or the external-editor round trip); scripts already had one via the File menu's "Load from file..." item. Add a "Load..." button to the notecard preview floater using the same async LLFilePickerReplyThread pattern, feeding into the existing loadNotecardText() path (plain setText, not importBuffer -- that's reserved for the "Linden text version" container format used for embedded-item round trips). Both loadNotecardText() (notecard, also used by the external-editor sync path) and LLScriptEdCore::loadScriptFromFile() (script) now strip a UTF-8 BOM and normalize CR/CRLF line endings, since files picked off disk may not be ASCII/LF like our own tmp-file round trips. The script path previously reconstructed the buffer with getline(), which left stray \r characters on Windows-authored files; it now reads the file in binary mode and normalizes explicitly. Verified LLEmbeddedItems::removeUnusedChars() scans live text and self-prunes stale embedded-item references at save time, so replacing a notecard's text via the new load button cannot leave dangling embedded items. Known pre-existing gap, not touched here: loadScriptFromFile/ saveScriptToFile pass a raw LLScriptEdCore* across the async file-picker callback rather than a safe LLHandle, so closing the floater while the OS file dialog is open could dangle. Left as-is to keep this change scoped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some notecard authors include a line made of one repeated character purely to indicate the character width the content was formatted for (word-wrap/ASCII-art alignment assumes that width). Detect the longest such line (minimum 20 chars, to avoid tripping on ordinary short dividers like "-----") and, when found, switch the notecard editor to a monospace font and resize the floater to fit -- the convention is meaningless in a proportional font, since character widths vary and the notecard editor currently uses SansSerif. Applied everywhere notecard text gets (re)loaded: the initial asset load and loadNotecardText() (shared by the external-editor sync path and the "Load..." button added earlier). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Batch upload only recognized texture, sound, and animation files (LLResourceUploadInfo::findAssetTypeAndCodecOfExtension); .txt/.lsl/ .luau files were silently skipped even though the bulk-upload file picker already accepts any file (FFLOAD_ALL). - findAssetTypeAndCodecOfExtension now maps .txt -> AT_NOTECARD and .lsl/.luau -> AT_LSL_TEXT. - LLAgentBenefits::findUploadCost now recognizes both as free (cost 0) -- notecards and scripts have always been free to create, unlike the L$-per-item texture/sound/animation costs this function otherwise reports. Without this, do_bulk_upload's cost-lookup gate would silently drop these files even with the extension recognized. - LLNewFileResourceUploadInfo::exportTempFile gains a plain-text passthrough branch: no transcoding needed (unlike image/sound/ animation), just the same UTF-8 BOM strip + CRLF normalization already applied to the single-file notecard/script load paths, since this may be an arbitrary file off disk. No changes needed in do_bulk_upload itself or the file picker filter -- both the cost-estimate and real-upload code paths already route non-texture types through findUploadCost/findAssetTypeAndCodecOfExtension generically, same as sound/animation today. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The POST_BUILD and LOCAL_DIST_DIR custom commands passed --actions="copy package" to viewer_manifest.py with the quotes baked into the CMake string literal. Without VERBATIM, that survives into the literal shell command ninja runs as --actions="copy\ package"; the shell's double-quote parsing doesn't treat backslash-space as an escape, so Python receives the raw value copy\ package and llmanifest.py's whitespace .split() corrupts it into ['copy\\', 'package']. is_packaging_viewer() still matched, since the exact 'package' token survived, so construct() kept registering every skins/app_settings/ character/fonts file. But process_file()'s per-file dispatch looks up a method named "<action>_action" for each action: 'copy\\_action' doesn't exist and is silently skipped via getattr(..., None), and 'package_action' is a documented no-op. Net effect: every local build copied zero resource files into RelWithDebInfo, while the compiled binary kept landing there fine via CMake's own link step, independent of viewer_manifest.py. install.sh then faithfully cp -a'd an already-stale resource tree next to a fresh binary. Fix is to drop the redundant embedded quotes -- CMake's own escaping already handles the space correctly (confirmed against the regenerated ninja command line), same as the working --channel="Vayu Test" pattern elsewhere in this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Batch/bulk upload already supported notecards and scripts, but there was no single-file equivalent to Image/Sound/Animation/Model/Material -- only the "Bulk..." picker. Neither asset type has an upload-preview floater the way image/sound/ anim do, so upload_single_file's floater-name switch isn't a fit for them (its default case is a silent no-op, same failure shape as the bulk upload used to have before .txt/.lsl/.luau were recognized). Instead, upload_single_notecard_or_script() reuses the direct upload_new_resource() path do_bulk_upload already takes for these two types once the extension is recognized -- no preview step, since both are free to create. FFLOAD_SCRIPT already existed and needed no filter changes. FFLOAD_NOTECARD is new, wired into all three llfilepicker.cpp platform blocks (Windows OFN, GTK/portal, legacy macOS) the same way FFLOAD_SCRIPT is, plus build_extensions_string so extension validation isn't a no-op on Windows. Menu items are unmetered labels (no [COST]/Upload.CalculateCosts), matching Model/Material rather than Sound/Animation, since LLAgentBenefits reports 0 cost for both and CalculateCosts doesn't know about either asset type. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "Limit framerate" checkbox and FPS slider (added alongside the Firestorm-ported frame limiter) landed at computed Y 317-333 in Preferences > Graphics, directly under the Save/Load/Delete preset buttons, which used a hardcoded top="310" left over from before those controls existed. Full vertical and horizontal overlap -- confirmed by simulating the XUI top-down layout flow rather than eyeballing pixel math by hand. Switched the preset-button row to flow relatively (top_pad="14") off the new content instead of a stale absolute position, so it and everything chained below it (Defaults/Advanced Settings buttons) stays clear regardless of what's added above. Still fits within the panel's declared height with margin to spare. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The status-bar quick-settings panel's sky/water/day-cycle preset
combos sat under one generic "Environment:" heading with nothing
distinguishing which dropdown controlled what.
Checked Firestorm's floater_quickprefs.xml for the reference pattern
-- it labels each combo individually ("Sky:", "Water:", "Day Cycle:").
Added the same per-dropdown labels here, using this panel's own
existing label idiom (label pulled up beside its control via negative
top_pad, already used for Draw Distance/LOD/etc. above it) rather than
importing Firestorm's separate same-row layout mechanics, so it stays
visually consistent with the rest of the panel. Kept the "Environment"
heading as a section divider from the quality/performance settings
above it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "PBR Materials" checkbox (RenderPBRMaterials) in Advanced Graphics Preferences overlapped the Reset/OK/Cancel button row -- computed Y 419-435 vs. the button row's Y 422-445, same horizontal range. That row uses a hardcoded absolute top="415" rather than flowing, because this floater has two side-by-side columns sharing one coordinate stream and the standard top_pad trick only tracks the immediately preceding document-order sibling, not whichever column is actually tallest -- so a relative fix risked being correct today and wrong the next time either column grows. Bumped the absolute position instead (415 -> 440) to clear the checkbox's new bottom, and grew the floater's declared height (452 -> 477) to keep it from clipping the button row, preserving the original ~7px bottom margin. Also corrected the tooltip. It previously implied unchecking the setting reveals a texture SL guarantees via "existing backward-compat convention" -- checked the actual SL Wiki/community documentation and that's not accurate: a good legacy fallback texture is optional creator best practice, not a system guarantee, and the untouched default is just SL's blank/wood texture. Rewrote it to say that plainly, and to spell out that pool routing and alpha-mask eligibility fall back too (not just shading), and that reflection probes/hero-probes/SSR/AO are unaffected since they're scene-wide systems controlled separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Roadmap item 15, sourced from real user feedback (vehicle builder Tiff Haystack relaying customer complaints): mouselook eye height has no tunable offset and is derived straight from the avatar skeleton's head joint, which commonly mismatches mesh body/head visual eye position; and the mouselook view tilts with vehicle roll/pitch unless the vehicle's own script opts out via FLAGS_CAMERA_DECOUPLED, which a passenger can't control. Adds AlchemyMouselookEyeHeightOffset (F32 meters) applied in calcCameraPositionTargetGlobal(), and AlchemyMouselookDecoupleVehicleTilt (bool) checked alongside the existing server-flag check in calcFocusPositionTargetGlobal(). Also applies the eye-height offset to both the origin and look-at target in the AlchemyRealisticMouselook override path in updateCamera(), which otherwise bypasses calcCameraPositionTargetGlobal() entirely while standing. Decoupling is scoped to rotation only, per the roadmap note's open question left unresolved by choice.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Opened in error against the wrong repository (should have targeted a fork, not upstream). Closing immediately — no review needed. |
Summary
AlchemyMouselookEyeHeightOffset(F32, meters, default 0.0): a user-tunable vertical offset applied inLLAgentCamera::calcCameraPositionTargetGlobal(), since mouselook eye height is currently derived straight from the avatar skeleton's head-joint Z position, which commonly mismatches mesh body/head visual eye placement.AlchemyMouselookDecoupleVehicleTilt(bool, default off): checked alongside the existingFLAGS_CAMERA_DECOUPLEDserver-flag check inLLAgentCamera::calcFocusPositionTargetGlobal(), letting a passenger opt out of the mouselook view tilting with vehicle roll/pitch even when the vehicle's own script hasn't set that flag. Scoped to rotation only — camera position is untouched.AlchemyRealisticMouselookoverride path inupdateCamera()(~line 1558), which otherwise bypassescalcCameraPositionTargetGlobal()entirely while standing and would have left the new slider inert for anyone with that setting on. Applied to both the camera origin and look-at target so it doesn't introduce pitch.Verification
llagentcamera.cppwith-fsyntax-onlyagainst the project's realcompile_commands.jsonflags (return code 0, no diagnostics).top/top_pad/top_deltachain in document order (per this repo's established practice for this kind of check) — new content bottoms out at 348px against 435px of panel height, no overflow. Confirmedpanel_preferences_move_mlook.xmlis live (embedded as the "Mouse Input" tab inpanel_preferences_move.xml), not dead.skins/is only copied into the build output at CMake configure time, not on every incremental build — a stale skins dir would make the new controls appear missing even though they're present.Test plan
🤖 Generated with Claude Code