Implement a dedicated slider.wav sound effect that triggers only during manual slider adjustments#578
Conversation
Merge from master
Merge from master
Reviewer's GuideImplements a dedicated, rate-limited slider SFX and central SFX playback API, refines VolumeSlider to distinguish manual vs programmatic updates and handle invalid buses, suppresses duplicate navigation sounds when keyboard-adjusting sliders, documents the scripts/ layout and milestones, and adds GUT tests to ensure audio/UI/web sync without feedback loops. Sequence diagram for manual vs programmatic volume slider updates with SFX and debounced savesequenceDiagram
actor User
participant VolumeSlider
participant AudioServer
participant AudioManager
participant Timer_save_debounce
rect rgb(235, 250, 255)
Note over User,AudioManager: Manual slider drag or keyboard left/right on focused slider
User->>VolumeSlider: Drag slider / press ui_left or ui_right
VolumeSlider->>VolumeSlider: _on_gui_input(event) sets _is_dragging = true
VolumeSlider->>VolumeSlider: _on_value_changed(new_value)
VolumeSlider->>VolumeSlider: is_equal_approx(new_value, _previous_value)?
alt Value changed
VolumeSlider->>VolumeSlider: _previous_value = new_value
VolumeSlider->>AudioServer: set_bus_volume_db(bus_index, linear_to_db(new_value))
VolumeSlider->>AudioManager: set_volume(bus_name, new_value)
VolumeSlider->>VolumeSlider: _handle_slider_sfx(new_value)
VolumeSlider->>Timer_save_debounce: start()
Note right of Timer_save_debounce: Debounces disk saves
Timer_save_debounce-->>VolumeSlider: timeout
VolumeSlider->>AudioManager: _on_debounce_timeout() calls save_volumes()
else Value jitter (no real change)
VolumeSlider-->>VolumeSlider: Return early, no SFX, no save restart
end
VolumeSlider->>VolumeSlider: _notification(NOTIFICATION_FOCUS_EXIT) clears _is_dragging
end
rect rgb(245, 235, 255)
Note over AudioManager,VolumeSlider: Programmatic volume sync (no SFX, no debounce)
AudioManager->>VolumeSlider: set_value_programmatically(new_value)
VolumeSlider->>VolumeSlider: Guard bus_index != -1
VolumeSlider->>VolumeSlider: clamped_value = clamp(new_value, min_value, max_value)
VolumeSlider->>VolumeSlider: set_value_no_signal(clamped_value)
VolumeSlider->>AudioServer: set_bus_volume_db(bus_index, linear_to_db(clamped_value))
VolumeSlider->>AudioManager: set_volume(bus_name, clamped_value)
VolumeSlider->>VolumeSlider: _previous_value = clamped_value (no SFX, no timer)
end
Sequence diagram for centralized AudioManager.play_sfx with caching and poolingsequenceDiagram
participant Caller as Any_UI_or_System
participant AudioManager
participant SFXCache as _sfx_cache
participant MissingCache as _missing_sfx_cache
participant Pool as _sfx_pool
participant AudioServer
Caller->>AudioManager: play_sfx(sfx_name, bus_name, pitch_scale, volume_db)
alt Empty sfx_name
AudioManager-->>Caller: Return
else Nonempty sfx_name
alt sfx_name in _missing_sfx_cache
AudioManager-->>Caller: Return (known missing)
else sfx_name not in _missing_sfx_cache
alt sfx_name not in _sfx_cache
AudioManager->>AudioManager: full_path = SFX_DIR_PATH + sfx_name + .wav
AudioManager->>AudioManager: stream = load(full_path)
alt stream loaded
AudioManager->>SFXCache: Check size against MAX_SFX_CACHE_SIZE
alt Cache full
SFXCache->>SFXCache: Evict oldest_key
AudioManager->>Caller: Log eviction
else Cache not full
end
SFXCache->>SFXCache: Store stream under sfx_name
else load failed
AudioManager->>Caller: Log warning (missing SFX)
MissingCache->>MissingCache: Mark sfx_name as missing
AudioManager-->>Caller: Return
end
else sfx_name in _sfx_cache
SFXCache->>AudioManager: stream = _sfx_cache[sfx_name]
SFXCache->>SFXCache: Erase sfx_name
SFXCache->>SFXCache: Reinsert stream as newest (LRU update)
end
AudioManager->>Pool: Find first AudioStreamPlayer where playing == false
alt Free player found
Pool-->>AudioManager: player
else All players busy
Pool-->>AudioManager: Fallback to _sfx_pool[0]
end
AudioManager->>AudioManager: Configure player.stream, pitch_scale, volume_db
AudioManager->>AudioServer: get_bus_index(bus_name)
alt Invalid bus
AudioManager->>Caller: Log warning, fallback to BUS_SFX_MENU
AudioManager->>Pool: player.bus = AudioConstants.BUS_SFX_MENU
else Valid bus
AudioManager->>Pool: player.bus = bus_name
end
AudioManager->>Pool: player.play()
end
end
Updated class diagram for VolumeSlider, AudioManager, AudioConstants, and Globals input gatingclassDiagram
class VolumeSlider {
<<HSlider>>
+int SFX_COOLDOWN_MS
+String bus_name
+int bus_index
+Timer save_debounce_timer
-int _last_sfx_time
-float _previous_value
-bool _is_dragging
+_ready() void
+set_value_programmatically(new_value float) void
+_on_gui_input(event InputEvent) void
+_notification(what int) void
+_on_value_changed(new_value float) void
+_handle_slider_sfx(_new_value float) void
+_on_debounce_timeout() void
+get_previous_value() float
+get_last_sfx_time() int
+is_user_dragging() bool
}
class AudioManager {
+String SFX_DIR_PATH
+int MAX_SFX_CACHE_SIZE
+int SFX_POOL_SIZE
+float master_volume
+bool master_muted
+String current_config_path
-Dictionary _sfx_cache
-Dictionary _missing_sfx_cache
-Array~AudioStreamPlayer~ _sfx_pool
+_ready() void
+_init_to_defaults() void
+load_volumes() void
+apply_all_volumes() void
+set_volume(bus_name String, volume float) void
+save_volumes() void
+reset_volumes() void
+play_sfx(sfx_name String, bus_name String, pitch_scale float, volume_db float) void
+signal volume_changed(bus_name String, volume float)
+signal mute_toggled(bus_name String, is_muted bool)
}
class AudioConstants {
+String BUS_MASTER
+String BUS_MUSIC
+String BUS_SFX
+String BUS_SFX_ROTORS
+String BUS_SFX_WEAPON
+String BUS_SFX_MENU
+String SFX_SLIDER
+String SFX_MUTE_TOGGLE
+String SFX_UI_NAVIGATION
+Dictionary BUS_CONFIG
}
class Globals {
+_input(_event InputEvent) void
+_play_ui_navigation_sfx() void
}
VolumeSlider --> AudioManager : uses set_volume
VolumeSlider --> AudioManager : uses play_sfx
VolumeSlider --> AudioManager : uses save_volumes
VolumeSlider --> AudioConstants : uses SFX_SLIDER
VolumeSlider --> AudioServer : uses buses
AudioManager --> AudioServer : applies bus volumes
AudioManager --> AudioConstants : uses BUS_SFX_MENU
Globals --> AudioManager : calls _play_ui_navigation_sfx
Globals --> Slider : suppresses nav SFX when Slider focused
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Godot import remap for Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant VolumeSlider as VolumeSlider
participant AudioServer as AudioServer
participant AudioManager as AudioManager
participant FS as FileSystem
participant Player as AudioStreamPlayer
User->>VolumeSlider: interact (drag / keyboard)
VolumeSlider->>AudioServer: set bus volume
VolumeSlider->>AudioManager: play_sfx("slider", bus, pitch, vol)
AudioManager->>AudioManager: check _sfx_cache
alt cache miss
AudioManager->>FS: load AudioStream (`files/sounds/sfx/slider.wav`)
FS-->>AudioManager: AudioStream
AudioManager->>AudioManager: cache stream (evict if needed)
end
AudioManager->>Player: create AudioStreamPlayer(stream, bus, pitch, vol)
Player->>Player: play()
Player-->>AudioManager: finished -> queue_free()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- Double-check whether
.importfiles likeslider.wav.importare meant to be source-controlled in this project, as some teams rely on per-developer generated import data instead. - Consider confirming the folder structure and naming convention for SFX assets (e.g.,
files/sounds/sfx/) matches existing patterns so future audio assets stay organized consistently.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Double-check whether `.import` files like `slider.wav.import` are meant to be source-controlled in this project, as some teams rely on per-developer generated import data instead.
- Consider confirming the folder structure and naming convention for SFX assets (e.g., `files/sounds/sfx/`) matches existing patterns so future audio assets stay organized consistently.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
|
Overall Grade |
Security Reliability Complexity Hygiene |
Code Review Summary
| Analyzer | Status | Updated (UTC) | Details |
|---|---|---|---|
| Python | Apr 26, 2026 8:18p.m. | Review ↗ | |
| JavaScript | Apr 26, 2026 8:18p.m. | Review ↗ |
Important
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
Merge from master
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@files/sounds/sfx/slider.wav.import`:
- Around line 1-24: The slider.wav asset is imported but never played; add an
AudioStreamPlayer instance (e.g., slider_player) and wire it into the existing
audio code so sliding produces feedback: in volume_slider.gd create or preload
an AudioStream for "res://files/sounds/sfx/slider.wav", instantiate an
AudioStreamPlayer (slider_player), set its bus to the menu volume bus used by
the project (same bus name used elsewhere, e.g., "Menu" or the bus constant),
and call slider_player.play() from the slider value change handler only for
genuine user interactions; implement the user-interaction gating by listening
for gui_input on the slider (handle InputEventMouseButton/Touch events) or by
setting/clearing a local flag like user_is_interacting that is set on gui_input
and checked in _on_value_changed to avoid playing on programmatic changes, and
ensure the new player is freed or reused consistently (attach to the same node
as other audio players and follow existing naming conventions).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 0273c5e7-8d6e-4784-80e1-2fe5fb56e3a0
⛔ Files ignored due to path filters (1)
files/sounds/sfx/slider.wavis excluded by!**/*.wav
📒 Files selected for processing (1)
files/sounds/sfx/slider.wav.import
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: GUT Unit Tests / unit-test
- GitHub Check: GDUnit4 Unit Tests / unit-test
|
|
|
Tip For best results, initiate chat on the files or code changes.
If the bot doesn't respond automatically, make sure it's installed on the repository (via the All Contributors GitHub App) or that the |
Having reviewed the details of Release v0.9.18 and PR #582, there are definitely a few key areas in your README.md that need to be updated to stay aligned with the current state of the project.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
README.md (1)
157-157: Spelling variant inconsistency: use "centralized" for consistency.The project predominantly uses American English spelling. Line 157 uses "centralised" (British), which is inconsistent with other documentation.
🔤 Proposed fix
-| `scripts/core/` | Foundational systems: `game_paths.gd` (centralised path registry), `globals.gd`, `main_scene.gd`, `settings.gd` | +| `scripts/core/` | Foundational systems: `game_paths.gd` (centralized path registry), `globals.gd`, `main_scene.gd`, `settings.gd` |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` at line 157, Replace the British spelling "centralised" with the American spelling "centralized" in the README table entry that reads "centralised path registry" (the table row listing `scripts/core/` and `game_paths.gd`) so the documentation consistently uses American English.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@README.md`:
- Around line 154-161: The markdown linter is failing because the table rows
containing entries like `scripts/core/`, `scripts/resources/`,
`scripts/entities/`, `scripts/system/`, and `scripts/managers/` exceed the
80-character limit; fix it by surrounding this table with the markdown-lint
disable/enable comments (disable MD013 before the table and re-enable after) so
the long cells are ignored, or alternatively wrap the long cell content across
multiple lines within the table cells (e.g., split descriptions into stacked
lines) if you prefer not to change linter directives—apply the change near the
table block that lists the `scripts/*/` directories.
- Line 80: The slider.wav asset was added but not used; update the
volume_slider.gd component to play the sound in the _on_value_changed() handler:
load or preload the files/sounds/sfx/slider.wav asset (or reference an
AudioStreamPlayer node) and trigger a short playback inside _on_value_changed()
(respecting the existing debounce/save logic), ensuring you do not block save
debounce; also remove or correct unrelated README edits (the Deepsource and
unrelated scripts/ reorganization notes) so the README reflects only this PR's
slider-audio change.
---
Nitpick comments:
In `@README.md`:
- Line 157: Replace the British spelling "centralised" with the American
spelling "centralized" in the README table entry that reads "centralised path
registry" (the table row listing `scripts/core/` and `game_paths.gd`) so the
documentation consistently uses American English.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c9f3011e-06f4-4ce9-988c-a4ad52d05e62
📒 Files selected for processing (1)
README.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-04-10T00:07:55.427Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 534
File: scripts/player.gd:146-149
Timestamp: 2026-04-10T00:07:55.427Z
Learning: In `scripts/globals.gd` and `scripts/player.gd` (GDScript, Godot 4), `current_fuel` is intentionally treated as volatile session data and should NOT be saved to or loaded from `settings.cfg`. Only `max_fuel` (tank capacity) is a persistent setting. `current_fuel` is always reset to `max_fuel` unconditionally in `player._ready()`. Persisting `current_fuel` is considered an architectural mistake by the project maintainer (ikostan). Mid-run fuel state persistence is planned via a separate `SaveGameResource` in PR `#535`.
Applied to files:
README.md
🪛 GitHub Actions: Pull Request Pipeline
README.md
[error] 154-154: markdownlint-cli2 (markdownlint v0.40.0) reported MD013/line-length: Line length expected 80, actual 215.
🪛 GitHub Check: Markdown Lint / lint (3.x)
README.md
[failure] 161-161: Line length
README.md:161:81 MD013/line-length Line length [Expected: 80; Actual: 215] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md
[failure] 160-160: Line length
README.md:160:81 MD013/line-length Line length [Expected: 80; Actual: 215] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md
[failure] 159-159: Line length
README.md:159:81 MD013/line-length Line length [Expected: 80; Actual: 215] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md
[failure] 158-158: Line length
README.md:158:81 MD013/line-length Line length [Expected: 80; Actual: 215] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md
[failure] 157-157: Line length
README.md:157:81 MD013/line-length Line length [Expected: 80; Actual: 215] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md
[failure] 156-156: Line length
README.md:156:81 MD013/line-length Line length [Expected: 80; Actual: 215] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md
[failure] 154-154: Line length
README.md:154:81 MD013/line-length Line length [Expected: 80; Actual: 215] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md
🪛 LanguageTool
README.md
[uncategorized] ~157-~157: Do not mix variants of the same word (‘centralise’ and ‘centralize’) within a single text.
Context: ... Foundational systems: game_paths.gd (centralised path registry), globals.gd, `main_sce...
(EN_WORD_COHERENCY)
🔇 Additional comments (2)
README.md (2)
308-320: Incorrect PR reference and mischaracterization of commit scope.This is PR
#456(not#578), a documentation update for Milestone#16. The commit message explicitly references "PR#582" and the purpose is to update the README to align with Release v0.9.18. This is a large infrastructure commit that addsslider.wavand many other assets to the repository—not a feature PR that requires slider.wav to be integrated.The milestone documentation (lines 308-320) is appropriate for this update: both Milestone 14 and Milestone 16 correctly document the project's status and active focus areas at their respective release points. No changes to the milestone sections are necessary.
> Likely an incorrect or invalid review comment.
152-152: No action needed. The reference to PR#582is accurate and documented correctly. There is no PR#578in the repository history, so no mismatch exists between the documented PR number and the actual PR this change belongs to.> Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The new
VolumeSlidertests reach into several internal members and helper methods (e.g.,_is_dragging,_previous_value,_handle_slider_sfx); consider adding small public/query helpers or refactoring the logic behind a non-underscored method so tests rely less on private implementation details and are more resilient to refactors. - In
AudioManager.play_sfx, you create a newAudioStreamPlayernode on every call and only rely on thefinishedsignal for cleanup; if slider or UI sounds can be triggered frequently, consider pooling or reusing a small number of players to avoid excessive node churn and signal connections at runtime.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new `VolumeSlider` tests reach into several internal members and helper methods (e.g., `_is_dragging`, `_previous_value`, `_handle_slider_sfx`); consider adding small public/query helpers or refactoring the logic behind a non-underscored method so tests rely less on private implementation details and are more resilient to refactors.
- In `AudioManager.play_sfx`, you create a new `AudioStreamPlayer` node on every call and only rely on the `finished` signal for cleanup; if slider or UI sounds can be triggered frequently, consider pooling or reusing a small number of players to avoid excessive node churn and signal connections at runtime.
## Individual Comments
### Comment 1
<location path="scripts/ui/components/volume_slider.gd" line_range="81-94" />
<code_context>
+## :param new_value: The target volume (0.0 to 1.0).
+## :type new_value: float
+## :rtype: void
+func set_value_programmatically(new_value: float) -> void:
+ # Guard against external updates if the bus is invalid
+ if bus_index == -1:
+ return
+
+ # Godot 4 native method: updates visual value without emitting 'value_changed'
+ set_value_no_signal(new_value)
+
+ # Explicitly sync the audio backend, since the signal was bypassed
+ AudioServer.set_bus_volume_db(bus_index, linear_to_db(new_value))
+ AudioManager.set_volume(bus_name, new_value)
+
+ # Sync the delta tracker so the next manual interaction calculates correctly
+ _previous_value = new_value
+
+
</code_context>
<issue_to_address>
**suggestion:** Consider clamping programmatic values to the slider’s range before applying them.
Currently `set_value_programmatically` forwards `new_value` directly to `set_value_no_signal`, `AudioServer`, and `AudioManager`. If callers pass values outside the configured range, you can get UI/backend divergence or invalid dB conversions. Clamping `new_value` to the slider’s `[min_value, max_value]` (and using the clamped value for `_previous_value`) would make this safer against incorrect callers.
```suggestion
func set_value_programmatically(new_value: float) -> void:
# Guard against external updates if the bus is invalid
if bus_index == -1:
return
# Clamp to the slider's configured range to avoid UI/backend divergence
var clamped_value := clamp(new_value, min_value, max_value)
# Godot 4 native method: updates visual value without emitting 'value_changed'
set_value_no_signal(clamped_value)
# Explicitly sync the audio backend, since the signal was bypassed
AudioServer.set_bus_volume_db(bus_index, linear_to_db(clamped_value))
AudioManager.set_volume(bus_name, clamped_value)
# Sync the delta tracker so the next manual interaction calculates correctly
_previous_value = clamped_value
```
</issue_to_address>
### Comment 2
<location path="scripts/ui/components/volume_slider.gd" line_range="102-104" />
<code_context>
+## :param event: The input event passed by the UI system.
+## :type event: InputEvent
+## :rtype: void
+func _on_gui_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
+ _is_dragging = event.pressed
+
+
</code_context>
<issue_to_address>
**suggestion:** Touch and non-mouse interactions won’t ever flip `_is_dragging`, which could unintentionally suppress SFX.
`_is_dragging` is only updated for `InputEventMouseButton` with `MOUSE_BUTTON_LEFT`, so for touch and other pointer events (`InputEventScreenTouch` / `InputEventScreenDrag`) it remains `false`. Because `_handle_slider_sfx` depends on `_is_dragging` or keyboard focus, pointer interactions that don’t grant focus may never play SFX. To keep behavior consistent across input methods, also update `_is_dragging` for touch/drag events or base the decision on a more general condition (e.g., hover + pressed).
```suggestion
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
_is_dragging = event.pressed
elif event is InputEventScreenTouch:
# Touch down/up should mirror mouse press/release behavior.
_is_dragging = event.pressed
elif event is InputEventScreenDrag:
# Any active drag implies the pointer is currently dragging the slider.
_is_dragging = true
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
scripts/ui/components/volume_slider.gd (2)
127-150: Guard ordering:_previous_valueis committed before interaction/rate guards.The early commit at Line 133 (before the interaction and cooldown checks) is intentional per the comment, but it has a subtle consequence: if a non-interactive
value_changedslips through (e.g., a future code path writes tovaluedirectly withoutset_value_no_signal),_previous_valuesilently advances and the next genuine user interaction may see no delta and skip SFX. Since the current upstream paths all useset_value_programmatically, this is latent rather than active — worth a brief inline note or moving the commit after the interaction guard so user-driven deltas remain authoritative.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/ui/components/volume_slider.gd` around lines 127 - 150, The handler _handle_slider_sfx commits _previous_value too early (before checking interaction and cooldown), which lets non-interactive programmatic changes advance the previous-value and mask the next real user delta; fix by keeping the initial is_equal_approx guard, but move the assignment _previous_value = new_value to after the interaction checks (is_mouse_active / has_focus()) and the SFX cooldown check so you only update _previous_value when you will actually play a sound; leave the _last_sfx_time update and AudioManager.play_sfx(AudioConstants.SFX_SLIDER) as-is after that assignment.
111-119: Debounce timer restarts even when the new value is effectively identical.
_on_value_changedunconditionally callssave_debounce_timer.start()and re-applies the dB write. Whilevalue_changedtypically only fires on real changes, sub-epsilon float jitter (especially from keyboard step rounding or externalvaluewrites that bypassedset_value_no_signal) can still trigger this path and schedule a redundant disk save 0.5 s later. Consider gating both the audio write and the debounce on theis_equal_approxcheck that's currently only used inside_handle_slider_sfx:♻️ Suggested gating
func _on_value_changed(new_value: float) -> void: + if is_equal_approx(new_value, _previous_value): + return + AudioServer.set_bus_volume_db(bus_index, linear_to_db(new_value)) AudioManager.set_volume(bus_name, new_value) # Attempt to play interaction feedback _handle_slider_sfx(new_value) # Godot automatically restarts an active timer when start() is called save_debounce_timer.start()Note:
_handle_slider_sfxwould then no longer need its ownis_equal_approxguard.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/ui/components/volume_slider.gd` around lines 111 - 119, The handler _on_value_changed currently always applies AudioServer.set_bus_volume_db and AudioManager.set_volume and always starts save_debounce_timer even for sub-epsilon jitter; add a stored last_applied_value (e.g., a class-level float) and at the top of _on_value_changed use is_equal_approx(new_value, last_applied_value) to early-return when effectively identical, otherwise update AudioServer.set_bus_volume_db(bus_index, linear_to_db(new_value)) and AudioManager.set_volume(bus_name, new_value), call _handle_slider_sfx(new_value) (remove its internal is_equal_approx guard), start save_debounce_timer, and set last_applied_value = new_value so redundant disk saves and audio writes are avoided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/ui/components/volume_slider.gd`:
- Around line 76-80: The docstring for set_value_safely(new_value) incorrectly
claims the input is 0.0–1.0 while the function actually uses the slider's
min_value/max_value; fix by clamping new_value to the slider's configured range
before applying it (use min_value and max_value) and update the docstring to
reflect that callers should pass a value in the slider's configured range (or
document you will clamp to min_value/max_value). Ensure you reference new_value,
min_value, max_value and the set_value_safely method in the change.
- Around line 81-94: set_value_programmatically currently uses the raw new_value
for set_value_no_signal, AudioServer.set_bus_volume_db, AudioManager.set_volume,
and _previous_value, so out-of-range inputs can desync UI and audio; clamp
new_value to the slider bounds (use the control's min_value and max_value) into
a local clamped_value before calling set_value_no_signal(clamped_value),
AudioServer.set_bus_volume_db(bus_index, linear_to_db(clamped_value)),
AudioManager.set_volume(bus_name, clamped_value), and assigning _previous_value
= clamped_value; keep the early guard for bus_index == -1 unchanged.
- Around line 102-104: The _is_dragging flag set in _on_gui_input can get stuck
if the release event is missed; update the control to also clear _is_dragging
when the control loses focus or the window loses focus by handling
NOTIFICATION_FOCUS_EXIT and NOTIFICATION_WM_WINDOW_FOCUS_OUT (implement or
extend _notification to set _is_dragging = false for those notifications), and
avoid using NOTIFICATION_MOUSE_EXIT so out-of-bounds dragging still works; keep
_on_gui_input logic unchanged except relying on these additional notifications
to defensively reset dragging state.
---
Nitpick comments:
In `@scripts/ui/components/volume_slider.gd`:
- Around line 127-150: The handler _handle_slider_sfx commits _previous_value
too early (before checking interaction and cooldown), which lets non-interactive
programmatic changes advance the previous-value and mask the next real user
delta; fix by keeping the initial is_equal_approx guard, but move the assignment
_previous_value = new_value to after the interaction checks (is_mouse_active /
has_focus()) and the SFX cooldown check so you only update _previous_value when
you will actually play a sound; leave the _last_sfx_time update and
AudioManager.play_sfx(AudioConstants.SFX_SLIDER) as-is after that assignment.
- Around line 111-119: The handler _on_value_changed currently always applies
AudioServer.set_bus_volume_db and AudioManager.set_volume and always starts
save_debounce_timer even for sub-epsilon jitter; add a stored last_applied_value
(e.g., a class-level float) and at the top of _on_value_changed use
is_equal_approx(new_value, last_applied_value) to early-return when effectively
identical, otherwise update AudioServer.set_bus_volume_db(bus_index,
linear_to_db(new_value)) and AudioManager.set_volume(bus_name, new_value), call
_handle_slider_sfx(new_value) (remove its internal is_equal_approx guard), start
save_debounce_timer, and set last_applied_value = new_value so redundant disk
saves and audio writes are avoided.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d19ea11d-e0ab-432a-90e9-485b865ebe97
📒 Files selected for processing (2)
scripts/ui/components/volume_slider.gdtest/gut/test_volume_slider.gd
🚧 Files skipped from review as they are similar to previous changes (1)
- test/gut/test_volume_slider.gd
📜 Review details
🔇 Additional comments (1)
scripts/ui/components/volume_slider.gd (1)
40-73: LGTM on_ready()hardening.Bus validation, early disablement (
editable/mouse_filter/focus_mode), seeding_previous_valuewith the linearized initial value, and using thegui_inputsignal instead of overriding_gui_inputtogether close the previously-flagged issues cleanly.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Drag state may get stuck if release event is missed. _is_dragging is only cleared on a MOUSE_BUTTON_LEFT release routed through this control's gui_input. Edge cases (focus loss, popups stealing input, window focus loss mid-drag, or touch event ending without a corresponding mouse release) can leave _is_dragging = true, silently re-enabling SFX on subsequent programmatic value_changed emissions. Clear _is_dragging on NOTIFICATION_FOCUS_EXIT and NOTIFICATION_WM_WINDOW_FOCUS_OUT. Do not use NOTIFICATION_MOUSE_EXIT—it fires when the cursor leaves the slider's bounds during a legitimate drag, which would break out-of-bounds dragging.
…or missing SFX by caching failures suggestion (performance): Avoid repeated warnings and load attempts for missing SFX by caching failures Right now, a missing/failed SFX will trigger a load() and a warning on every play_sfx call with that name. To avoid repeated failed loads and log spam, cache a sentinel value (e.g., null or a dedicated marker object) in _sfx_cache for missing assets so subsequent calls can immediately return without reloading or logging. Alternatively, track missing IDs in a separate Set and short‑circuit when encountered.
Please standardize spelling across the README to avoid mixed variants in project docs (Line 157 currently uses centralised).
…kage. 18-43: Restore AudioManager state after each test to prevent cross-suite leakage. AudioManager is an autoload singleton, so mutations to current_config_path, master_volume, and sfx_volume here persist after this suite finishes. A later suite that calls AudioManager.save_volumes() could end up writing to user://test_audio_sync.cfg (which this suite deletes in after_each), or reading volumes this suite left at 1.0. Sourcery AI caught a critical testing anti-pattern here. Because AudioManager is an autoloaded singleton, its state persists across the entire test suite run. If this specific test suite changes the config path or volume levels and fails to put them back, any test that runs after this one is going to inherit that dirty state, leading to flaky, order-dependent failures that are incredibly frustrating to debug.
107-120: Rate-limit test can be flaky under load. Guard 3 uses Time.get_ticks_msec() - _last_sfx_time < SFX_COOLDOWN_MS (60 ms). Setting _last_sfx_time = current_time just before _handle_slider_sfx(0.6) relies on the gap between those two lines staying under 60 ms. On a loaded CI runner (GC pause, scheduling delay) the gap can exceed 60 ms, causing the guard to pass and the assertion on line 120 to fail. A more deterministic formulation: set _last_sfx_time = Time.get_ticks_msec() + SFX_COOLDOWN_MS (or a large future value) so the cooldown is guaranteed to be in the future regardless of execution speed. This is a spot-on catch by Sourcery AI. Time-based tests are notoriously flaky in CI/CD pipelines (like GitHub Actions) because virtual machines often experience CPU scheduling delays, garbage collection pauses, or hypervisor throttling. If the runner stalls for even 61 milliseconds between your variable declaration and the function call, the 60ms cooldown window will expire, the guard will allow the sound to play, the timestamp will update, and the test will fail randomly. By intentionally setting the _last_sfx_time to a timestamp in the future, the delta calculation inside the slider (current_time - _last_sfx_time) results in a negative number. Since a negative number is always less than the SFX_COOLDOWN_MS, the guard becomes 100% deterministic and mathematically impossible to bypass, regardless of how slow the CI runner is.
13-52: Test mutates live AudioManager / audio server state without isolation. _slider.value = 0.8 on line 47 fires _on_value_changed, which writes to AudioServer.set_bus_volume_db and AudioManager.set_volume (mutating the real autoload), and also starts the real save_debounce_timer (0.5 s). In a slow CI run the timer can fire before add_child_autoqfree reaps the slider, triggering AudioManager.save_volumes() → a write to the real user:// config (this suite, unlike test_audio_sync_decoupling.gd, doesn't override AudioManager.current_config_path). At minimum: Override AudioManager.current_config_path to a test path in before_each and clean it up in after_each. Snapshot & restore AudioManager.master_volume around the test. This is another excellent catch by Sourcery AI. Because the AudioManager is a global Autoload, any test that touches it is essentially touching a live wire. In test_manual_value_change_starts_debounce_timer, you simulate a user dragging the slider by setting _slider.value = 0.8. This triggers the actual _on_value_changed signal, which updates the live AudioManager and starts the 0.5-second save timer. If a CI runner lags and takes more than 0.5 seconds to clean up the test, that timer will time out and execute AudioManager.save_volumes(). Without isolation, it would overwrite your game's real user:// configuration file with the dummy test values! To fix this, we need to apply the exact same snapshot-and-restore pattern here that we used in the decoupling suite.
…ack. 93-104: test_sfx_guard_allows_valid_interaction will trigger real audio playback. Once all three guards pass, _handle_slider_sfx calls AudioManager.play_sfx(AudioConstants.SFX_SLIDER), which loads slider.wav from disk and spawns a real AudioStreamPlayer on the autoload. That's audible during local test runs and adds a transient node to AudioManager. Consider stubbing the call (partial double on AudioManager) or asserting on an observable side-effect (e.g., a signal) instead, so the test proves the guard logic without I/O.
41-43: Duplicate section header. Line 41 repeats the exact same # --- NEW: SFX CACHING & MANAGEMENT --- banner already placed at line 15. Since both declarations belong to the same logical group but are split by the @export_category blocks, consider either consolidating them (move _sfx_cache next to the constants) or differentiating the headers (e.g., # --- SFX CACHE STATE ---) so readers aren't confused by two identical "NEW" sections.
408-410: Connect finished before calling play() for robustness. Connecting the signal after play() risks a race condition and reads as reversed intent. Swap the order and use CONNECT_ONE_SHOT to make the single-fire cleanup explicit. This is a classic event-driven programming catch. Sourcery AI is absolutely correct.Connecting a signal after triggering the action that emits it creates a tiny, but real, race condition window. If an audio file is extremely short, or if the engine experiences a heavy thread stutter, the finished signal could theoretically fire before the connect line executes. If that happens, the AudioStreamPlayer will never be freed, resulting in a persistent memory leak (an orphaned node).Adding CONNECT_ONE_SHOT is also an excellent best practice here—it explicitly tells Godot's engine that this connection is meant to fire exactly once and then automatically disconnect itself, keeping the signal registry perfectly clean.
Working on it... |
The new VolumeSlider tests reach into several internal members and helper methods (e.g., _is_dragging, _previous_value, _handle_slider_sfx); consider adding small public/query helpers or refactoring the logic behind a non-underscored method so tests rely less on private implementation details and are more resilient to refactors. In AudioManager.play_sfx, you create a new AudioStreamPlayer node on every call and only rely on the finished signal for cleanup; if slider or UI sounds can be triggered frequently, consider pooling or reusing a small number of players to avoid excessive node churn and signal connections at runtime.
Working on this one now... |
In AudioManager.play_sfx, you create a new AudioStreamPlayer node on every call and only rely on the finished signal for cleanup; if slider or UI sounds can be triggered frequently, consider pooling or reusing a small number of players to avoid excessive node churn and signal connections at runtime.
…guards 127-150: Guard ordering: _previous_value is committed before interaction/rate guards. The early commit at Line 133 (before the interaction and cooldown checks) is intentional per the comment, but it has a subtle consequence: if a non-interactive value_changed slips through (e.g., a future code path writes to value directly without set_value_no_signal), _previous_value silently advances and the next genuine user interaction may see no delta and skip SFX. Since the current upstream paths all use set_value_programmatically, this is latent rather than active — worth a brief inline note or moving the commit after the interaction guard so user-driven deltas remain authoritative.
…cal. 111-119: Debounce timer restarts even when the new value is effectively identical. _on_value_changed unconditionally calls save_debounce_timer.start() and re-applies the dB write. While value_changed typically only fires on real changes, sub-epsilon float jitter (especially from keyboard step rounding or external value writes that bypassed set_value_no_signal) can still trigger this path and schedule a redundant disk save 0.5 s later. Consider gating both the audio write and the debounce on the is_equal_approx check that's currently only used inside _handle_slider_sfx
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- In
test_volume_slider.gd(GUT suite),test_sfx_guard_allows_valid_interactioncontains a second, unrelated block after the cleanup that reinitializes_sliderstate and calls_handle_slider_sfx; this looks like a copy-paste from the identical-value test and should either be split into a separate test or removed to keep the test’s intent clear and focused. - In
VolumeSlider.set_value_programmatically, you always propagate the clamped value toAudioServerandAudioManagereven if it hasn’t changed; adding a short-circuit whenis_equal_approx(clamped_value, _previous_value)is true would avoid redundant backend calls and keep behavior consistent with_on_value_changed’s duplicate-guard.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `test_volume_slider.gd` (GUT suite), `test_sfx_guard_allows_valid_interaction` contains a second, unrelated block after the cleanup that reinitializes `_slider` state and calls `_handle_slider_sfx`; this looks like a copy-paste from the identical-value test and should either be split into a separate test or removed to keep the test’s intent clear and focused.
- In `VolumeSlider.set_value_programmatically`, you always propagate the clamped value to `AudioServer` and `AudioManager` even if it hasn’t changed; adding a short-circuit when `is_equal_approx(clamped_value, _previous_value)` is true would avoid redundant backend calls and keep behavior consistent with `_on_value_changed`’s duplicate-guard.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
In test_volume_slider.gd (GUT suite), test_sfx_guard_allows_valid_interaction contains a second, unrelated block after the cleanup that reinitializes _slider state and calls _handle_slider_sfx; this looks like a copy-paste from the identical-value test and should either be split into a separate test or removed to keep the test’s intent clear and focused.
In VolumeSlider.set_value_programmatically, you always propagate the clamped value to AudioServer and AudioManager even if it hasn’t changed; adding a short-circuit when is_equal_approx(clamped_value, _previous_value) is true would avoid redundant backend calls and keep behavior consistent with _on_value_changed’s duplicate-guard.
All addressed. |
name: Default Pull Request Template
about: Suggesting changes to SkyLockAssault
title: ''
labels: ''
assignees: ''
PR Summary: Audio Pipeline Performance, UI Safety, and Test Suite Hardening
Bug Fixes & UX Safety
VolumeSlider. If an invalid or typoedbus_nameis detected (bus_index == -1), the component safely aborts, disables the UI (editable = false), and drops mouse/keyboard focus (MOUSE_FILTER_IGNORE,FOCUS_NONE) to prevent silent runtime crashes._notificationlistener inVolumeSliderto catchNOTIFICATION_FOCUS_EXITandNOTIFICATION_WM_WINDOW_FOCUS_OUT. This guarantees the_is_draggingflag safely resets even if the system loses focus (e.g., Alt-Tabbing) before a mouse release event fires._previous_valueassignment inside_handle_slider_sfxto execute after interaction guards. This ensures rogue programmatic updates don't artificially advance the delta tracker and swallow genuine manual user interactions.Performance & Architecture Optimizations
AudioManagerto instantiate a reusable pool ofAudioStreamPlayernodes (default size 8) on_ready. This completely eliminates the severe CPU and memory fragmentation churn caused by constantly calling.new()andqueue_free()on rapid slider drags._missing_sfx_cacheinAudioManager. If an audio file fails to load once, the system flags it and short-circuits future requests, preventing massive disk I/O spikes and log spam.is_equal_approxchecks at the very top of both_on_value_changedandset_value_programmatically. This prevents redundantAudioServerbackend calculations and stops the debounced disk-save timer from spinning up due to micro-jitters from controllers or float rounding.get_previous_value(),get_last_sfx_time(),is_user_dragging()) toVolumeSliderto allow the test suite to validate logic without directly accessing private, underscored variables.Test Suite Hardening (GUT & GdUnit4)
AudioManagersingleton acrossbefore_eachandafter_eachlifecycle methods. This isolates test environments and prevents strayuser://disk writes during CI runs._last_sfx_timeartificially into the future, guaranteeing the cooldown guard triggers mathematically regardless of thread pauses or garbage collection spikes.MockAudioManagerclass. This allows the suite to verify interaction logic and signal paths instantly without touching the disk or generating audio noise.class_nameregistry parsing bugs in the GdUnit4 runner.Description
What does this PR do? (e.g., "Fixes player jump physics in level 2" or "Adds
new enemy AI script")
Related Issue
Closes #ISSUE_NUMBER (if applicable)
Changes
system")
Testing
works on Win10 with 60 FPS")
Checklist
Additional Notes
Anything else? (e.g., "Tested on Win10 64-bit; needs Linux validation")
Summary by Sourcery
Add a dedicated, rate-limited slider sound effect that plays only for genuine user-driven volume adjustments, while hardening the audio/UI sync pipeline against feedback loops and noisy saves.
New Features:
Bug Fixes:
Enhancements:
Tests:
Summary by Sourcery
Add a dedicated, rate-limited slider sound effect and central SFX playback API while hardening audio/UI synchronization to avoid feedback loops and noisy saves.
New Features:
Bug Fixes:
Enhancements:
Tests:
Summary by CodeRabbit
New Features
Improvements
Documentation
Tests
Here is a summary of @ikostan’s contributions as the author and primary developer of PR #578:
Core Feature Implementation
slider.wavsound effect that triggers exclusively during genuine, manual user interactions (e.g., active mouse dragging or keyboard focus), completely silencing redundant audio during programmatic UI syncs.Architecture & Performance Optimization (
AudioManager)play_sfxmethod to handle all non-positional UI audio routing safely.AudioStreamPlayernodes, drastically reducing node instantiation churn, CPU overhead, and memory fragmentation.UI Component Hardening (
VolumeSlider)set_value_programmaticallyAPI to safely update slider visuals and backend audio servers without emitting thevalue_changedsignal, breaking potential audio feedback loops.Test-Driven Reliability (GUT Framework)
VolumeSlider.AudioManagersingleton duringbefore_eachandafter_eachtest phases to guarantee zero cross-suite state leakage.MockAudioManager) to intercept and assert audio calls during testing, verifying complex state logic without triggering actual sound playback or disk reads.Summary of the automated and AI-driven contributions to PR #578:
@sourcery-ai Generated the primary PR summary, detailing the new features (dedicated slider sound effect, AudioManager.play_sfx API), bug fixes (audio spam rate-limiting, fixing feedback loops), and added GUT test coverage.
Provided architectural feedback, reminding the team to verify if .import files should be source-controlled and to standardize the SFX folder structure (files/sounds/sfx/).
Identified a copy-paste error in the test_volume_slider.gd suite and suggested adding an early-return short-circuit in VolumeSlider.set_value_programmatically to prevent redundant backend calls.
@coderabbitai
@deepsource-io