diff --git a/Cocoa/AppDelegate.h b/Cocoa/AppDelegate.h index 608a50ce..22e0c365 100644 --- a/Cocoa/AppDelegate.h +++ b/Cocoa/AppDelegate.h @@ -1,6 +1,6 @@ #import -@interface AppDelegate : NSObject +@interface AppDelegate : NSObject @property IBOutlet NSWindow *preferencesWindow; @property (strong) IBOutlet NSView *graphicsTab; diff --git a/Cocoa/AppDelegate.m b/Cocoa/AppDelegate.m index 34046202..e54012fd 100644 --- a/Cocoa/AppDelegate.m +++ b/Cocoa/AppDelegate.m @@ -50,6 +50,8 @@ JOYAxes2DEmulateButtonsKey: @YES, JOYHatsEmulateButtonsKey: @YES, }]; + + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; } - (IBAction)toggleDeveloperMode:(id)sender @@ -101,4 +103,8 @@ return YES; } +- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification +{ + [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:YES]; +} @end diff --git a/Cocoa/Document.m b/Cocoa/Document.m index ff47cd9f..ff77f8ef 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -358,6 +358,23 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) } NSTimer *hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode]; + + /* Clear pending alarms, don't play alarms while playing*/ + NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; + for (NSUserNotification *notification in [center scheduledNotifications]) { + if ([notification.identifier isEqualToString:self.fileName]) { + [center removeScheduledNotification:notification]; + break; + } + } + + for (NSUserNotification *notification in [center deliveredNotifications]) { + if ([notification.identifier isEqualToString:self.fileName]) { + [center removeDeliveredNotification:notification]; + break; + } + } + while (running) { if (rewind) { rewind = false; @@ -381,6 +398,22 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) self.view.mouseHidingEnabled = NO; GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); GB_save_cheats(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"cht"] UTF8String]); + unsigned time_to_alarm = GB_time_to_alarm(&gb); + + if (time_to_alarm) { + NSUserNotification *notification = [[NSUserNotification alloc] init]; + NSString *friendlyName = [[self.fileName lastPathComponent] stringByDeletingPathExtension]; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil]; + friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""]; + friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + notification.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName]; + notification.informativeText = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName]; + notification.identifier = self.fileName; + notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm]; + notification.soundName = NSUserNotificationDefaultSoundName; + [center scheduleNotification:notification]; + } [_view setRumble:0]; stopping = false; } diff --git a/Core/gb.c b/Core/gb.c index d0632ddd..ce9b9af9 100644 --- a/Core/gb.c +++ b/Core/gb.c @@ -564,6 +564,8 @@ typedef struct __attribute__((packed)) { uint64_t last_rtc_second; uint16_t minutes; uint16_t days; + uint16_t alarm_minutes, alarm_days; + uint8_t alarm_enabled; } GB_huc3_rtc_time_t; typedef union { @@ -612,12 +614,18 @@ int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size) __builtin_bswap64(gb->last_rtc_second), __builtin_bswap16(gb->huc3_minutes), __builtin_bswap16(gb->huc3_days), + __builtin_bswap16(gb->huc3_alarm_minutes), + __builtin_bswap16(gb->huc3_alarm_days), + gb->huc3_alarm_enabled, }; #else GB_huc3_rtc_time_t rtc_save = { gb->last_rtc_second, gb->huc3_minutes, gb->huc3_days, + gb->huc3_alarm_minutes, + gb->huc3_alarm_days, + gb->huc3_alarm_enabled, }; #endif memcpy(buffer, &rtc_save, sizeof(rtc_save)); @@ -666,12 +674,18 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) __builtin_bswap64(gb->last_rtc_second), __builtin_bswap16(gb->huc3_minutes), __builtin_bswap16(gb->huc3_days), + __builtin_bswap16(gb->huc3_alarm_minutes), + __builtin_bswap16(gb->huc3_alarm_days), + gb->huc3_alarm_enabled, }; #else GB_huc3_rtc_time_t rtc_save = { gb->last_rtc_second, gb->huc3_minutes, gb->huc3_days, + gb->huc3_alarm_minutes, + gb->huc3_alarm_days, + gb->huc3_alarm_enabled, }; #endif @@ -726,10 +740,16 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t gb->last_rtc_second = __builtin_bswap64(rtc_save.last_rtc_second); gb->huc3_minutes = __builtin_bswap16(rtc_save.minutes); gb->huc3_days = __builtin_bswap16(rtc_save.days); + gb->huc3_alarm_minutes = __builtin_bswap16(rtc_save.alarm_minutes); + gb->huc3_alarm_days = __builtin_bswap16(rtc_save.alarm_days); + gb->huc3_alarm_enabled = rtc_save.alarm_enabled; #else gb->last_rtc_second = rtc_save.last_rtc_second; gb->huc3_minutes = rtc_save.minutes; gb->huc3_days = rtc_save.days; + gb->huc3_alarm_minutes = rtc_save.alarm_minutes; + gb->huc3_alarm_days = rtc_save.alarm_days; + gb->huc3_alarm_enabled = rtc_save.alarm_enabled; #endif if (gb->last_rtc_second > time(NULL)) { /* We must reset RTC here, or it will not advance. */ @@ -802,6 +822,7 @@ reset_rtc: gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */ gb->huc3_days = 0xFFFF; gb->huc3_minutes = 0xFFF; + gb->huc3_alarm_enabled = false; exit: return; } @@ -827,10 +848,16 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) gb->last_rtc_second = __builtin_bswap64(rtc_save.last_rtc_second); gb->huc3_minutes = __builtin_bswap16(rtc_save.minutes); gb->huc3_days = __builtin_bswap16(rtc_save.days); + gb->huc3_alarm_minutes = __builtin_bswap16(rtc_save.alarm_minutes); + gb->huc3_alarm_days = __builtin_bswap16(rtc_save.alarm_days); + gb->huc3_alarm_enabled = rtc_save.alarm_enabled; #else gb->last_rtc_second = rtc_save.last_rtc_second; gb->huc3_minutes = rtc_save.minutes; gb->huc3_days = rtc_save.days; + gb->huc3_alarm_minutes = rtc_save.alarm_minutes; + gb->huc3_alarm_days = rtc_save.alarm_days; + gb->huc3_alarm_enabled = rtc_save.alarm_enabled; #endif if (gb->last_rtc_second > time(NULL)) { /* We must reset RTC here, or it will not advance. */ @@ -902,6 +929,7 @@ reset_rtc: gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */ gb->huc3_days = 0xFFFF; gb->huc3_minutes = 0xFFF; + gb->huc3_alarm_enabled = false; exit: fclose(f); return; @@ -1568,3 +1596,14 @@ void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t gb->boot_rom_load_callback = callback; request_boot_rom(gb); } + +unsigned GB_time_to_alarm(GB_gameboy_t *gb) +{ + if (gb->cartridge_type->mbc_type != GB_HUC3) return 0; + if (!gb->huc3_alarm_enabled) return 0; + if (!(gb->huc3_alarm_days & 0x2000)) return 0; + unsigned current_time = (gb->huc3_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3_minutes * 60 + (time(NULL) % 60); + unsigned alarm_time = (gb->huc3_alarm_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3_alarm_minutes * 60; + if (current_time > alarm_time) return 0; + return alarm_time - current_time; +} diff --git a/Core/gb.h b/Core/gb.h index 4e6cb3ee..27b95b33 100644 --- a/Core/gb.h +++ b/Core/gb.h @@ -438,6 +438,8 @@ struct GB_gameboy_internal_s { uint8_t huc3_mode; uint8_t huc3_access_index; uint16_t huc3_minutes, huc3_days; + uint16_t huc3_alarm_minutes, huc3_alarm_days; + bool huc3_alarm_enabled; uint8_t huc3_read; uint8_t huc3_access_flags; ); @@ -779,6 +781,9 @@ void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data); void GB_disconnect_serial(GB_gameboy_t *gb); +/* For cartridges with an alarm clock */ +unsigned GB_time_to_alarm(GB_gameboy_t *gb); // 0 if no alarm + /* For integration with SFC/SNES emulators */ void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback); void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback); diff --git a/Core/memory.c b/Core/memory.c index 85ee0999..7f7686a4 100644 --- a/Core/memory.c +++ b/Core/memory.c @@ -554,77 +554,97 @@ static void write_vram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->vram[(addr & 0x1FFF) + (uint16_t) gb->cgb_vram_bank * 0x2000] = value; } +static bool huc3_write(GB_gameboy_t *gb, uint8_t value) +{ + switch (gb->huc3_mode) { + case 0xB: // RTC Write + switch (value >> 4) { + case 1: + if (gb->huc3_access_index < 3) { + gb->huc3_read = (gb->huc3_minutes >> (gb->huc3_access_index * 4)) & 0xF; + } + else if (gb->huc3_access_index < 7) { + gb->huc3_read = (gb->huc3_days >> ((gb->huc3_access_index - 3) * 4)) & 0xF; + } + else { + // GB_log(gb, "Attempting to read from unsupported HuC-3 register: %03x\n", gb->huc3_access_index); + } + gb->huc3_access_index++; + break; + case 2: + case 3: + if (gb->huc3_access_index < 3) { + gb->huc3_minutes &= ~(0xF << (gb->huc3_access_index * 4)); + gb->huc3_minutes |= ((value & 0xF) << (gb->huc3_access_index * 4)); + } + else if (gb->huc3_access_index < 7) { + gb->huc3_days &= ~(0xF << ((gb->huc3_access_index - 3) * 4)); + gb->huc3_days |= ((value & 0xF) << ((gb->huc3_access_index - 3) * 4)); + } + else if (gb->huc3_access_index >= 0x58 && gb->huc3_access_index <= 0x5a) { + gb->huc3_alarm_minutes &= ~(0xF << ((gb->huc3_access_index - 0x58) * 4)); + gb->huc3_alarm_minutes |= ((value & 0xF) << ((gb->huc3_access_index - 0x58) * 4)); + } + else if (gb->huc3_access_index >= 0x5b && gb->huc3_access_index <= 0x5e) { + gb->huc3_alarm_days &= ~(0xF << ((gb->huc3_access_index - 0x5b) * 4)); + gb->huc3_alarm_days |= ((value & 0xF) << ((gb->huc3_access_index - 0x5b) * 4)); + } + else if (gb->huc3_access_index == 0x5f) { + gb->huc3_alarm_enabled = value & 1; + } + else { + // GB_log(gb, "Attempting to write %x to unsupported HuC-3 register: %03x\n", value & 0xF, gb->huc3_access_index); + } + if ((value >> 4) == 3) { + gb->huc3_access_index++; + } + break; + case 4: + gb->huc3_access_index &= 0xF0; + gb->huc3_access_index |= value & 0xF; + break; + case 5: + gb->huc3_access_index &= 0x0F; + gb->huc3_access_index |= (value & 0xF) << 4; + break; + case 6: + gb->huc3_access_flags = (value & 0xF); + break; + + default: + break; + } + + return true; + case 0xD: // RTC status + // Not sure what writes here mean, they're always 0xFE + return true; + case 0xE: { // IR mode + bool old_input = effective_ir_input(gb); + gb->cart_ir = value & 1; + bool new_input = effective_ir_input(gb); + if (new_input != old_input) { + if (gb->infrared_callback) { + gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); + } + gb->cycles_since_ir_change = 0; + } + return true; + } + case 0xC: + return true; + default: + return false; + case 0: // Disabled + case 0xA: // RAM + return false; + } +} + static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { if (gb->cartridge_type->mbc_type == GB_HUC3) { - switch (gb->huc3_mode) { - case 0xB: // RTC Write - switch (value >> 4) { - case 1: - if (gb->huc3_access_index < 3) { - gb->huc3_read = (gb->huc3_minutes >> (gb->huc3_access_index * 4)) & 0xF; - } - else if (gb->huc3_access_index < 7) { - gb->huc3_read = (gb->huc3_days >> ((gb->huc3_access_index - 3) * 4)) & 0xF; - } - else { - GB_log(gb, "Attempting to read from unsupported HuC-3 register: %03x\n", gb->huc3_access_index); - } - gb->huc3_access_index++; - return; - case 2: - case 3: - if (gb->huc3_access_index < 3) { - gb->huc3_minutes &= ~(0xF << (gb->huc3_access_index * 4)); - gb->huc3_minutes |= ((value & 0xF) << (gb->huc3_access_index * 4)); - } - else if (gb->huc3_access_index < 7) { - gb->huc3_days &= ~(0xF << ((gb->huc3_access_index - 3) * 4)); - gb->huc3_days |= ((value & 0xF) << ((gb->huc3_access_index - 3) * 4)); - } - if ((value >> 4) == 3) { - gb->huc3_access_index++; - } - return; - case 4: - gb->huc3_access_index &= 0xF0; - gb->huc3_access_index |= value & 0xF; - return; - case 5: - gb->huc3_access_index &= 0x0F; - gb->huc3_access_index |= (value & 0xF) << 4; - return; - case 6: - gb->huc3_access_flags = (value & 0xF); - return; - - default: - break; - } - - return; - case 0xD: // RTC status - // Not sure what writes here mean, they're always 0xFE - return; - case 0xE: { // IR mode - bool old_input = effective_ir_input(gb); - gb->cart_ir = value & 1; - bool new_input = effective_ir_input(gb); - if (new_input != old_input) { - if (gb->infrared_callback) { - gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); - } - gb->cycles_since_ir_change = 0; - } - return; - } - default: - GB_log(gb, "Unsupported HuC-3 mode %x write: [%04x] = %02x\n", gb->huc3_mode, addr, value); - return; - case 0: // Disabled - case 0xA: // RAM - break; - } + if (huc3_write(gb, value)) return; } if (gb->camera_registers_mapped) {