Skip to content

Commit 6a1a71b

Browse files
committed
Release 0.3.8 clutch tuning fix
Clarify the advanced clutch shift controls after tester feedback: clutch bite, clean shift, missed clutch, and the new clutch unload percentage. Persist and clamp clutch_body_cut in the typed Forza shift tuning config, use it in drivetrain body rumble, and add regression coverage for the rumble reduction path. Bump package metadata and release notes to 0.3.8.
1 parent a48b1ca commit 6a1a71b

25 files changed

Lines changed: 185 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,50 @@
1+
# DualSense Command Center 0.3.8
2+
3+
Release date: 2026-06-01
4+
5+
0.3.8 is a clutch-tuning fix release. It keeps the 0.3.7 shift behavior, but
6+
makes the advanced controls easier to understand and easier to edit.
7+
8+
## Clutch Control Fixes
9+
10+
- `Clutch at %` is now `Clutch bite %`, which means the clutch press threshold.
11+
- `With clutch` is now `Clean shift`.
12+
- `No clutch` is now `Missed clutch`.
13+
- New `Clutch unload %` control adjusts how much DSCC-generated drivetrain body
14+
rumble drops while the clutch is pressed.
15+
- Number fields select their current value on focus, so users can type a new
16+
value without fighting the existing digit.
17+
18+
## Runtime Safety
19+
20+
- The clutch unload value is stored in the typed Forza shift tuning config.
21+
- Saved 0.3.7 profiles load safely with the new default value.
22+
- The backend clamps the new setting before use.
23+
- A regression test now proves the clutch unload setting changes drivetrain body
24+
rumble.
25+
26+
## Thanks
27+
28+
- Thanks to **ゾアン・ファンイ** for testing the clutch build quickly and pointing out
29+
where the labels and number fields were getting in the way.
30+
31+
## Validation Gate
32+
33+
This release passed:
34+
35+
```powershell
36+
cargo +stable-x86_64-pc-windows-gnu fmt --all -- --check
37+
cargo +stable-x86_64-pc-windows-gnu test -p dscc-agent
38+
cargo +stable-x86_64-pc-windows-gnu clippy -p dscc-agent --all-targets --target x86_64-pc-windows-gnu -- -D warnings
39+
npm.cmd --prefix web run typecheck
40+
npm.cmd --prefix web run build
41+
npm.cmd --prefix web run test:haptics-graph
42+
npm.cmd --prefix web run test:button-map
43+
npm.cmd --prefix web run test:release-size
44+
npm.cmd --prefix web run test:source-audit
45+
npm.cmd --prefix web run test:visual-smoke
46+
```
47+
148
# DualSense Command Center 0.3.7
249

350
Release date: 2026-06-01

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ lightbar controls, and racing telemetry without Python setup or scripts.
2121

2222
Get the latest Windows installer from [GitHub Releases](https://github.com/shiftedx/dualsense-command/releases/latest).
2323

24-
- Current release: `0.3.7`
24+
- Current release: `0.3.8`
2525
- Recommended download: **DSCC Standard** Windows x86_64 MSI
2626
- Linux builds: beta archive with bundled web UI
2727

crates/dscc-adapters/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dscc-adapters"
3-
version = "0.3.7"
3+
version = "0.3.8"
44
edition.workspace = true
55
license.workspace = true
66
rust-version.workspace = true

crates/dscc-agent/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dscc-agent"
3-
version = "0.3.7"
3+
version = "0.3.8"
44
edition.workspace = true
55
license.workspace = true
66
rust-version.workspace = true

crates/dscc-agent/src/agent_types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,8 @@ pub struct ForzaShiftTuningConfig {
656656
pub with_clutch_duration_ms: f64,
657657
#[serde(default = "default_forza_shift_without_clutch_duration_ms")]
658658
pub without_clutch_duration_ms: f64,
659+
#[serde(default = "default_forza_shift_clutch_body_cut")]
660+
pub clutch_body_cut: f64,
659661
}
660662

661663
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]

crates/dscc-agent/src/config_model.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ impl Default for ForzaShiftTuningConfig {
573573
without_clutch_strength: default_forza_shift_without_clutch_strength(),
574574
with_clutch_duration_ms: default_forza_shift_with_clutch_duration_ms(),
575575
without_clutch_duration_ms: default_forza_shift_without_clutch_duration_ms(),
576+
clutch_body_cut: default_forza_shift_clutch_body_cut(),
576577
}
577578
}
578579
}
@@ -635,6 +636,12 @@ impl ForzaShiftTuningConfig {
635636
500.0,
636637
default_forza_shift_without_clutch_duration_ms(),
637638
);
639+
self.clutch_body_cut = finite_clamp(
640+
self.clutch_body_cut,
641+
0.0,
642+
1.0,
643+
default_forza_shift_clutch_body_cut(),
644+
);
638645
self
639646
}
640647
}
@@ -683,6 +690,10 @@ pub(crate) fn default_forza_shift_without_clutch_duration_ms() -> f64 {
683690
FORZA_SHIFT_WITHOUT_CLUTCH_DURATION_MS
684691
}
685692

