lib/sys/src/apple/gamecontroller.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 const apple = @import("root.zig");
  4 
  5 const objc = apple.objc;
  6 
  7 pub const supported = builtin.os.tag == .macos;
  8 
  9 pub const Button = enum {
 10     a,
 11     b,
 12     x,
 13     y,
 14     menu,
 15     options,
 16     home,
 17     left_shoulder,
 18     right_shoulder,
 19     left_thumbstick,
 20     right_thumbstick,
 21     dpad_up,
 22     dpad_down,
 23     dpad_left,
 24     dpad_right,
 25 };
 26 
 27 pub const Axis = enum {
 28     left_thumbstick_x,
 29     left_thumbstick_y,
 30     right_thumbstick_x,
 31     right_thumbstick_y,
 32     left_trigger,
 33     right_trigger,
 34 };
 35 
 36 pub const Controller = struct {
 37     object: objc.Id,
 38 
 39     pub fn retain(self: Controller) Controller {
 40         return .{ .object = objc.retain(self.object) };
 41     }
 42 
 43     pub fn release(self: Controller) void {
 44         objc.release(self.object);
 45     }
 46 
 47     pub fn eql(self: Controller, other: Controller) bool {
 48         return self.object == other.object;
 49     }
 50 };
 51 
 52 pub const Profile = struct {
 53     object: objc.Id,
 54 };
 55 
 56 pub const ControllerList = struct {
 57     autorelease_pool: objc.AutoreleasePool,
 58     array: objc.Id,
 59     count: usize,
 60     object_at_index: objc.SEL,
 61 
 62     pub fn deinit(self: *ControllerList) void {
 63         objc.release(self.array);
 64         self.autorelease_pool.deinit();
 65         self.* = undefined;
 66     }
 67 
 68     pub fn at(self: *const ControllerList, index: usize) Controller {
 69         std.debug.assert(index < self.count);
 70         return .{ .object = objc.send(
 71             objc.Id,
 72             self.array,
 73             self.object_at_index,
 74             .{index},
 75         ) };
 76     }
 77 };
 78 
 79 const Selectors = struct {
 80     controllers: objc.SEL,
 81     controller_with_extended_gamepad: objc.SEL,
 82     count: objc.SEL,
 83     object_at_index: objc.SEL,
 84     extended_gamepad: objc.SEL,
 85     vendor_name: objc.SEL,
 86     utf8_string: objc.SEL,
 87     responds_to_selector: objc.SEL,
 88     is_pressed: objc.SEL,
 89     value: objc.SEL,
 90     set_value: objc.SEL,
 91     set_value_for_x_axis_y_axis: objc.SEL,
 92     button_a: objc.SEL,
 93     button_b: objc.SEL,
 94     button_x: objc.SEL,
 95     button_y: objc.SEL,
 96     button_menu: objc.SEL,
 97     button_options: objc.SEL,
 98     button_home: objc.SEL,
 99     left_shoulder: objc.SEL,
100     right_shoulder: objc.SEL,
101     left_trigger: objc.SEL,
102     right_trigger: objc.SEL,
103     left_thumbstick: objc.SEL,
104     right_thumbstick: objc.SEL,
105     left_thumbstick_button: objc.SEL,
106     right_thumbstick_button: objc.SEL,
107     dpad: objc.SEL,
108     x_axis: objc.SEL,
109     y_axis: objc.SEL,
110     up: objc.SEL,
111     down: objc.SEL,
112     left: objc.SEL,
113     right: objc.SEL,
114 
115     fn init() Selectors {
116         return .{
117             .controllers = objc.selector("controllers"),
118             .controller_with_extended_gamepad = objc.selector("controllerWithExtendedGamepad"),
119             .count = objc.selector("count"),
120             .object_at_index = objc.selector("objectAtIndex:"),
121             .extended_gamepad = objc.selector("extendedGamepad"),
122             .vendor_name = objc.selector("vendorName"),
123             .utf8_string = objc.selector("UTF8String"),
124             .responds_to_selector = objc.selector("respondsToSelector:"),
125             .is_pressed = objc.selector("isPressed"),
126             .value = objc.selector("value"),
127             .set_value = objc.selector("setValue:"),
128             .set_value_for_x_axis_y_axis = objc.selector("setValueForXAxis:yAxis:"),
129             .button_a = objc.selector("buttonA"),
130             .button_b = objc.selector("buttonB"),
131             .button_x = objc.selector("buttonX"),
132             .button_y = objc.selector("buttonY"),
133             .button_menu = objc.selector("buttonMenu"),
134             .button_options = objc.selector("buttonOptions"),
135             .button_home = objc.selector("buttonHome"),
136             .left_shoulder = objc.selector("leftShoulder"),
137             .right_shoulder = objc.selector("rightShoulder"),
138             .left_trigger = objc.selector("leftTrigger"),
139             .right_trigger = objc.selector("rightTrigger"),
140             .left_thumbstick = objc.selector("leftThumbstick"),
141             .right_thumbstick = objc.selector("rightThumbstick"),
142             .left_thumbstick_button = objc.selector("leftThumbstickButton"),
143             .right_thumbstick_button = objc.selector("rightThumbstickButton"),
144             .dpad = objc.selector("dpad"),
145             .x_axis = objc.selector("xAxis"),
146             .y_axis = objc.selector("yAxis"),
147             .up = objc.selector("up"),
148             .down = objc.selector("down"),
149             .left = objc.selector("left"),
150             .right = objc.selector("right"),
151         };
152     }
153 };
154 
155 pub const Runtime = struct {
156     controller_class: objc.Id,
157     selectors: Selectors,
158 
159     pub fn init() ?Runtime {
160         if (comptime !supported) return null;
161         return .{
162             .controller_class = objc.class("GCController") orelse return null,
163             .selectors = .init(),
164         };
165     }
166 
167     pub fn controllers(self: *const Runtime) ControllerList {
168         const autorelease_pool = objc.AutoreleasePool.init();
169         const borrowed = objc.send(
170             objc.Id,
171             self.controller_class,
172             self.selectors.controllers,
173             .{},
174         );
175         const array = objc.retain(borrowed);
176         return .{
177             .autorelease_pool = autorelease_pool,
178             .array = array,
179             .count = objc.send(usize, array, self.selectors.count, .{}),
180             .object_at_index = self.selectors.object_at_index,
181         };
182     }
183 
184     pub fn extendedGamepad(self: *const Runtime, controller: Controller) ?Profile {
185         const object = objc.send(
186             ?objc.Id,
187             controller.object,
188             self.selectors.extended_gamepad,
189             .{},
190         ) orelse return null;
191         return .{ .object = object };
192     }
193 
194     pub fn controllerName(
195         self: *const Runtime,
196         controller: Controller,
197     ) ?[*:0]const u8 {
198         const string = objc.send(
199             ?objc.Id,
200             controller.object,
201             self.selectors.vendor_name,
202             .{},
203         ) orelse return null;
204         return objc.send(?[*:0]const u8, string, self.selectors.utf8_string, .{});
205     }
206 
207     pub fn buttonPressed(
208         self: *const Runtime,
209         profile: Profile,
210         button: Button,
211     ) bool {
212         const object = self.buttonObject(profile, button) orelse return false;
213         return objc.isTrue(objc.send(objc.BOOL, object, self.selectors.is_pressed, .{}));
214     }
215 
216     pub fn axisValue(self: *const Runtime, profile: Profile, axis: Axis) f32 {
217         const object = self.axisObject(profile, axis) orelse return 0;
218         return objc.send(f32, object, self.selectors.value, .{});
219     }
220 
221     pub fn createRetainedSnapshot(self: *const Runtime) ?Controller {
222         if (!self.responds(
223             self.controller_class,
224             self.selectors.controller_with_extended_gamepad,
225         )) return null;
226         const borrowed = objc.send(
227             ?objc.Id,
228             self.controller_class,
229             self.selectors.controller_with_extended_gamepad,
230             .{},
231         ) orelse return null;
232         return (Controller{ .object = borrowed }).retain();
233     }
234 
235     pub fn setButtonValue(
236         self: *const Runtime,
237         profile: Profile,
238         button: Button,
239         value: f32,
240     ) bool {
241         std.debug.assert(value >= 0);
242         std.debug.assert(value <= 1);
243         const is_direction = switch (button) {
244             .dpad_up, .dpad_down, .dpad_left, .dpad_right => true,
245             else => false,
246         };
247         if (is_direction) return self.setDirectionButtonValue(profile, button, value);
248         const object = self.buttonObject(profile, button) orelse return false;
249         if (!self.responds(object, self.selectors.set_value)) return false;
250         objc.send(void, object, self.selectors.set_value, .{value});
251         return true;
252     }
253 
254     pub fn setAxisValue(
255         self: *const Runtime,
256         profile: Profile,
257         axis: Axis,
258         value: f32,
259     ) bool {
260         std.debug.assert(value >= -1);
261         std.debug.assert(value <= 1);
262         if (axis == .left_trigger or axis == .right_trigger) {
263             std.debug.assert(value >= 0);
264         }
265         const object = self.axisObject(profile, axis) orelse return false;
266         if (!self.responds(object, self.selectors.set_value)) return false;
267         objc.send(void, object, self.selectors.set_value, .{value});
268         return true;
269     }
270 
271     pub fn setDirectionPadValue(
272         self: *const Runtime,
273         profile: Profile,
274         x: f32,
275         y: f32,
276     ) bool {
277         std.debug.assert(x >= -1);
278         std.debug.assert(x <= 1);
279         std.debug.assert(y >= -1);
280         std.debug.assert(y <= 1);
281         const dpad = objc.send(?objc.Id, profile.object, self.selectors.dpad, .{}) orelse
282             return false;
283         if (!self.responds(dpad, self.selectors.set_value_for_x_axis_y_axis)) return false;
284         objc.send(
285             void,
286             dpad,
287             self.selectors.set_value_for_x_axis_y_axis,
288             .{ x, y },
289         );
290         return true;
291     }
292 
293     fn setDirectionButtonValue(
294         self: *const Runtime,
295         profile: Profile,
296         button: Button,
297         value: f32,
298     ) bool {
299         const dpad = objc.send(?objc.Id, profile.object, self.selectors.dpad, .{}) orelse
300             return false;
301         const x_axis = objc.send(?objc.Id, dpad, self.selectors.x_axis, .{}) orelse return false;
302         const y_axis = objc.send(?objc.Id, dpad, self.selectors.y_axis, .{}) orelse return false;
303         var x = objc.send(f32, x_axis, self.selectors.value, .{});
304         var y = objc.send(f32, y_axis, self.selectors.value, .{});
305         switch (button) {
306             .dpad_up => if (y > 0 or value > 0) {
307                 y = value;
308             },
309             .dpad_down => if (y < 0 or value > 0) {
310                 y = -value;
311             },
312             .dpad_left => if (x < 0 or value > 0) {
313                 x = -value;
314             },
315             .dpad_right => if (x > 0 or value > 0) {
316                 x = value;
317             },
318             else => unreachable,
319         }
320         return self.setDirectionPadValue(profile, x, y);
321     }
322 
323     fn buttonObject(
324         self: *const Runtime,
325         profile: Profile,
326         button: Button,
327     ) ?objc.Id {
328         return switch (button) {
329             .dpad_up => self.directionButton(profile, self.selectors.up),
330             .dpad_down => self.directionButton(profile, self.selectors.down),
331             .dpad_left => self.directionButton(profile, self.selectors.left),
332             .dpad_right => self.directionButton(profile, self.selectors.right),
333             else => self.directButton(profile, button),
334         };
335     }
336 
337     fn directButton(
338         self: *const Runtime,
339         profile: Profile,
340         button: Button,
341     ) ?objc.Id {
342         const selector = self.directButtonSelector(button) orelse return null;
343         if (isOptionalButton(button) and !self.responds(profile.object, selector)) return null;
344         return objc.send(?objc.Id, profile.object, selector, .{});
345     }
346 
347     fn directionButton(
348         self: *const Runtime,
349         profile: Profile,
350         selector: objc.SEL,
351     ) ?objc.Id {
352         const dpad = objc.send(?objc.Id, profile.object, self.selectors.dpad, .{}) orelse
353             return null;
354         return objc.send(?objc.Id, dpad, selector, .{});
355     }
356 
357     fn axisObject(self: *const Runtime, profile: Profile, axis: Axis) ?objc.Id {
358         const direct = switch (axis) {
359             .left_trigger => self.selectors.left_trigger,
360             .right_trigger => self.selectors.right_trigger,
361             else => null,
362         };
363         if (direct) |selector| return objc.send(?objc.Id, profile.object, selector, .{});
364         const stick_selector = switch (axis) {
365             .left_thumbstick_x, .left_thumbstick_y => self.selectors.left_thumbstick,
366             .right_thumbstick_x, .right_thumbstick_y => self.selectors.right_thumbstick,
367             else => unreachable,
368         };
369         const stick = objc.send(?objc.Id, profile.object, stick_selector, .{}) orelse return null;
370         const axis_selector = switch (axis) {
371             .left_thumbstick_x, .right_thumbstick_x => self.selectors.x_axis,
372             .left_thumbstick_y, .right_thumbstick_y => self.selectors.y_axis,
373             else => unreachable,
374         };
375         return objc.send(?objc.Id, stick, axis_selector, .{});
376     }
377 
378     fn directButtonSelector(self: *const Runtime, button: Button) ?objc.SEL {
379         return switch (button) {
380             .a => self.selectors.button_a,
381             .b => self.selectors.button_b,
382             .x => self.selectors.button_x,
383             .y => self.selectors.button_y,
384             .menu => self.selectors.button_menu,
385             .options => self.selectors.button_options,
386             .home => self.selectors.button_home,
387             .left_shoulder => self.selectors.left_shoulder,
388             .right_shoulder => self.selectors.right_shoulder,
389             .left_thumbstick => self.selectors.left_thumbstick_button,
390             .right_thumbstick => self.selectors.right_thumbstick_button,
391             .dpad_up, .dpad_down, .dpad_left, .dpad_right => null,
392         };
393     }
394 
395     fn responds(self: *const Runtime, object: objc.Id, selector: objc.SEL) bool {
396         return objc.isTrue(objc.send(
397             objc.BOOL,
398             object,
399             self.selectors.responds_to_selector,
400             .{selector},
401         ));
402     }
403 };
404 
405 fn isOptionalButton(button: Button) bool {
406     return switch (button) {
407         .menu, .options, .home, .left_thumbstick, .right_thumbstick => true,
408         else => false,
409     };
410 }
411 
412 test "GameController snapshot round-trips raw extended inputs" {
413     if (comptime !supported) return error.SkipZigTest;
414 
415     const runtime = Runtime.init().?;
416     const controller = runtime.createRetainedSnapshot().?;
417     defer controller.release();
418     const profile = runtime.extendedGamepad(controller).?;
419 
420     try std.testing.expect(runtime.setButtonValue(profile, .a, 1));
421     try std.testing.expect(runtime.buttonPressed(profile, .a));
422     try std.testing.expect(runtime.setDirectionPadValue(profile, 0, 1));
423     try std.testing.expect(runtime.buttonPressed(profile, .dpad_up));
424     try std.testing.expect(runtime.setAxisValue(profile, .left_thumbstick_x, 0.75));
425     try std.testing.expectApproxEqAbs(
426         @as(f32, 0.75),
427         runtime.axisValue(profile, .left_thumbstick_x),
428         0.001,
429     );
430     try std.testing.expect(runtime.setAxisValue(profile, .right_trigger, 0.5));
431     try std.testing.expectApproxEqAbs(
432         @as(f32, 0.5),
433         runtime.axisValue(profile, .right_trigger),
434         0.001,
435     );
436 }
437 
438 test "GameController connected list has valid borrowed entries" {
439     if (comptime !supported) return error.SkipZigTest;
440 
441     const runtime = Runtime.init().?;
442     for (0..64) |_| {
443         var controllers = runtime.controllers();
444         defer controllers.deinit();
445         for (0..controllers.count) |index| {
446             try std.testing.expect(controllers.at(index).object != objc.nil);
447         }
448     }
449 }