Show each controller's mapping source and uuid in pad status

A mapping bug report needs to know whether gilrs found an SDL row for a pad
and which id such a row would need. pad status now lists mapping_source
(sdl, driver or none) and uuid (32 hex digits, the id an SDL row starts with).

With splitscreen on, PadRouter holds the gilrs reader, so pad status said
backend:false and listed no controllers, unless the reader was lent to P1.
The router now leaves what it sees for the status of each game that has a
control socket; that list is only built when one is open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
martipopsandClaude Opus 5.5 committed 2026-11-05 11:53:03 -05:00
1 parent 2a013743f2
commit 7ac3021dfb
3 files changed
+87 -20

No files matched your search

+1
View File
@@ -142,6 +142,7 @@ mod hardware {
}
pub(in crate::game) fn lend_output(&mut self, _: &mut Pads, _: PadId) {}
pub(in crate::game) fn reclaim_output(&mut self, _: &mut Pads) {}
pub(in crate::game) fn report_status(&self, _: &mut Pads, _: Option<PadId>) {}
pub(in crate::game) fn connected(&self, _: PadId) -> bool {
false
}
+79 -20
View File
@@ -34,6 +34,51 @@ pub(super) struct Devices {
haptics_tried: bool,
/// What it was last told to show.
shown: Option<horizon_dualsense::device::State>,
/// While `PadRouter` has the reader and isn't lending it: what `pad status` lists.
routed: Option<Vec<Value>>,
}
/// `pad status`'s line for a controller. `uuid` is the id an SDL mapping row starts with, so a
/// report says which row a pad needs; `mapping_source` says whether one was found.
fn controller_status(
id: gilrs::GamepadId,
pad: &gilrs::Gamepad,
active: Option<gilrs::GamepadId>,
) -> Value {
let mapping_source = match pad.mapping_source() {
gilrs::MappingSource::SdlMappings => "sdl",
gilrs::MappingSource::Driver => "driver",
gilrs::MappingSource::None => "none",
};
let uuid: String = pad.uuid().iter().map(|b| format!("{b:02x}")).collect();
json!({
"name": pad.name(),
"vendor": pad.vendor_id(),
"product": pad.product_id(),
"mapping_source": mapping_source,
"uuid": uuid,
"active": active == Some(id),
})
}
impl Devices {
/// The hardware part of `pad status`.
fn status(&self) -> Value {
let pads: Vec<Value> = match (&self.gilrs, &self.routed) {
(Some(g), _) => g
.gamepads()
.map(|(id, p)| controller_status(id, &p, self.active))
.collect(),
(None, Some(routed)) => routed.clone(),
(None, None) => Vec::new(),
};
json!({
"backend": self.gilrs.is_some() || self.routed.is_some(),
"controllers": pads,
"dualsense_output": self.output.as_ref().and_then(|o| o.open()).map(|t| format!("{t:?}")),
"trigger_effects": self.shown.map(|s| s.triggers),
})
}
}
fn gilrs_button(button: Button) -> gilrs::Button {
@@ -249,6 +294,7 @@ impl PadRouter {
/// Routing ends: P1 gets the reader back, open as it was.
pub(in crate::game) fn hand_back(&mut self, pads: &mut Pads) {
self.started = false;
pads.devices.routed = None;
if let Some(gilrs) = self.gilrs.take() {
pads.devices.gilrs = Some(gilrs);
pads.devices.started = true;
@@ -280,6 +326,16 @@ impl PadRouter {
self.gilrs = pads.devices.gilrs.take();
}
/// `pad status` is answered in P1's turn, which can't reach this reader: leave it what the
/// reader sees. `owner`: the pad driving P1.
pub(in crate::game) fn report_status(&self, pads: &mut Pads, owner: Option<PadId>) {
pads.devices.routed = self.gilrs.as_ref().map(|g| {
g.gamepads()
.map(|(id, p)| controller_status(id, &p, owner))
.collect()
});
}
/// The buttons pressed and the controllers gone or back since the last call, in order.
pub(in crate::game) fn events(&mut self) -> Vec<PadEvent> {
if !self.started {
@@ -591,27 +647,8 @@ impl Game<'_> {
}
}
/// The hardware part of `pad status`.
pub(super) fn pad_devices_status(&self) -> Value {
let devices = &self.pad.devices;
let pads: Vec<Value> = devices.gilrs.as_ref().map_or(Vec::new(), |g| {
g.gamepads()
.map(|(id, p)| {
json!({
"name": p.name(),
"vendor": p.vendor_id(),
"product": p.product_id(),
"active": devices.active == Some(id),
})
})
.collect()
});
json!({
"backend": devices.gilrs.is_some(),
"controllers": pads,
"dualsense_output": devices.output.as_ref().and_then(|o| o.open()).map(|t| format!("{t:?}")),
"trigger_effects": devices.shown.map(|s| s.triggers),
})
self.pad.devices.status()
}
}
@@ -646,6 +683,28 @@ mod one_reader_tests {
assert!(pads.devices.started, "so P1 won't open a second");
}
/// While the router has the reader, `pad status` still says controllers can be read.
#[test]
fn status_sees_the_routers_reader_until_it_comes_back() {
let Some(gilrs) = reader() else {
return;
};
let mut pads = Pads::default();
pads.devices.gilrs = Some(gilrs);
pads.devices.started = true;
let mut router = PadRouter::default();
let backend = |pads: &Pads| pads.devices.status()["backend"].as_bool().unwrap();
router.adopt(&mut pads);
assert!(!backend(&pads), "nothing reported yet");
router.report_status(&mut pads, None);
assert!(backend(&pads), "the router's reader counts");
router.hand_back(&mut pads);
assert!(pads.devices.routed.is_none(), "P1 reads its own again");
assert!(backend(&pads));
}
#[test]
fn a_router_with_its_own_reader_leaves_a_lent_ones_owner_alone() {
let (Some(own), Some(other)) = (reader(), reader()) else {
@@ -273,10 +273,17 @@ impl Split<'_> {
self.host.pad.release_hardware();
}
let owner = self.p1_pad.owner;
// Only something on the control socket can ask.
if self.host.control.is_some() {
self.pads.report_status(&mut self.host.pad, owner);
}
let state = self.pad_state(owner);
self.host.pad.feed_from(owner, state);
for i in 0..self.guests.len() {
let pad = self.guests[i].pad;
if self.guests[i].game.control.is_some() {
self.pads.report_status(&mut self.guests[i].game.pad, pad);
}
let state = self.pad_state(pad);
self.guests[i].game.pad.feed_from(pad, state);
}