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 }