693+
pub(crate) fn default_forza_shift_clutch_body_cut() -> f64 {
694+
FORZA_SHIFT_CLUTCH_BODY_CUT
695+
}
696+
686697
pub(crate) fn normalize_forza_shift_clutch_mode(mode: &str) -> &'static str {
687698
match mode {
688699
"off" => "off",

crates/dscc-agent/src/effects/output_enhancements.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ pub(crate) fn forza_rumble_output(
131131
} else {
132132
0.0
133133
};
134-
let clutch_uncouple = 1.0 - clutch * 0.78;
134+
let clutch_uncouple = 1.0 - clutch * shift_tuning.clutch_body_cut;
135135
let drivetrain = (rpm * rpm * (0.35 + throttle * 0.65) * clutch_uncouple).clamp(0.0, 1.0);
136136

137137
let mut low = 0.0;

crates/dscc-agent/src/runtime_constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,6 @@ pub(crate) const FORZA_SHIFT_WITH_CLUTCH_STRENGTH: f64 = 0.58;
8888
pub(crate) const FORZA_SHIFT_WITHOUT_CLUTCH_STRENGTH: f64 = 1.0;
8989
pub(crate) const FORZA_SHIFT_WITH_CLUTCH_DURATION_MS: f64 = 130.0;
9090
pub(crate) const FORZA_SHIFT_WITHOUT_CLUTCH_DURATION_MS: f64 = 240.0;
91+
pub(crate) const FORZA_SHIFT_CLUTCH_BODY_CUT: f64 = 0.78;
9192
pub(crate) const FORZA_SUSPENSION_IMPACT_TRIGGER_AT: f64 = 0.42;
9293
pub(crate) const FORZA_SUSPENSION_IMPACT_RESET_AT: f64 = 0.22;

crates/dscc-agent/src/tests/effects/body_effects.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,64 @@ fn forza_surface_rumble_is_suppressed_while_stationary() {
173173
assert!(rumble.high_frequency > 0.25);
174174
}
175175

176+
#[test]
177+
fn forza_clutch_body_cut_controls_drivetrain_rumble() {
178+
let mut forza = ForzaTelemetryConfig::default().normalized();
179+
forza.body_rumble_mode = "dscc_full_control".to_string();
180+
for effect in &mut forza.effects {
181+
effect.enabled = false;
182+
}
183+
let throttle = forza
184+
.effects
185+
.iter_mut()
186+
.find(|effect| effect.id == "throttle_resistance")
187+
.expect("default throttle tuning exists");
188+
throttle.enabled = true;
189+
throttle.intensity = 100;
190+
throttle.route = "body_both".to_string();
191+
192+
let snapshot = |clutch| {
193+
SignalSnapshot::from_updates([
194+
signal_update("input.throttle", 1.0),
195+
signal_update("input.brake", 0.0),
196+
signal_update("input.clutch", clutch),
197+
signal_update("input.handbrake", 0.0),
198+
signal_update("vehicle.rpm_ratio", 0.9),
199+
signal_update("vehicle.speed_kmh", 96.0),
200+
signal_update("wheel.slip.max", 0.0),
201+
signal_update("wheel.slip.front_max", 0.0),
202+
signal_update("wheel.slip.rear_max", 0.0),
203+
signal_update("surface.rumble.max", 0.0),
204+
signal_update("surface.rumble_strip.max", 0.0),
205+
signal_update("surface.puddle.max", 0.0),
206+
signal_update("suspension.travel.max", 0.0),
207+
signal_update("vehicle.acceleration.magnitude", 0.0),
208+
signal_update("drivetrain.shift_pulse", 0.0),
209+
])
210+
};
211+
212+
let coupled =
213+
forza_rumble_output(&forza, &snapshot(0.0), 1.0, "Balanced").expect("throttle rumble");
214+
let uncoupled =
215+
forza_rumble_output(&forza, &snapshot(1.0), 1.0, "Balanced").expect("reduced rumble");
216+
assert!(
217+
uncoupled.low_frequency < coupled.low_frequency * 0.35,
218+
"default clutch body cut should reduce drivetrain rumble, got {} from {}",
219+
uncoupled.low_frequency,
220+
coupled.low_frequency
221+
);
222+
223+
forza.shift.clutch_body_cut = 0.0;
224+
let no_cut =
225+
forza_rumble_output(&forza, &snapshot(1.0), 1.0, "Balanced").expect("no-cut rumble");
226+
assert!(
227+
(no_cut.low_frequency - coupled.low_frequency).abs() < 0.001,
228+
"0% clutch unload should preserve drivetrain rumble, got {} from {}",
229+
no_cut.low_frequency,
230+
coupled.low_frequency
231+
);
232+
}
233+
176234
#[test]
177235
fn forza_suspension_impact_latches_landing_body_thump() {
178236
let mut runtime = test_forza_effect_runtime();

0 commit comments

Comments
 (0)