diff --git a/GNUmakefile b/GNUmakefile index 08c4a3edf..ed8a16331 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -192,6 +192,7 @@ OBJS := $(OBJS) \ src/hid/hid.o \ src/unix/apple/image.o \ src/unix/apple/macosx/hid.o \ + src/unix/apple/macosx/hid_gc.o \ src/unix/apple/macosx/gfx/metal-ctx.o else diff --git a/bang.sh b/bang.sh new file mode 100644 index 000000000..eacff07c5 --- /dev/null +++ b/bang.sh @@ -0,0 +1,4 @@ +make +cd test +clear +make 0-minimal \ No newline at end of file diff --git a/src/hid/hid.c b/src/hid/hid.c index 199f77ba3..f113d0242 100644 --- a/src/hid/hid.c +++ b/src/hid/hid.c @@ -4,7 +4,7 @@ #include "hid.h" #include "utils.h" - +#include "stdio.h" #include #include @@ -88,6 +88,10 @@ void mty_hid_driver_init(struct hid_dev *device) case MTY_CTYPE_XBOX: xbox_init(device); break; + case MTY_CTYPE_XBOXW: + printf("Got to initing MTY_CTYPE_XBOXW\n"); + xboxw_init(device); + break; } } @@ -134,6 +138,10 @@ void mty_hid_driver_rumble(struct hid *hid, uint32_t id, uint16_t low, uint16_t case MTY_CTYPE_XBOX: xbox_rumble(device, low, high); break; + case MTY_CTYPE_XBOXW: + printf("IT IS AN XBOXW!\n"); + xboxw_rumble(device, low, high); + break; case MTY_CTYPE_DEFAULT: mty_hid_default_rumble(hid, id, low, high); break; diff --git a/src/hid/xboxw.h b/src/hid/xboxw.h index fdf2fbcfb..1556d78b0 100644 --- a/src/hid/xboxw.h +++ b/src/hid/xboxw.h @@ -4,6 +4,59 @@ #pragma once +struct xboxw_state { + bool rumble; + uint16_t low; + uint16_t high; +}; + +// Rumble + +static void xboxw_rumble(struct hid_dev *device, uint16_t low, uint16_t high) +{ + struct xboxw_state *ctx = mty_hid_device_get_state(device); + + printf("Got here 1\n"); + // Xbox 360 wired controller rumble packet format + uint8_t rumble_packet[8] = {0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + rumble_packet[3] = low >> 8; // Low frequency motor intensity + rumble_packet[4] = high >> 8; // High frequency motor intensity + printf("Got here 2 - low: %u, high: %u\n", low >> 8, high >> 8); + + mty_hid_device_write(device, rumble_packet, sizeof(rumble_packet)); + printf("Got here 3\n"); + + // Store values for potential retransmission + ctx->low = low; + ctx->high = high; + ctx->rumble = true; +} + +static void xboxw_do_rumble(struct hid_dev *device) +{ + struct xboxw_state *ctx = mty_hid_device_get_state(device); + + if (ctx->rumble) { + uint8_t rumble_packet[8] = {0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + rumble_packet[3] = ctx->low >> 8; + rumble_packet[4] = ctx->high >> 8; + printf("Got here 2\n"); + + mty_hid_device_write(device, rumble_packet, sizeof(rumble_packet)); + printf("Got here 4\n"); + ctx->rumble = false; + } +} + +static void xboxw_init(struct hid_dev *device) +{ + printf("Got to xboxw_init\n"); + struct xboxw_state *ctx = mty_hid_device_get_state(device); + ctx->rumble = false; + ctx->low = 0; + ctx->high = 0; +} + static bool xboxw_state(struct hid_dev *device, const void *data, size_t size, MTY_ControllerEvent *c) { const uint8_t *d = data; @@ -65,6 +118,7 @@ static bool xboxw_state(struct hid_dev *device, const void *data, size_t size, M c->buttons[MTY_CBUTTON_LEFT_TRIGGER] = c->axes[MTY_CAXIS_TRIGGER_L].value > 0; c->buttons[MTY_CBUTTON_RIGHT_TRIGGER] = c->axes[MTY_CAXIS_TRIGGER_R].value > 0; + xboxw_do_rumble(device); return true; } diff --git a/src/unix/apple/macosx/hid.c b/src/unix/apple/macosx/hid.c index cc7af297e..3f9ed675d 100644 --- a/src/unix/apple/macosx/hid.c +++ b/src/unix/apple/macosx/hid.c @@ -3,9 +3,11 @@ // You can obtain one at https://spdx.org/licenses/MIT.html. #include "hid/hid.h" +#include #include #include +#include "hid_gc.h" #define HID_DEV_GET_USAGE(dev) \ hid_device_get_prop_int(dev, CFSTR(kIOHIDPrimaryUsageKey)) @@ -63,6 +65,11 @@ static void hid_device_destroy(void *hdevice) struct hid_dev *ctx = hdevice; + // Clean up GCController rumble context if it exists + if (ctx->device) { + mty_hid_gc_cleanup(ctx->device); + } + MTY_Free(ctx->state); MTY_Free(ctx); } @@ -223,17 +230,44 @@ void mty_hid_destroy(struct hid **hid) void mty_hid_device_write(struct hid_dev *ctx, const void *buf, size_t size) { const uint8_t *buf8 = buf; - + + // Check if this is an Xbox controller rumble packet + if (ctx->vid == 0x045E && size >= 8 && buf8[0] == 0x00 && buf8[1] == 0x08) { + // This is an Xbox rumble packet, use GCController instead + uint8_t low_intensity = buf8[3]; + uint8_t high_intensity = buf8[4]; + // Convert 8-bit (0-255) to 16-bit (0-65535) range + uint16_t low = ((uint16_t)low_intensity << 8) | low_intensity; + uint16_t high = ((uint16_t)high_intensity << 8) | high_intensity; + + printf("Xbox rumble: low=%u, high=%u\n", low, high); + + if (mty_hid_gc_rumble(ctx->device, low, high)) { + return; // Successfully sent rumble via GCController + } + // Fall through to try IOHIDDeviceSetReport if GCController fails + } + + printf("buf8[0]: %d\n", buf8[0]); + printf("size: %zu\n", size); + printf("ctx->device: %p\n", ctx->device); + if (buf8[0] == 0) { buf += 1; size -= 1; } + printf("buf after adjustment: %p\n", buf); + printf("size after adjustment: %zu\n", size); + IOReturn e = IOHIDDeviceSetReport(ctx->device, kIOHIDReportTypeOutput, buf8[0], buf, size); - if (e != kIOReturnSuccess) + printf("Return from IOHIDDeviceSetReport (e): 0x%X\n", e); + + if (e != kIOReturnSuccess) { + printf("'IOHIDDeviceSetReport' failed with error 0x%X\n", e); MTY_Log("'IOHIDDeviceSetReport' failed with error 0x%X", e); + } } - bool mty_hid_device_feature(struct hid_dev *ctx, void *buf, size_t size, size_t *size_out) { const uint8_t *buf8 = buf; diff --git a/src/unix/apple/macosx/hid_gc.h b/src/unix/apple/macosx/hid_gc.h new file mode 100644 index 000000000..1e328bac8 --- /dev/null +++ b/src/unix/apple/macosx/hid_gc.h @@ -0,0 +1,29 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT License was not distributed with this file, +// You can obtain one at https://spdx.org/licenses/MIT.html. + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Send rumble to an Xbox controller using GCController framework +// device: IOHIDDeviceRef pointer +// low: Low frequency rumble intensity (0-65535) +// high: High frequency rumble intensity (0-65535) +// Returns true if rumble was sent successfully +bool mty_hid_gc_rumble(void *device, uint16_t low, uint16_t high); + +// Clean up rumble context for a device +void mty_hid_gc_cleanup(void *device); + +// Check if GCController rumble is available for a device +bool mty_hid_gc_rumble_available(void *device); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/unix/apple/macosx/hid_gc.m b/src/unix/apple/macosx/hid_gc.m new file mode 100644 index 000000000..a9e20ad94 --- /dev/null +++ b/src/unix/apple/macosx/hid_gc.m @@ -0,0 +1,350 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT License was not distributed with this file, +// You can obtain one at https://spdx.org/licenses/MIT.html. + +#import +#import +#import +#import + +// Structure to hold rumble context for a controller +@interface MTYRumbleMotor : NSObject +@property(nonatomic, strong) CHHapticEngine *engine API_AVAILABLE(macos(11.0)); +@property(nonatomic, strong) id player API_AVAILABLE(macos(11.0)); +@property(nonatomic) BOOL active; +@end + +@implementation MTYRumbleMotor + +- (void)cleanup { + @autoreleasepool { + if (@available(macOS 11.0, *)) { + if (self.player != nil) { + [self.player cancelAndReturnError:nil]; + self.player = nil; + } + if (self.engine != nil) { + [self.engine stopWithCompletionHandler:nil]; + self.engine = nil; + } + } + } +} + +- (BOOL)setIntensity:(float)intensity { + @autoreleasepool { + if (@available(macOS 11.0, *)) { + NSError *error = nil; + + if (self.engine == nil) { + NSLog(@"Haptics engine was stopped"); + return NO; + } + + // Stop rumble if intensity is 0 + if (intensity == 0.0f) { + if (self.player && self.active) { + [self.player stopAtTime:0 error:&error]; + } + self.active = NO; + return YES; + } + + // Create player if needed + if (self.player == nil) { + CHHapticEventParameter *eventParam = [[CHHapticEventParameter alloc] + initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f]; + CHHapticEvent *event = [[CHHapticEvent alloc] + initWithEventType:CHHapticEventTypeHapticContinuous + parameters:@[eventParam] + relativeTime:0 + duration:GCHapticDurationInfinite]; + CHHapticPattern *pattern = [[CHHapticPattern alloc] + initWithEvents:@[event] + parameters:@[] + error:&error]; + + if (error != nil) { + NSLog(@"Couldn't create haptic pattern: %@", error.localizedDescription); + return NO; + } + + self.player = [self.engine createPlayerWithPattern:pattern error:&error]; + if (error != nil) { + NSLog(@"Couldn't create haptic player: %@", error.localizedDescription); + return NO; + } + self.active = NO; + } + + // Update intensity + CHHapticDynamicParameter *param = [[CHHapticDynamicParameter alloc] + initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl + value:intensity + relativeTime:0]; + [self.player sendParameters:@[param] atTime:0 error:&error]; + if (error != nil) { + NSLog(@"Couldn't update haptic player: %@", error.localizedDescription); + return NO; + } + + // Start playback if not active + if (!self.active) { + [self.player startAtTime:0 error:&error]; + self.active = YES; + } + + return YES; + } + return NO; + } +} + +- (id)initWithController:(GCController *)controller locality:(GCHapticsLocality)locality API_AVAILABLE(macos(11.0)) { + @autoreleasepool { + self = [super init]; + if (self) { + NSError *error = nil; + __weak __typeof(self) weakSelf = self; + + self.engine = [controller.haptics createEngineWithLocality:locality]; + if (self.engine == nil) { + NSLog(@"Couldn't create haptics engine for locality: %@", locality); + return nil; + } + + [self.engine startAndReturnError:&error]; + if (error != nil) { + NSLog(@"Couldn't start haptics engine: %@", error.localizedDescription); + return nil; + } + + // Set up handlers for engine stopping/resetting + self.engine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) { + MTYRumbleMotor *strongSelf = weakSelf; + if (strongSelf) { + strongSelf.player = nil; + strongSelf.engine = nil; + } + }; + + self.engine.resetHandler = ^{ + MTYRumbleMotor *strongSelf = weakSelf; + if (strongSelf) { + strongSelf.player = nil; + [strongSelf.engine startAndReturnError:nil]; + } + }; + } + return self; + } +} + +@end + +// Rumble context for a controller with low and high frequency motors +@interface MTYRumbleContext : NSObject +@property(nonatomic, strong) MTYRumbleMotor *lowFrequencyMotor; +@property(nonatomic, strong) MTYRumbleMotor *highFrequencyMotor; +@property(nonatomic, strong) GCController *controller; +@end + +@implementation MTYRumbleContext + +- (id)initWithController:(GCController *)controller { + self = [super init]; + if (self) { + self.controller = controller; + + if (@available(macOS 11.0, *)) { + // Initialize rumble motors + self.lowFrequencyMotor = [[MTYRumbleMotor alloc] + initWithController:controller + locality:GCHapticsLocalityLeftHandle]; + self.highFrequencyMotor = [[MTYRumbleMotor alloc] + initWithController:controller + locality:GCHapticsLocalityRightHandle]; + + if (!self.lowFrequencyMotor || !self.highFrequencyMotor) { + NSLog(@"Failed to initialize rumble motors"); + return nil; + } + } + } + return self; +} + +- (BOOL)rumbleWithLowFrequency:(uint16_t)lowFrequency highFrequency:(uint16_t)highFrequency { + if (@available(macOS 11.0, *)) { + BOOL result = YES; + result &= [self.lowFrequencyMotor setIntensity:(float)lowFrequency / 65535.0f]; + result &= [self.highFrequencyMotor setIntensity:(float)highFrequency / 65535.0f]; + return result; + } + return NO; +} + +- (void)cleanup { + if (@available(macOS 11.0, *)) { + [self.lowFrequencyMotor cleanup]; + [self.highFrequencyMotor cleanup]; + } +} + +@end + +// Global storage for controller rumble contexts +static NSMutableDictionary *g_rumbleContexts = nil; +static dispatch_once_t g_rumbleContextsOnce; + +// Initialize the global rumble contexts dictionary +static void mty_hid_gc_init(void) { + dispatch_once(&g_rumbleContextsOnce, ^{ + g_rumbleContexts = [[NSMutableDictionary alloc] init]; + }); +} + +// Find GCController for a given IOHIDDevice +static GCController *mty_hid_gc_find_controller(IOHIDDeviceRef device) { + @autoreleasepool { + // Get vendor and product IDs from the IOHIDDevice + NSNumber *vendorID = (__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey)); + NSNumber *productID = (__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)); + + if (!vendorID || !productID) { + NSLog(@"mty_hid_gc: No vendor/product ID"); + return nil; + } + + uint16_t vid = [vendorID unsignedShortValue]; + uint16_t pid = [productID unsignedShortValue]; + + NSLog(@"mty_hid_gc: Looking for controller VID: 0x%04X, PID: 0x%04X", vid, pid); + + // Check if this is an Xbox controller + if (vid != 0x045E) { // Microsoft vendor ID + NSLog(@"mty_hid_gc: Not a Microsoft controller"); + return nil; + } + + // Check for Xbox 360 wired controller PIDs - add more PIDs + BOOL isXbox360 = (pid == 0x028E || pid == 0x028F || pid == 0x02A1 || + pid == 0x0291 || pid == 0x0719 || pid == 0x02A0 || + pid == 0x02DD || pid == 0x02E3 || pid == 0x02FF || pid == 0x02EA); + if (!isXbox360) { + NSLog(@"mty_hid_gc: Not an Xbox 360/One controller PID"); + return nil; + } + + NSLog(@"mty_hid_gc: Found %lu GCControllers", (unsigned long)[[GCController controllers] count]); + + // Find matching GCController + for (GCController *controller in [GCController controllers]) { + // For Xbox controllers, GCController doesn't expose vendor/product IDs directly + // We need to match based on the controller type + if (@available(macOS 10.15, *)) { + NSString *productCategory = controller.productCategory; + NSString *vendorName = controller.vendorName; + NSLog(@"mty_hid_gc: Checking controller - Category: %@, Vendor: %@", productCategory, vendorName); + + // Check for Xbox controller - be more lenient with matching + if ([productCategory isEqualToString:@"Xbox One"] || + [vendorName containsString:@"Xbox"] || + [vendorName containsString:@"Microsoft"] || + [productCategory containsString:@"Xbox"]) { + NSLog(@"mty_hid_gc: Found matching Xbox controller!"); + // Found a potential match + // Store the rumble context for this controller + NSValue *deviceKey = [NSValue valueWithPointer:device]; + MTYRumbleContext *context = g_rumbleContexts[deviceKey]; + if (!context) { + context = [[MTYRumbleContext alloc] initWithController:controller]; + if (context) { + g_rumbleContexts[deviceKey] = context; + NSLog(@"mty_hid_gc: Created rumble context successfully"); + } else { + NSLog(@"mty_hid_gc: Failed to create rumble context"); + } + } + return controller; + } + } + } + + NSLog(@"mty_hid_gc: No matching GCController found"); + return nil; + } +} + +// C interface for rumble functionality +bool mty_hid_gc_rumble(void *device, uint16_t low, uint16_t high) { + @autoreleasepool { + // Only log non-zero rumble for less noise + if (low > 0 || high > 0) { + NSLog(@"mty_hid_gc_rumble: RUMBLE ON - low=%u, high=%u", low, high); + } + mty_hid_gc_init(); + + IOHIDDeviceRef hidDevice = (IOHIDDeviceRef)device; + NSValue *deviceKey = [NSValue valueWithPointer:hidDevice]; + + // Try to get existing rumble context + MTYRumbleContext *context = g_rumbleContexts[deviceKey]; + + // If no context exists, try to find and create one + if (!context) { + NSLog(@"mty_hid_gc_rumble: No existing context, trying to find controller"); + GCController *controller = mty_hid_gc_find_controller(hidDevice); + if (controller) { + if (@available(macOS 11.0, *)) { + if (controller.haptics) { + context = [[MTYRumbleContext alloc] initWithController:controller]; + if (context) { + g_rumbleContexts[deviceKey] = context; + NSLog(@"mty_hid_gc_rumble: Created new context"); + } + } else { + NSLog(@"mty_hid_gc_rumble: Controller has no haptics support"); + } + } + } + } + + // Apply rumble if we have a context + if (context) { + NSLog(@"mty_hid_gc_rumble: Applying rumble"); + return [context rumbleWithLowFrequency:low highFrequency:high]; + } + + NSLog(@"mty_hid_gc_rumble: Failed - no context available"); + return false; + } +} + +// Clean up rumble context for a device +void mty_hid_gc_cleanup(void *device) { + @autoreleasepool { + if (g_rumbleContexts) { + IOHIDDeviceRef hidDevice = (IOHIDDeviceRef)device; + NSValue *deviceKey = [NSValue valueWithPointer:hidDevice]; + + MTYRumbleContext *context = g_rumbleContexts[deviceKey]; + if (context) { + [context cleanup]; + [g_rumbleContexts removeObjectForKey:deviceKey]; + } + } + } +} + +// Check if GCController rumble is available for a device +bool mty_hid_gc_rumble_available(void *device) { + @autoreleasepool { + if (@available(macOS 11.0, *)) { + IOHIDDeviceRef hidDevice = (IOHIDDeviceRef)device; + GCController *controller = mty_hid_gc_find_controller(hidDevice); + return (controller && controller.haptics != nil); + } + return false; + } +} \ No newline at end of file diff --git a/test/GNUmakefile b/test/GNUmakefile index c11a97fce..613a94266 100644 --- a/test/GNUmakefile +++ b/test/GNUmakefile @@ -20,7 +20,9 @@ LIBS = \ -framework IOKit \ -framework Metal \ -framework QuartzCore \ - -framework WebKit + -framework WebKit \ + -framework GameController \ + -framework CoreHaptics else diff --git a/test/src/0-minimal.c b/test/src/0-minimal.c index 8f49a65e2..7a5675ee9 100644 --- a/test/src/0-minimal.c +++ b/test/src/0-minimal.c @@ -1,22 +1,29 @@ #include "matoya.h" - +#include "stdio.h" // Your top level application context struct context { MTY_App *app; bool quit; }; +// This function will fire for each event // This function will fire for each event static void event_func(const MTY_Event *evt, void *opaque) { - struct context *ctx = opaque; + struct context *ctx = opaque; - MTY_PrintEvent(evt); + MTY_PrintEvent(evt); - if (evt->type == MTY_EVENT_CLOSE) - ctx->quit = true; -} + if (evt->type == MTY_EVENT_CLOSE) + ctx->quit = true; + + if (evt->type == MTY_EVENT_CONTROLLER) { + printf("Controller ID: %u\n", evt->controller.id); // Print the controller ID + printf("Triggering Rumble\n"); + MTY_AppRumbleController(ctx->app, evt->controller.id, UINT16_MAX, UINT16_MAX); + } +} // This function fires once per "cycle", either blocked by a // call to MTY_WindowPresent or limited by MTY_AppSetTimeout static bool app_func(void *opaque) @@ -47,4 +54,4 @@ int main(int argc, char **argv) MTY_AppDestroy(&ctx.app); return 0; -} +} \ No newline at end of file diff --git a/test/test_gc b/test/test_gc new file mode 100755 index 000000000..191700b50 Binary files /dev/null and b/test/test_gc differ diff --git a/test/test_gc.m b/test/test_gc.m new file mode 100644 index 000000000..4e33b0014 --- /dev/null +++ b/test/test_gc.m @@ -0,0 +1,119 @@ +#import +#import +#import + +int main(int argc, char **argv) { + @autoreleasepool { + // Monitor for controller connections + [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + GCController *controller = note.object; + NSLog(@"Controller connected: %@ - %@", controller.vendorName, controller.productCategory); + }]; + + // List current controllers + NSArray *controllers = [GCController controllers]; + NSLog(@"Currently connected controllers: %lu", (unsigned long)controllers.count); + + for (GCController *controller in controllers) { + NSLog(@"Controller: %@ - Category: %@", controller.vendorName, controller.productCategory); + + if (@available(macOS 11.0, *)) { + if (controller.haptics) { + NSLog(@" Has haptics support!"); + + // Try to create and test rumble + NSError *error = nil; + CHHapticEngine *engine = [controller.haptics createEngineWithLocality:GCHapticsLocalityDefault]; + if (engine) { + [engine startAndReturnError:&error]; + if (!error) { + NSLog(@" Successfully started haptic engine!"); + + // Create a simple rumble pattern + CHHapticEventParameter *intensity = [[CHHapticEventParameter alloc] + initWithParameterID:CHHapticEventParameterIDHapticIntensity value:0.5]; + CHHapticEvent *event = [[CHHapticEvent alloc] + initWithEventType:CHHapticEventTypeHapticContinuous + parameters:@[intensity] + relativeTime:0 + duration:0.5]; + CHHapticPattern *pattern = [[CHHapticPattern alloc] + initWithEvents:@[event] + parameters:@[] + error:&error]; + + if (!error) { + id player = [engine createPlayerWithPattern:pattern error:&error]; + if (!error) { + NSLog(@" Testing rumble for 0.5 seconds..."); + [player startAtTime:0 error:&error]; + [NSThread sleepForTimeInterval:1.0]; + [player stopAtTime:0 error:&error]; + NSLog(@" Rumble test complete!"); + } + } + } + } + } else { + NSLog(@" No haptics support"); + } + } + } + + // Keep running for a bit to detect controllers + NSLog(@"Waiting 5 seconds for controller detection..."); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]]; + + // Check again + controllers = [GCController controllers]; + NSLog(@"Final controller count: %lu", (unsigned long)controllers.count); + + // Test rumble on all controllers + for (GCController *controller in controllers) { + NSLog(@"Testing rumble on: %@ - %@", controller.vendorName, controller.productCategory); + + if (@available(macOS 11.0, *)) { + if (controller.haptics) { + NSError *error = nil; + CHHapticEngine *engine = [controller.haptics createEngineWithLocality:GCHapticsLocalityDefault]; + if (engine) { + [engine startAndReturnError:&error]; + if (!error) { + // Create a simple rumble pattern + CHHapticEventParameter *intensity = [[CHHapticEventParameter alloc] + initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0]; + CHHapticEvent *event = [[CHHapticEvent alloc] + initWithEventType:CHHapticEventTypeHapticContinuous + parameters:@[intensity] + relativeTime:0 + duration:1.0]; + CHHapticPattern *pattern = [[CHHapticPattern alloc] + initWithEvents:@[event] + parameters:@[] + error:&error]; + + if (!error) { + id player = [engine createPlayerWithPattern:pattern error:&error]; + if (!error) { + NSLog(@" RUMBLING NOW for 1 second..."); + [player startAtTime:0 error:&error]; + [NSThread sleepForTimeInterval:1.5]; + NSLog(@" Rumble complete!"); + } + } + } + } + } else { + NSLog(@" No haptics support"); + } + } else { + NSLog(@" macOS 11.0+ required for haptics"); + } + } + } + + return 0; +} \ No newline at end of file