#import #import #import #import "GBAudioClient.h" #import "Document.h" #import "GBApp.h" #import "HexFiend/HexFiend.h" #import "GBMemoryByteArray.h" #import "GBWarningPopover.h" #import "GBCheatWindowController.h" #import "GBTerminalTextFieldCell.h" #import "BigSurToolbar.h" #import "GBPaletteEditorController.h" #import "GBCheatSearchController.h" #import "GBObjectView.h" #import "GBPaletteView.h" #import "GBHexStatusBarRepresenter.h" #import "NSObject+DefaultsObserver.h" #define likely(x) GB_likely(x) #define unlikely(x) GB_unlikely(x) @implementation NSString (relativePath) - (NSString *)pathRelativeToDirectory:(NSString *)directory { NSMutableArray *baseComponents = [[directory pathComponents] mutableCopy]; NSMutableArray *selfComponents = [[self pathComponents] mutableCopy]; while (baseComponents.count) { if (![baseComponents.firstObject isEqualToString:selfComponents.firstObject]) { break; } [baseComponents removeObjectAtIndex:0]; [selfComponents removeObjectAtIndex:0]; } while (baseComponents.count) { [baseComponents removeObjectAtIndex:0]; [selfComponents insertObject:@".." atIndex:0]; } return [selfComponents componentsJoinedByString:@"/"]; } @end #define GB_MODEL_PAL_BIT_OLD 0x1000 /* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */ /* Todo: Split into category files! This is so messy!!! */ @interface Document () @property GBAudioClient *audioClient; @end @implementation Document { GB_gameboy_t _gb; volatile bool _running; volatile bool _stopping; NSConditionLock *_hasDebuggerInput; NSMutableArray *_debuggerInputQueue; NSMutableAttributedString *_pendingConsoleOutput; NSRecursiveLock *_consoleOutputLock; NSTimer *_consoleOutputTimer; NSTimer *_hexTimer; bool _fullScreen; bool _inSyncInput; NSString *_debuggerCommandWhilePaused; HFController *_hexController; NSString *_lastConsoleInput; HFLineCountingRepresenter *_lineRep; GBHexStatusBarRepresenter *_statusRep; CVImageBufferRef _cameraImage; AVCaptureSession *_cameraSession; AVCaptureConnection *_cameraConnection; AVCaptureStillImageOutput *_cameraOutput; GB_oam_info_t _oamInfo[40]; NSMutableData *_currentPrinterImageData; bool _romWarningIssued; NSMutableString *_capturedOutput; bool _logToSideView; bool _shouldClearSideView; enum model _currentModel; bool _usesAutoModel; bool _rewind; bool _modelsChanging; NSCondition *_audioLock; GB_sample_t *_audioBuffer; size_t _audioBufferSize; size_t _audioBufferPosition; size_t _audioBufferNeeded; double _volume; bool _borderModeChanged; /* Link cable*/ Document *_master; Document *_slave; signed _linkOffset; bool _linkCableBit; NSSavePanel *_audioSavePanel; bool _isRecordingAudio; void (^ volatile _pendingAtomicBlock)(); NSDate *_fileModificationTime; __weak NSThread *_emulationThread; GBCheatSearchController *_cheatSearchController; } static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self loadBootROM: type]; } static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self vblankWithType:type]; } static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self log:string withAttributes: attributes]; } static char *consoleInput(GB_gameboy_t *gb) { Document *self = (__bridge Document *)GB_get_user_data(gb); return [self getDebuggerInput]; } static char *asyncConsoleInput(GB_gameboy_t *gb) { Document *self = (__bridge Document *)GB_get_user_data(gb); char *ret = [self getAsyncDebuggerInput]; return ret; } static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { return (r << 0) | (g << 8) | (b << 16) | 0xFF000000; } static void cameraRequestUpdate(GB_gameboy_t *gb) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self cameraRequestUpdate]; } static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y) { Document *self = (__bridge Document *)GB_get_user_data(gb); return [self cameraGetPixelAtX:x andY:y]; } static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure]; } static void printDone(GB_gameboy_t *gb) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self printDone]; } static void setWorkboyTime(GB_gameboy_t *gb, time_t t) { [[NSUserDefaults standardUserDefaults] setInteger:time(NULL) - t forKey:@"GBWorkboyTimeOffset"]; } static time_t getWorkboyTime(GB_gameboy_t *gb) { return time(NULL) - [[NSUserDefaults standardUserDefaults] integerForKey:@"GBWorkboyTimeOffset"]; } static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self gotNewSample:sample]; } static void rumbleCallback(GB_gameboy_t *gb, double amp) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self rumbleChanged:amp]; } static void _linkCableBitStart(GB_gameboy_t *gb, bool bit_to_send) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self _linkCableBitStart:bit_to_send]; } static bool _linkCableBitEnd(GB_gameboy_t *gb) { Document *self = (__bridge Document *)GB_get_user_data(gb); return [self _linkCableBitEnd]; } static void infraredStateChanged(GB_gameboy_t *gb, bool on) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self infraredStateChanged:on]; } static void debuggerReloadCallback(GB_gameboy_t *gb) { Document *self = (__bridge Document *)GB_get_user_data(gb); dispatch_sync(dispatch_get_main_queue(), ^{ bool wasRunning = self->_running; self->_running = false; // Hack for output capture [self loadROM]; self->_running = wasRunning; GB_reset(gb); }); } - (instancetype)init { self = [super init]; if (self) { _hasDebuggerInput = [[NSConditionLock alloc] initWithCondition:0]; _debuggerInputQueue = [[NSMutableArray alloc] init]; _consoleOutputLock = [[NSRecursiveLock alloc] init]; _audioLock = [[NSCondition alloc] init]; _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; } return self; } - (GB_model_t)internalModel { switch (_currentModel) { case MODEL_DMG: return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"]; case MODEL_NONE: case MODEL_QUICK_RESET: case MODEL_AUTO: case MODEL_CGB: return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]; case MODEL_SGB: { GB_model_t model = (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]; if (model == (GB_MODEL_SGB | GB_MODEL_PAL_BIT_OLD)) { model = GB_MODEL_SGB_PAL; } return model; } case MODEL_MGB: return GB_MODEL_MGB; case MODEL_AGB: return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBAGBModel"]; } } - (void)updatePalette { GB_set_palette(&_gb, [GBPaletteEditorController userPalette]); } - (void)initCommon { GB_init(&_gb, [self internalModel]); GB_set_user_data(&_gb, (__bridge void *)(self)); GB_set_boot_rom_load_callback(&_gb, (GB_boot_rom_load_callback_t)boot_rom_load); GB_set_vblank_callback(&_gb, (GB_vblank_callback_t) vblank); GB_set_enable_skipped_frame_vblank_callbacks(&_gb, true); GB_set_log_callback(&_gb, (GB_log_callback_t) consoleLog); GB_set_input_callback(&_gb, (GB_input_callback_t) consoleInput); GB_set_async_input_callback(&_gb, (GB_input_callback_t) asyncConsoleInput); [self updatePalette]; GB_set_rgb_encode_callback(&_gb, rgbEncode); GB_set_camera_get_pixel_callback(&_gb, cameraGetPixel); GB_set_camera_update_request_callback(&_gb, cameraRequestUpdate); GB_apu_set_sample_callback(&_gb, audioCallback); GB_set_rumble_callback(&_gb, rumbleCallback); GB_set_infrared_callback(&_gb, infraredStateChanged); GB_debugger_set_reload_callback(&_gb, debuggerReloadCallback); GB_gameboy_t *gb = &_gb; __unsafe_unretained Document *weakSelf = self; [self observeStandardDefaultsKey:@"GBColorCorrection" withBlock:^(NSNumber *value) { GB_set_color_correction_mode(gb, value.unsignedIntValue); }]; [self observeStandardDefaultsKey:@"GBLightTemperature" withBlock:^(NSNumber *value) { GB_set_light_temperature(gb, value.doubleValue); }]; [self observeStandardDefaultsKey:@"GBInterferenceVolume" withBlock:^(NSNumber *value) { GB_set_interference_volume(gb, value.doubleValue); }]; GB_set_border_mode(&_gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); [self observeStandardDefaultsKey:@"GBBorderMode" withBlock:^(NSNumber *value) { weakSelf->_borderModeChanged = true; }]; [self observeStandardDefaultsKey:@"GBHighpassFilter" withBlock:^(NSNumber *value) { GB_set_highpass_filter_mode(gb, value.unsignedIntValue); }]; [self observeStandardDefaultsKey:@"GBRewindLength" withBlock:^(NSNumber *value) { [weakSelf performAtomicBlock:^{ GB_set_rewind_length(gb, value.unsignedIntValue); }]; }]; [self observeStandardDefaultsKey:@"GBRTCMode" withBlock:^(NSNumber *value) { GB_set_rtc_mode(gb, value.unsignedIntValue); }]; [self observeStandardDefaultsKey:@"GBRumbleMode" withBlock:^(NSNumber *value) { GB_set_rumble_mode(gb, value.unsignedIntValue); }]; [self observeStandardDefaultsKey:@"GBDebuggerFont" withBlock:^(NSString *value) { [weakSelf updateFonts]; }]; [self observeStandardDefaultsKey:@"GBDebuggerFontSize" withBlock:^(NSString *value) { [weakSelf updateFonts]; }]; [self observeStandardDefaultsKey:@"GBTurboCap" withBlock:^(NSNumber *value) { if (!_master) { GB_set_turbo_cap(gb, value.doubleValue); } }]; } - (void)updateMinSize { self.mainWindow.contentMinSize = NSMakeSize(GB_get_screen_width(&_gb), GB_get_screen_height(&_gb)); if (self.mainWindow.contentView.bounds.size.width < GB_get_screen_width(&_gb) || self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&_gb)) { [self.mainWindow zoom:nil]; } self.osdView.usesSGBScale = GB_get_screen_width(&_gb) == 256; } - (void)vblankWithType:(GB_vblank_type_t)type { if (type == GB_VBLANK_TYPE_SKIPPED_FRAME) { double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb); [_cpuView addSample:frameUsage]; return; } if (_gbsVisualizer) { dispatch_async(dispatch_get_main_queue(), ^{ [_gbsVisualizer setNeedsDisplay:true]; }); } double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb); [_cpuView addSample:frameUsage]; if (self.consoleWindow.visible) { double secondUsage = GB_debugger_get_second_cpu_usage(&_gb); dispatch_async(dispatch_get_main_queue(), ^{ [_cpuView setNeedsDisplay:true]; _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100]; }); } if (type != GB_VBLANK_TYPE_REPEAT) { [self.view flip]; if (_borderModeChanged) { dispatch_sync(dispatch_get_main_queue(), ^{ size_t previous_width = GB_get_screen_width(&_gb); GB_set_border_mode(&_gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); if (GB_get_screen_width(&_gb) != previous_width) { [self.view screenSizeChanged]; [self updateMinSize]; } }); _borderModeChanged = false; } GB_set_pixels_output(&_gb, self.view.pixels); } if (self.vramWindow.isVisible) { dispatch_async(dispatch_get_main_queue(), ^{ self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0; [self reloadVRAMData: nil]; }); } if (self.view.isRewinding) { _rewind = true; [self.osdView displayText:@"Rewinding…"]; } } - (void)gotNewSample:(GB_sample_t *)sample { if (_gbsVisualizer) { [_gbsVisualizer addSample:sample]; } [_audioLock lock]; if (_audioClient.isPlaying) { if (_audioBufferPosition == _audioBufferSize) { if (_audioBufferSize >= 0x4000) { _audioBufferPosition = 0; [_audioLock unlock]; return; } if (_audioBufferSize == 0) { _audioBufferSize = 512; } else { _audioBufferSize += _audioBufferSize >> 2; } _audioBuffer = realloc(_audioBuffer, sizeof(*sample) * _audioBufferSize); } if (_volume != 1) { sample->left *= _volume; sample->right *= _volume; } _audioBuffer[_audioBufferPosition++] = *sample; } if (_audioBufferPosition == _audioBufferNeeded) { [_audioLock signal]; _audioBufferNeeded = 0; } [_audioLock unlock]; } - (void)rumbleChanged:(double)amp { [_view setRumble:amp]; } - (void)preRun { GB_set_pixels_output(&_gb, self.view.pixels); GB_set_sample_rate(&_gb, 96000); _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { [_audioLock lock]; if (_audioBufferPosition < nFrames) { _audioBufferNeeded = nFrames; [_audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:(double)(_audioBufferNeeded - _audioBufferPosition) / sampleRate]]; _audioBufferNeeded = 0; } if (_stopping || GB_debugger_is_stopped(&_gb)) { memset(buffer, 0, nFrames * sizeof(*buffer)); [_audioLock unlock]; return; } if (_audioBufferPosition < nFrames) { // Not enough audio memset(buffer, 0, (nFrames - _audioBufferPosition) * sizeof(*buffer)); memcpy(buffer, _audioBuffer, _audioBufferPosition * sizeof(*buffer)); // Do not reset the audio position to avoid more underflows } else if (_audioBufferPosition < nFrames + 4800) { memcpy(buffer, _audioBuffer, nFrames * sizeof(*buffer)); memmove(_audioBuffer, _audioBuffer + nFrames, (_audioBufferPosition - nFrames) * sizeof(*buffer)); _audioBufferPosition = _audioBufferPosition - nFrames; } else { memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer)); _audioBufferPosition = 0; } [_audioLock unlock]; } andSampleRate:96000]; if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) { [_audioClient start]; } _hexTimer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:true]; [[NSRunLoop mainRunLoop] addTimer:_hexTimer forMode:NSDefaultRunLoopMode]; /* Clear pending alarms, don't play alarms while playing */ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; for (NSUserNotification *notification in [center scheduledNotifications]) { if ([notification.identifier isEqualToString:self.fileURL.path]) { [center removeScheduledNotification:notification]; break; } } for (NSUserNotification *notification in [center deliveredNotifications]) { if ([notification.identifier isEqualToString:self.fileURL.path]) { [center removeDeliveredNotification:notification]; break; } } } } static unsigned *multiplication_table_for_frequency(unsigned frequency) { unsigned *ret = malloc(sizeof(*ret) * 0x100); for (unsigned i = 0; i < 0x100; i++) { ret[i] = i * frequency; } return ret; } - (void)run { assert(!_master); [self preRun]; if (_slave) { [_slave preRun]; unsigned *masterTable = multiplication_table_for_frequency(GB_get_clock_rate(&_gb)); unsigned *slaveTable = multiplication_table_for_frequency(GB_get_clock_rate(&_slave->_gb)); while (_running) { if (_linkOffset <= 0) { _linkOffset += slaveTable[GB_run(&_gb)]; } else { _linkOffset -= masterTable[GB_run(&_slave->_gb)]; } if (unlikely(_pendingAtomicBlock)) { _pendingAtomicBlock(); _pendingAtomicBlock = nil; } } free(masterTable); free(slaveTable); [_slave postRun]; } else { while (_running) { if (_rewind) { _rewind = false; GB_rewind_pop(&_gb); if (!GB_rewind_pop(&_gb)) { _rewind = self.view.isRewinding; } } else { GB_run(&_gb); } if (unlikely(_pendingAtomicBlock)) { _pendingAtomicBlock(); _pendingAtomicBlock = nil; } } } [self postRun]; _stopping = false; } - (void)postRun { [_hexTimer invalidate]; [_audioLock lock]; _audioBufferPosition = _audioBufferNeeded = 0; [_audioLock signal]; [_audioLock unlock]; [_audioClient stop]; _audioClient = nil; self.view.mouseHidingEnabled = false; GB_save_battery(&_gb, self.savPath.UTF8String); GB_save_cheats(&_gb, self.chtPath.UTF8String); unsigned time_to_alarm = GB_time_to_alarm(&_gb); if (time_to_alarm) { [NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate]; NSUserNotification *notification = [[NSUserNotification alloc] init]; NSString *friendlyName = [[self.fileURL 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.fileURL.path; notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm]; notification.soundName = NSUserNotificationDefaultSoundName; [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification]; [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"]; } [_view setRumble:0]; } - (void) start { dispatch_async(dispatch_get_main_queue(), ^{ [self updateDebuggerButtons]; [_slave updateDebuggerButtons]; }); self.gbsPlayPauseButton.state = true; self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0; if (_master) { [_master start]; return; } if (_running) return; _running = true; NSThread *emulationThraed = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; _emulationThread = emulationThraed; [emulationThraed start]; } - (void) stop { dispatch_async(dispatch_get_main_queue(), ^{ [self updateDebuggerButtons]; [_slave updateDebuggerButtons]; }); self.gbsPlayPauseButton.state = false; if (_master) { if (!_master->_running) return; GB_debugger_set_disabled(&_gb, true); if (GB_debugger_is_stopped(&_gb)) { [self interruptDebugInputRead]; } [_master stop]; GB_debugger_set_disabled(&_gb, false); return; } if (!_running) return; GB_debugger_set_disabled(&_gb, true); if (GB_debugger_is_stopped(&_gb)) { [self interruptDebugInputRead]; } [_audioLock lock]; _stopping = true; [_audioLock signal]; [_audioLock unlock]; _running = false; while (_stopping) { [_audioLock lock]; [_audioLock signal]; [_audioLock unlock]; } GB_debugger_set_disabled(&_gb, false); } - (NSString *)bootROMPathForName:(NSString *)name { NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"]; if (url) { NSString *path = [url path]; path = [path stringByAppendingPathComponent:name]; path = [path stringByAppendingPathExtension:@"bin"]; if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { return path; } } return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"]; } - (void)loadBootROM: (GB_boot_rom_t)type { static NSString *const names[] = { [GB_BOOT_ROM_DMG_0] = @"dmg0_boot", [GB_BOOT_ROM_DMG] = @"dmg_boot", [GB_BOOT_ROM_MGB] = @"mgb_boot", [GB_BOOT_ROM_SGB] = @"sgb_boot", [GB_BOOT_ROM_SGB2] = @"sgb2_boot", [GB_BOOT_ROM_CGB_0] = @"cgb0_boot", [GB_BOOT_ROM_CGB] = @"cgb_boot", [GB_BOOT_ROM_CGB_E] = @"cgbE_boot", [GB_BOOT_ROM_AGB_0] = @"agb0_boot", [GB_BOOT_ROM_AGB] = @"agb_boot", }; NSString *name = names[type]; NSString *path = [self bootROMPathForName:name]; /* These boot types are not commonly available, and they are indentical from an emulator perspective, so fall back to the more common variants if they can't be found. */ if (!path && type == GB_BOOT_ROM_CGB_E) { [self loadBootROM:GB_BOOT_ROM_CGB]; return; } if (!path && type == GB_BOOT_ROM_AGB_0) { [self loadBootROM:GB_BOOT_ROM_AGB]; return; } GB_load_boot_rom(&_gb, [path UTF8String]); } - (enum model)bestModelForROM { uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL); if (!rom) return MODEL_CGB; if (rom[0x143] & 0x80) { // Has CGB features return MODEL_CGB; } if (rom[0x146] == 3) { // Has SGB features return MODEL_SGB; } if (rom[0x14B] == 1) { // Nintendo-licensed (most likely has boot ROM palettes) return MODEL_CGB; } if (rom[0x14B] == 0x33 && rom[0x144] == '0' && rom[0x145] == '1') { // Ditto return MODEL_CGB; } return MODEL_DMG; } - (IBAction)reset:(id)sender { [self stop]; size_t old_width = GB_get_screen_width(&_gb); if ([sender tag] > MODEL_NONE) { /* User explictly selected a model, save the preference */ _currentModel = (enum model)[sender tag]; _usesAutoModel = _currentModel == MODEL_AUTO; [[NSUserDefaults standardUserDefaults] setInteger:_currentModel forKey:@"GBEmulatedModel"]; } /* Reload the ROM, SAV and SYM files */ [self loadROM]; if ([sender tag] == MODEL_QUICK_RESET) { GB_quick_reset(&_gb); } else { GB_switch_model_and_reset(&_gb, [self internalModel]); } if (old_width != GB_get_screen_width(&_gb)) { [self.view screenSizeChanged]; } [self updateMinSize]; [self start]; if (_gbsTracks) { [self changeGBSTrack:sender]; } if (_hexController) { /* Verify bank sanity, especially when switching models. */ [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:0]; [self hexUpdateBank:self.memoryBankInput ignoreErrors:true]; } char title[17]; GB_get_rom_title(&_gb, title); [self.osdView displayText:[NSString stringWithFormat:@"SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&_gb)]]; } - (IBAction)togglePause:(id)sender { if (_master) { [_master togglePause:sender]; return; } if (_running) { [self stop]; } else { [self start]; } } - (void)dealloc { [_cameraSession stopRunning]; self.view.gb = NULL; GB_free(&_gb); if (_cameraImage) { CVBufferRelease(_cameraImage); } if (_audioBuffer) { free(_audioBuffer); } } - (NSFont *)debuggerFontOfSize:(unsigned)size { if (!size) { size = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"]; } bool retry = false; again:; NSString *selectedFont = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBDebuggerFont"]; if (@available(macOS 10.15, *)) { if ([selectedFont isEqual:@"SF Mono"]) { return [NSFont monospacedSystemFontOfSize:size weight:NSFontWeightRegular]; } } NSFont *ret = [NSFont fontWithName:selectedFont size:size]; if (ret) return ret; if (retry) { return [NSFont userFixedPitchFontOfSize:size]; } [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBDebuggerFont"]; retry = true; goto again; } - (void)updateFonts { _hexController.font = [self debuggerFontOfSize:12]; [self.paletteView reloadData:self]; [self.objectView reloadData:self]; NSFont *newFont = [self debuggerFontOfSize:0]; NSFont *newBoldFont = [[NSFontManager sharedFontManager] convertFont:newFont toHaveTrait:NSBoldFontMask]; self.debuggerSideViewInput.font = newFont; unsigned inputHeight = MAX(ceil([@" " sizeWithAttributes:@{ NSFontAttributeName: newFont }].height) + 6, 26); NSRect frame = _consoleInput.frame; unsigned oldHeight = frame.size.height; frame.size.height = inputHeight; _consoleInput.frame = frame; frame = _debugBar.frame; frame.origin.y += (signed)(inputHeight - oldHeight); _debugBar.frame = frame; frame = _debuggerScrollView.frame; frame.origin.y += (signed)(inputHeight - oldHeight); frame.size.height -= (signed)(inputHeight - oldHeight); _debuggerScrollView.frame = frame; _consoleInput.font = newFont; for (NSTextView *view in @[_debuggerSideView, _consoleOutput]) { NSMutableAttributedString *newString = view.attributedString.mutableCopy; [view.attributedString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, view.attributedString.length) options:0 usingBlock:^(NSFont *value, NSRange range, BOOL *stop) { if ([[NSFontManager sharedFontManager] fontNamed:value.fontName hasTraits:NSBoldFontMask]) { [newString addAttributes:@{ NSFontAttributeName: newBoldFont } range:range]; } else { [newString addAttributes:@{ NSFontAttributeName: newFont } range:range]; } }]; [view.textStorage setAttributedString:newString]; } [_consoleOutput scrollToEndOfDocument:nil]; } - (void)windowControllerDidLoadNib:(NSWindowController *)aController { [super windowControllerDidLoadNib:aController]; // Interface Builder bug? [self.consoleWindow setContentSize:self.consoleWindow.frame.size]; /* Close Open Panels, if any */ for (NSWindow *window in [[NSApplication sharedApplication] windows]) { if ([window isKindOfClass:[NSOpenPanel class]]) { [(NSOpenPanel *)window cancel:self]; } } NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init]; [paragraph_style setLineSpacing:2]; self.debuggerSideViewInput.font = [self debuggerFontOfSize:0]; self.debuggerSideViewInput.textColor = [NSColor whiteColor]; self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style; [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"]; ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &_gb; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateSideView) name:NSTextDidChangeNotification object:self.debuggerSideViewInput]; self.consoleOutput.textContainerInset = NSMakeSize(4, 4); [self.view becomeFirstResponder]; self.view.frameBlendingMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; CGRect window_frame = self.mainWindow.frame; window_frame.size.width = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowWidth"], window_frame.size.width); window_frame.size.height = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowHeight"], window_frame.size.height); [self.mainWindow setFrame:window_frame display:true]; self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised; NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height; CGRect vram_window_rect = self.vramWindow.frame; vram_window_rect.size.height = 384 + height_diff + 48; [self.vramWindow setFrame:vram_window_rect display:true animate:false]; if (@available(macOS 11.0, *)) { self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent]; self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent]; self.vramWindow.subtitle = [self.fileURL.path lastPathComponent]; } else { self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]]; self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [self.fileURL.path lastPathComponent]]; self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [self.fileURL.path lastPathComponent]]; } self.consoleWindow.level = NSNormalWindowLevel; self.debuggerSplitView.dividerColor = self.debuggerVerticalLine.borderColor; [self.debuggerVerticalLine removeFromSuperview]; // No longer used, just there for the color if (@available(macOS 11.0, *)) { self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded; self.printerFeedWindow.toolbarStyle = NSWindowToolbarStyleUnifiedCompact; self.printerFeedWindow.toolbar.items[1].image = [NSImage imageWithSystemSymbolName:@"square.and.arrow.down" accessibilityDescription:@"Save"]; self.printerFeedWindow.toolbar.items[2].image = [NSImage imageWithSystemSymbolName:@"printer" accessibilityDescription:@"Print"]; self.printerFeedWindow.toolbar.items[1].bordered = false; self.printerFeedWindow.toolbar.items[2].bordered = false; } else { NSToolbarItem *spinner = self.printerFeedWindow.toolbar.items[0]; [self.printerFeedWindow.toolbar removeItemAtIndex:0]; [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:spinner.itemIdentifier atIndex:2]; [self.printerFeedWindow.toolbar removeItemAtIndex:1]; [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:NSToolbarPrintItemIdentifier atIndex:1]; [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier atIndex:2]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatePalette) name:@"GBColorPaletteChanged" object:nil]; __unsafe_unretained Document *weakSelf = self; [self observeStandardDefaultsKey:@"GBFrameBlendingMode" withBlock:^(NSNumber *value) { weakSelf.view.frameBlendingMode = (GB_frame_blending_mode_t)value.unsignedIntValue; }]; [self observeStandardDefaultsKey:@"GBDMGModel" withBlock:^(id newValue) { weakSelf->_modelsChanging = true; if (weakSelf->_currentModel == MODEL_DMG) { [weakSelf reset:nil]; } weakSelf->_modelsChanging = false; }]; [self observeStandardDefaultsKey:@"GBSGBModel" withBlock:^(id newValue) { weakSelf->_modelsChanging = true; if (weakSelf->_currentModel == MODEL_SGB) { [weakSelf reset:nil]; } weakSelf->_modelsChanging = false; }]; [self observeStandardDefaultsKey:@"GBCGBModel" withBlock:^(id newValue) { weakSelf->_modelsChanging = true; if (weakSelf->_currentModel == MODEL_CGB) { [weakSelf reset:nil]; } weakSelf->_modelsChanging = false; }]; [self observeStandardDefaultsKey:@"GBAGBModel" withBlock:^(id newValue) { weakSelf->_modelsChanging = true; if (weakSelf->_currentModel == MODEL_AGB) { [weakSelf reset:nil]; } weakSelf->_modelsChanging = false; }]; [self observeStandardDefaultsKey:@"GBVolume" withBlock:^(id newValue) { weakSelf->_volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; }]; _currentModel = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBEmulatedModel"]; _usesAutoModel = _currentModel == MODEL_AUTO; [self initCommon]; self.view.gb = &_gb; self.view.osdView = _osdView; [self.view screenSizeChanged]; if ([self loadROM]) { _mainWindow.alphaValue = 0; // Hack hack ugly hack dispatch_async(dispatch_get_main_queue(), ^{ [self close]; }); } else { [self reset:nil]; } } - (void)initMemoryView { _hexController = [[HFController alloc] init]; _hexController.font = [self debuggerFontOfSize:12]; [_hexController setBytesPerColumn:1]; [_hexController setEditMode:HFOverwriteMode]; [_hexController setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]]; /* Here we're going to make three representers - one for the hex, one for the ASCII, and one for the scrollbar. To lay these all out properly, we'll use a fourth HFLayoutRepresenter. */ HFLayoutRepresenter *layoutRep = [[HFLayoutRepresenter alloc] init]; HFHexTextRepresenter *hexRep = [[HFHexTextRepresenter alloc] init]; HFStringEncodingTextRepresenter *asciiRep = [[HFStringEncodingTextRepresenter alloc] init]; HFVerticalScrollerRepresenter *scrollRep = [[HFVerticalScrollerRepresenter alloc] init]; _lineRep = [[HFLineCountingRepresenter alloc] init]; _statusRep = [[GBHexStatusBarRepresenter alloc] init]; _statusRep.gb = &_gb; _statusRep.bankForDescription = -1; _lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal; /* Add all our reps to the controller. */ [_hexController addRepresenter:layoutRep]; [_hexController addRepresenter:hexRep]; [_hexController addRepresenter:asciiRep]; [_hexController addRepresenter:scrollRep]; [_hexController addRepresenter:_lineRep]; [_hexController addRepresenter:_statusRep]; /* Tell the layout rep which reps it should lay out. */ [layoutRep addRepresenter:hexRep]; [layoutRep addRepresenter:scrollRep]; [layoutRep addRepresenter:asciiRep]; [layoutRep addRepresenter:_lineRep]; [layoutRep addRepresenter:_statusRep]; [(NSView *)[hexRep view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; /* Grab the layout rep's view and stick it into our container. */ NSView *layoutView = [layoutRep view]; NSRect layoutViewFrame = self.memoryView.frame; [layoutView setFrame:layoutViewFrame]; [layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin]; [self.memoryView addSubview:layoutView]; self.memoryView = layoutView; CGSize contentSize = _memoryWindow.contentView.frame.size; while (_hexController.bytesPerLine < 16) { contentSize.width += 4; [_memoryWindow setContentSize:contentSize]; } while (_hexController.bytesPerLine > 16) { contentSize.width -= 4; [_memoryWindow setContentSize:contentSize]; } self.memoryBankItem.enabled = false; } + (BOOL)autosavesInPlace { return true; } - (NSString *)windowNibName { // Override returning the nib file name of the document // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead. return @"Document"; } - (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type { return true; } - (IBAction)changeGBSTrack:(id)sender { if (!_running) { [self start]; } [self performAtomicBlock:^{ GB_gbs_switch_track(&_gb, self.gbsTracks.indexOfSelectedItem); }]; } - (IBAction)gbsNextPrevPushed:(id)sender { if (self.gbsNextPrevButton.selectedSegment == 0) { // Previous if (self.gbsTracks.indexOfSelectedItem == 0) { [self.gbsTracks selectItemAtIndex:self.gbsTracks.numberOfItems - 1]; } else { [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem - 1]; } } else { // Next if (self.gbsTracks.indexOfSelectedItem == self.gbsTracks.numberOfItems - 1) { [self.gbsTracks selectItemAtIndex: 0]; } else { [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem + 1]; } } [self changeGBSTrack:sender]; } - (void)prepareGBSInterface: (GB_gbs_info_t *)info { GB_set_rendering_disabled(&_gb, true); _view = nil; for (NSView *view in [_mainWindow.contentView.subviews copy]) { [view removeFromSuperview]; } if (@available(macOS 11, *)) { [[NSBundle mainBundle] loadNibNamed:@"GBS11" owner:self topLevelObjects:nil]; } else { [[NSBundle mainBundle] loadNibNamed:@"GBS" owner:self topLevelObjects:nil]; } [_mainWindow setContentSize:self.gbsPlayerView.bounds.size]; _mainWindow.styleMask &= ~NSWindowStyleMaskResizable; dispatch_async(dispatch_get_main_queue(), ^{ // Cocoa is weird, no clue why it's needed [_mainWindow standardWindowButton:NSWindowZoomButton].enabled = false; }); [_mainWindow.contentView addSubview:self.gbsPlayerView]; _mainWindow.movableByWindowBackground = true; [_mainWindow setContentBorderThickness:24 forEdge:NSRectEdgeMinY]; self.gbsTitle.stringValue = [NSString stringWithCString:info->title encoding:NSISOLatin1StringEncoding] ?: @"GBS Player"; self.gbsAuthor.stringValue = [NSString stringWithCString:info->author encoding:NSISOLatin1StringEncoding] ?: @"Unknown Composer"; NSString *copyright = [NSString stringWithCString:info->copyright encoding:NSISOLatin1StringEncoding]; if (copyright) { copyright = [@"©" stringByAppendingString:copyright]; } self.gbsCopyright.stringValue = copyright ?: @"Missing copyright information"; for (unsigned i = 0; i < info->track_count; i++) { [self.gbsTracks addItemWithTitle:[NSString stringWithFormat:@"Track %u", i + 1]]; } [self.gbsTracks selectItemAtIndex:info->first_track]; self.gbsPlayPauseButton.image.template = true; self.gbsPlayPauseButton.alternateImage.template = true; self.gbsRewindButton.image.template = true; for (unsigned i = 0; i < 2; i++) { [self.gbsNextPrevButton imageForSegment:i].template = true; } if (!_audioClient.isPlaying) { [_audioClient start]; } if (@available(macOS 10.10, *)) { _mainWindow.titlebarAppearsTransparent = true; } if (@available(macOS 26.0, *)) { // There's a new minimum width for segmented controls in Solarium NSRect frame = _gbsNextPrevButton.frame; frame.origin.x -= 16; _gbsNextPrevButton.frame = frame; frame = _gbsTracks.frame; frame.size.width -= 16; _gbsTracks.frame = frame; } } - (bool)isCartContainer { return [self.fileName.pathExtension.lowercaseString isEqualToString:@"gbcart"]; } - (NSString *)savPath { if (self.isCartContainer) { return [self.fileName stringByAppendingPathComponent:@"battery.sav"]; } return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path; } - (NSString *)chtPath { if (self.isCartContainer) { return [self.fileName stringByAppendingPathComponent:@"cheats.cht"]; } return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path; } - (NSString *)saveStatePath:(unsigned)index { if (self.isCartContainer) { return [self.fileName stringByAppendingPathComponent:[NSString stringWithFormat:@"state.s%u", index]]; } return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%u", index]].path; } - (NSString *)romPath { NSString *fileName = self.fileName; if (self.isCartContainer) { NSArray *paths = [[NSString stringWithContentsOfFile:[fileName stringByAppendingPathComponent:@"rom.gbl"] encoding:NSUTF8StringEncoding error:nil] componentsSeparatedByString:@"\n"]; fileName = nil; bool needsRebuild = false; for (NSString *path in paths) { NSURL *url = [NSURL URLWithString:path relativeToURL:self.fileURL]; if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) { if (fileName && ![fileName isEqualToString:url.path]) { needsRebuild = true; break; } fileName = url.path; } else { needsRebuild = true; } } if (fileName && needsRebuild) { [[NSString stringWithFormat:@"%@\n%@\n%@", [fileName pathRelativeToDirectory:self.fileName], fileName, [[NSURL fileURLWithPath:fileName].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")]] writeToFile:[self.fileName stringByAppendingPathComponent:@"rom.gbl"] atomically:false encoding:NSUTF8StringEncoding error:nil]; } } return fileName; } static bool is_path_writeable(const char *path) { if (!access(path, W_OK)) return true; int fd = creat(path, 0644); if (fd == -1) return false; close(fd); unlink(path); return true; } - (int)loadROM { __block int ret = 0; NSString *fileName = self.romPath; if (!fileName) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Could not locate the ROM referenced by this Game Boy Cartridge"]; [alert setAlertStyle:NSAlertStyleCritical]; [alert runModal]; return 1; } NSString *rom_warnings = [self captureOutputForBlock:^{ GB_debugger_clear_symbols(&_gb); if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"isx"]) { ret = GB_load_isx(&_gb, fileName.UTF8String); if (!self.isCartContainer) { GB_load_battery(&_gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"ram"].path.UTF8String); } } else if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"gbs"]) { __block GB_gbs_info_t info; ret = GB_load_gbs(&_gb, fileName.UTF8String, &info); [self prepareGBSInterface:&info]; } else { ret = GB_load_rom(&_gb, [fileName UTF8String]); } if (GB_save_battery_size(&_gb)) { if (!is_path_writeable(self.savPath.UTF8String)) { GB_log(&_gb, "The save path for this ROM is not writeable, progress will not be saved.\n"); } } GB_load_battery(&_gb, self.savPath.UTF8String); GB_load_cheats(&_gb, self.chtPath.UTF8String, true); dispatch_async(dispatch_get_main_queue(), ^{ [self.cheatWindowController cheatsUpdated]; }); GB_debugger_load_symbol_file(&_gb, [[[NSBundle mainBundle] pathForResource:@"registers" ofType:@"sym"] UTF8String]); GB_debugger_load_symbol_file(&_gb, [[fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sym"].UTF8String); }]; if (ret) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:rom_warnings?: @"Could not load ROM"]; [alert setAlertStyle:NSAlertStyleCritical]; [alert runModal]; } else if (rom_warnings && !_romWarningIssued) { _romWarningIssued = true; [GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow]; } _fileModificationTime = [[NSFileManager defaultManager] attributesOfItemAtPath:fileName error:nil][NSFileModificationDate]; if (_usesAutoModel) { _currentModel = [self bestModelForROM]; } return ret; } - (void)showWindows { if (GB_is_inited(&_gb)) { if (![_fileModificationTime isEqualToDate:[[NSFileManager defaultManager] attributesOfItemAtPath:self.fileName error:nil][NSFileModificationDate]]) { [self reset:nil]; } } [super showWindows]; } - (void)close { [self disconnectLinkCable]; if (!self.gbsPlayerView) { [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"]; [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"]; } [self stop]; [_consoleOutputLock lock]; [_consoleOutputTimer invalidate]; [_consoleOutputLock unlock]; [self.consoleWindow close]; [self.memoryWindow close]; [self.vramWindow close]; [self.printerFeedWindow close]; [self.cheatsWindow close]; [_cheatSearchController.window close]; [super close]; } - (IBAction) interrupt:(id)sender { [self log:"^C\n"]; GB_debugger_break(&_gb); [self start]; [self.consoleWindow makeKeyAndOrderFront:nil]; double secondUsage = GB_debugger_get_second_cpu_usage(&_gb); _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100]; [self.consoleInput becomeFirstResponder]; } - (IBAction)mute:(id)sender { if (_audioClient.isPlaying) { [_audioClient stop]; } else { [_audioClient start]; if (_volume == 0) { [GBWarningPopover popoverWithContents:@"Warning: Volume is set to to zero in the preferences panel" onWindow:self.mainWindow]; } } [[NSUserDefaults standardUserDefaults] setBool:!_audioClient.isPlaying forKey:@"Mute"]; } - (bool) isPaused { if (self.partner) { return !self.partner->_running || GB_debugger_is_stopped(&_gb) || GB_debugger_is_stopped(&self.partner->_gb); } return (!_running) || GB_debugger_is_stopped(&_gb); } - (BOOL)validateUserInterfaceItem:(id)anItem { if ([anItem action] == @selector(mute:)) { if (_running) { [(NSMenuItem *)anItem setState:!_audioClient.isPlaying]; } else { [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]]; } } else if ([anItem action] == @selector(togglePause:)) { [(NSMenuItem *)anItem setState:self.isPaused]; return !GB_debugger_is_stopped(&_gb); } else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE && anItem.tag != MODEL_QUICK_RESET) { [(NSMenuItem *)anItem setState:(anItem.tag == _currentModel) || (anItem.tag == MODEL_AUTO && _usesAutoModel)]; } else if ([anItem action] == @selector(interrupt:)) { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { return false; } } else if ([anItem action] == @selector(disconnectAllAccessories:)) { [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_NONE && !self.partner]; } else if ([anItem action] == @selector(connectPrinter:)) { [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_PRINTER]; } else if ([anItem action] == @selector(connectWorkboy:)) { [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_WORKBOY]; } else if ([anItem action] == @selector(connectLinkCable:)) { [(NSMenuItem *)anItem setState:[(NSMenuItem *)anItem representedObject] == _master || [(NSMenuItem *)anItem representedObject] == _slave]; } else if ([anItem action] == @selector(toggleCheats:)) { [(NSMenuItem *)anItem setState:GB_cheats_enabled(&_gb)]; } else if ([anItem action] == @selector(toggleDisplayBackground:)) { [(NSMenuItem *)anItem setState:!GB_is_background_rendering_disabled(&_gb)]; } else if ([anItem action] == @selector(toggleDisplayObjects:)) { [(NSMenuItem *)anItem setState:!GB_is_object_rendering_disabled(&_gb)]; } else if ([anItem action] == @selector(toggleAudioRecording:)) { [(NSMenuItem *)anItem setTitle:_isRecordingAudio? @"Stop Audio Recording" : @"Start Audio Recording…"]; } else if ([anItem action] == @selector(toggleAudioChannel:)) { [(NSMenuItem *)anItem setState:!GB_is_channel_muted(&_gb, [anItem tag])]; } else if ([anItem action] == @selector(increaseWindowSize:)) { return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionIncrease]; } else if ([anItem action] == @selector(decreaseWindowSize:)) { return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionDecrease]; } else if ([anItem action] == @selector(reloadROM:)) { return !_gbsTracks; } return [super validateUserInterfaceItem:anItem]; } - (void) windowWillEnterFullScreen:(NSNotification *)notification { _fullScreen = true; self.view.mouseHidingEnabled = _running; } - (void) windowWillExitFullScreen:(NSNotification *)notification { _fullScreen = false; self.view.mouseHidingEnabled = false; } enum GBWindowResizeAction { GBWindowResizeActionZoom, GBWindowResizeActionIncrease, GBWindowResizeActionDecrease, }; - (bool)newRect:(NSRect *)rect forWindow:(NSWindow *)window action:(enum GBWindowResizeAction)action { if (_fullScreen) return false; if (!rect) { rect = alloca(sizeof(*rect)); } size_t width = GB_get_screen_width(&_gb), height = GB_get_screen_height(&_gb); *rect = window.contentView.frame; unsigned titlebarSize = window.contentView.superview.frame.size.height - rect->size.height; unsigned stepX = width / [[window screen] backingScaleFactor]; unsigned stepY = height / [[window screen] backingScaleFactor]; if (action == GBWindowResizeActionDecrease) { if (rect->size.width <= width || rect->size.height <= height) { return false; } } typeof(floor) *roundFunc = action == GBWindowResizeActionDecrease? ceil : floor; unsigned currentFactor = MIN(roundFunc(rect->size.width / stepX), roundFunc(rect->size.height / stepY)); rect->size.width = currentFactor * stepX; rect->size.height = currentFactor * stepY + titlebarSize; if (action == GBWindowResizeActionDecrease) { rect->size.width -= stepX; rect->size.height -= stepY; } else { rect->size.width += stepX; rect->size.height += stepY; } NSRect maxRect = [_mainWindow screen].visibleFrame; if (rect->size.width > maxRect.size.width || rect->size.height > maxRect.size.height) { if (action == GBWindowResizeActionIncrease) { return false; } rect->size.width = width; rect->size.height = height + titlebarSize; } rect->origin = window.frame.origin; if (action == GBWindowResizeActionZoom) { rect->origin.y -= rect->size.height - window.frame.size.height; } else { rect->origin.y -= (rect->size.height - window.frame.size.height) / 2; rect->origin.x -= (rect->size.width - window.frame.size.width) / 2; } if (rect->origin.x < maxRect.origin.x) { rect->origin.x = maxRect.origin.x; } if (rect->origin.y < maxRect.origin.y) { rect->origin.y = maxRect.origin.y; } if (rect->origin.x + rect->size.width > maxRect.origin.x + maxRect.size.width) { rect->origin.x = maxRect.origin.x + maxRect.size.width - rect->size.width; } if (rect->origin.y + rect->size.height > maxRect.origin.y + maxRect.size.height) { rect->origin.y = maxRect.origin.y + maxRect.size.height - rect->size.height; } return true; } - (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame { if (_fullScreen) { return newFrame; } [self newRect:&newFrame forWindow:window action:GBWindowResizeActionZoom]; return newFrame; } - (IBAction)increaseWindowSize:(id)sender { NSRect rect; if ([self newRect:&rect forWindow:_mainWindow action:GBWindowResizeActionIncrease]) { [_mainWindow setFrame:rect display:true animate:true]; } } - (IBAction)decreaseWindowSize:(id)sender { NSRect rect; if ([self newRect:&rect forWindow:_mainWindow action:GBWindowResizeActionDecrease]) { [_mainWindow setFrame:rect display:true animate:true]; } } - (void) appendPendingOutput { [_consoleOutputLock lock]; if (_shouldClearSideView) { _shouldClearSideView = false; [self.debuggerSideView setString:@""]; } if (_pendingConsoleOutput) { NSTextView *textView = _logToSideView? self.debuggerSideView : self.consoleOutput; [_hexController reloadData]; [self reloadVRAMData: nil]; [textView.textStorage appendAttributedString:_pendingConsoleOutput]; if (!_logToSideView) { [textView scrollToEndOfDocument:nil]; } if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { [self.consoleWindow orderFront:nil]; } _pendingConsoleOutput = nil; } [_consoleOutputLock unlock]; } - (void)log:(const char *)string withAttributes:(GB_log_attributes_t)attributes { NSString *nsstring = @(string); // For ref-counting if (_capturedOutput) { [_capturedOutput appendString:nsstring]; return; } NSFont *font = [self debuggerFontOfSize:0]; NSUnderlineStyle underline = NSUnderlineStyleNone; if (attributes & GB_LOG_BOLD) { font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask]; } if (attributes & GB_LOG_UNDERLINE_MASK) { underline = (attributes & GB_LOG_UNDERLINE_MASK) == GB_LOG_DASHED_UNDERLINE? NSUnderlinePatternDot | NSUnderlineStyleSingle : NSUnderlineStyleSingle; } NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init]; [paragraph_style setLineSpacing:2]; NSMutableAttributedString *attributed = [[NSMutableAttributedString alloc] initWithString:nsstring attributes:@{NSFontAttributeName: font, NSForegroundColorAttributeName: [NSColor whiteColor], NSUnderlineStyleAttributeName: @(underline), NSParagraphStyleAttributeName: paragraph_style}]; [_consoleOutputLock lock]; if (!_pendingConsoleOutput) { _pendingConsoleOutput = attributed; } else { [_pendingConsoleOutput appendAttributedString:attributed]; } if (![_consoleOutputTimer isValid]) { _consoleOutputTimer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:false]; [[NSRunLoop mainRunLoop] addTimer:_consoleOutputTimer forMode:NSDefaultRunLoopMode]; } [_consoleOutputLock unlock]; /* Make sure mouse is not hidden while debugging */ self.view.mouseHidingEnabled = false; } - (IBAction)showConsoleWindow:(id)sender { [self.consoleWindow orderFront:nil]; double secondUsage = GB_debugger_get_second_cpu_usage(&_gb); _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100]; } - (void)queueDebuggerCommand:(NSString *)command { if (!_master && !_running && !GB_debugger_is_stopped(&_gb)) { _debuggerCommandWhilePaused = command; GB_debugger_break(&_gb); [self start]; return; } if (!_inSyncInput) { [self log:">"]; } [self log:[command UTF8String]]; [self log:"\n"]; [_hasDebuggerInput lock]; [_debuggerInputQueue addObject:command]; [_hasDebuggerInput unlockWithCondition:1]; } - (IBAction)consoleInput:(NSTextField *)sender { NSString *line = [sender stringValue]; if ([line isEqualToString:@""] && _lastConsoleInput) { line = _lastConsoleInput; } else if (line) { _lastConsoleInput = line; } else { line = @""; } [self queueDebuggerCommand: line]; [sender setStringValue:@""]; } - (void) interruptDebugInputRead { [_hasDebuggerInput lock]; [_debuggerInputQueue addObject:[NSNull null]]; [_hasDebuggerInput unlockWithCondition:1]; } - (void) updateSideView { if (!GB_debugger_is_stopped(&_gb)) { return; } if (![NSThread isMainThread]) { dispatch_sync(dispatch_get_main_queue(), ^{ [self updateSideView]; }); return; } [_consoleOutputLock lock]; _shouldClearSideView = true; [self appendPendingOutput]; _logToSideView = true; [_consoleOutputLock unlock]; for (NSString *line in [self.debuggerSideViewInput.string componentsSeparatedByString:@"\n"]) { NSString *stripped = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([stripped length]) { char *dupped = strdup([stripped UTF8String]); GB_attributed_log(&_gb, GB_LOG_BOLD, "%s:\n", dupped); GB_debugger_execute_command(&_gb, dupped); GB_log(&_gb, "\n"); free(dupped); } } [_consoleOutputLock lock]; [self appendPendingOutput]; _logToSideView = false; [_consoleOutputLock unlock]; } - (char *)getDebuggerInput { bool isPlaying = _audioClient.isPlaying; if (isPlaying) { [_audioClient stop]; } [_audioLock lock]; [_audioLock signal]; [_audioLock unlock]; _inSyncInput = true; [self updateSideView]; dispatch_async(dispatch_get_main_queue(), ^{ [self updateDebuggerButtons]; }); [self.partner updateDebuggerButtons]; [self log:">"]; if (_debuggerCommandWhilePaused) { NSString *command = _debuggerCommandWhilePaused; _debuggerCommandWhilePaused = nil; dispatch_async(dispatch_get_main_queue(), ^{ [self queueDebuggerCommand:command]; }); } [_hasDebuggerInput lockWhenCondition:1]; NSString *input = [_debuggerInputQueue firstObject]; [_debuggerInputQueue removeObjectAtIndex:0]; [_hasDebuggerInput unlockWithCondition:[_debuggerInputQueue count] != 0]; _inSyncInput = false; _shouldClearSideView = true; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC / 10)), dispatch_get_main_queue(), ^{ if (_shouldClearSideView) { _shouldClearSideView = false; [self.debuggerSideView setString:@""]; } [self updateDebuggerButtons]; [self.partner updateDebuggerButtons]; }); if (isPlaying) { [_audioClient start]; } if ((id) input == [NSNull null]) { return NULL; } return strdup([input UTF8String]); } - (char *) getAsyncDebuggerInput { [_hasDebuggerInput lock]; NSString *input = [_debuggerInputQueue firstObject]; if (input) { [_debuggerInputQueue removeObjectAtIndex:0]; } [_hasDebuggerInput unlockWithCondition:[_debuggerInputQueue count] != 0]; if ((id)input == [NSNull null]) { return NULL; } return input? strdup([input UTF8String]): NULL; } - (IBAction)saveState:(id)sender { bool __block success = false; [self performAtomicBlock:^{ success = GB_save_state(&_gb, [self saveStatePath:[sender tag]].UTF8String) == 0; }]; if (!success) { [GBWarningPopover popoverWithContents:@"Failed to write save state." onWindow:self.mainWindow]; NSBeep(); } else { [self.osdView displayText:@"State saved"]; } } - (int)loadStateFile:(const char *)path noErrorOnNotFound:(bool)noErrorOnFileNotFound; { int __block result = false; NSString *error = [self captureOutputForBlock:^{ result = GB_load_state(&_gb, path); }]; if (result == ENOENT && noErrorOnFileNotFound) { return ENOENT; } if (result) { NSBeep(); } else { [self.osdView displayText:@"State loaded"]; } if (error) { [GBWarningPopover popoverWithContents:error onWindow:self.mainWindow]; } return result; } - (IBAction)loadState:(id)sender { int ret = [self loadStateFile:[self saveStatePath:[sender tag]].UTF8String noErrorOnNotFound:true]; if (ret == ENOENT && !self.isCartContainer) { [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"sn%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:false]; } } - (IBAction)clearConsole:(id)sender { [self.consoleOutput setString:@""]; } - (void)log:(const char *)log { [self log:log withAttributes:0]; } - (void)performAtomicBlock: (void (^)())block { while (!GB_is_inited(&_gb)); bool isRunning = _running && !GB_debugger_is_stopped(&_gb); if (_master) { isRunning |= _master->_running; } if (!isRunning) { block(); return; } if (_master) { [_master performAtomicBlock:block]; return; } if ([NSThread currentThread] == _emulationThread) { block(); return; } _pendingAtomicBlock = block; while (_pendingAtomicBlock); } - (NSString *)captureOutputForBlock: (void (^)())block { _capturedOutput = [[NSMutableString alloc] init]; [self performAtomicBlock:block]; NSString *ret = [_capturedOutput stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; _capturedOutput = nil; return [ret length]? ret : nil; } + (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale { NSImage *ret = [[NSImage alloc] initWithSize:NSMakeSize(width * scale, height * scale)]; NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:width pixelsHigh:height bitsPerSample:8 samplesPerPixel:3 hasAlpha:false isPlanar:false colorSpaceName:NSDeviceRGBColorSpace bitmapFormat:0 bytesPerRow:4 * width bitsPerPixel:32]; memcpy(rep.bitmapData, data.bytes, data.length); [ret addRepresentation:rep]; return ret; } - (void) reloadMemoryView { if (self.memoryWindow.isVisible) { [_hexController reloadData]; } if (_cheatSearchController.window.isVisible) { if ([_cheatSearchController.tableView editedColumn] != 2) { [_cheatSearchController.tableView reloadData]; } } } - (IBAction) reloadVRAMData: (id) sender { if (self.vramWindow.isVisible) { uint8_t *io_regs = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_IO, NULL, NULL); switch ([self.vramTabView.tabViewItems indexOfObject:self.vramTabView.selectedTabViewItem]) { case 0: /* Tileset */ { GB_palette_type_t palette_type = GB_PALETTE_NONE; NSUInteger palette_menu_index = self.tilesetPaletteButton.indexOfSelectedItem; if (palette_menu_index) { palette_type = palette_menu_index > 8? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND; } size_t bufferLength = 256 * 192 * 4; NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength]; data.length = bufferLength; GB_draw_tileset(&_gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 1) & 7); self.tilesetImageView.image = [Document imageFromData:data width:256 height:192 scale:1.0]; self.tilesetImageView.layer.magnificationFilter = kCAFilterNearest; } break; case 1: /* Tilemap */ { GB_palette_type_t palette_type = GB_PALETTE_NONE; NSUInteger palette_menu_index = self.tilemapPaletteButton.indexOfSelectedItem; if (palette_menu_index > 1) { palette_type = palette_menu_index > 9? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND; } else if (palette_menu_index == 1) { palette_type = GB_PALETTE_AUTO; } size_t bufferLength = 256 * 256 * 4; NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength]; data.length = bufferLength; GB_draw_tilemap(&_gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 2) & 7, (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem, (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem); self.tilemapImageView.scrollRect = NSMakeRect(io_regs[GB_IO_SCX], io_regs[GB_IO_SCY], 160, 144); self.tilemapImageView.image = [Document imageFromData:data width:256 height:256 scale:1.0]; self.tilemapImageView.layer.magnificationFilter = kCAFilterNearest; } break; case 2: /* OAM */ { _oamCount = GB_get_oam_info(&_gb, _oamInfo, &_oamHeight); dispatch_async(dispatch_get_main_queue(), ^{ [self.objectView reloadData:self]; }); } break; case 3: /* Palettes */ { dispatch_async(dispatch_get_main_queue(), ^{ [self.paletteView reloadData:self]; }); } break; } } } - (IBAction) showMemory:(id)sender { if (!_hexController) { [self initMemoryView]; } [self.memoryWindow makeKeyAndOrderFront:sender]; } - (IBAction)hexGoTo:(id)sender { NSString *expression = [sender stringValue]; __block uint16_t addr = 0; __block uint16_t bank = 0; __block bool fail = false; NSString *error = [self captureOutputForBlock:^{ if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) { fail = true; } }]; if (error) { NSBeep(); [GBWarningPopover popoverWithContents:error onView:sender]; } if (fail) return; if (bank != (typeof(bank))-1) { GB_memory_mode_t mode = [(GBMemoryByteArray *)(_hexController.byteArray) mode]; if (addr < 0x4000) { if (bank == 0) { if (mode != GBMemoryROM && mode != GBMemoryEntireSpace) { mode = GBMemoryEntireSpace; } } else { addr |= 0x4000; mode = GBMemoryROM; } } else if (addr < 0x8000) { mode = GBMemoryROM; } else if (addr < 0xA000) { mode = GBMemoryVRAM; } else if (addr < 0xC000) { mode = GBMemoryExternalRAM; } else if (addr < 0xD000) { if (mode != GBMemoryRAM && mode != GBMemoryEntireSpace) { mode = GBMemoryEntireSpace; } } else if (addr < 0xE000) { mode = GBMemoryRAM; } else { mode = GBMemoryEntireSpace; } [_memorySpaceButton selectItemAtIndex:mode]; [self hexUpdateSpace:_memorySpaceButton.cell]; [_memoryBankInput setStringValue:[NSString stringWithFormat:@"$%02x", bank]]; [self hexUpdateBank:_memoryBankInput]; } addr -= _lineRep.valueOffset; if (addr >= _hexController.byteArray.length) { GB_log(&_gb, "Value $%04x is out of range.\n", addr); return; } [_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]]; [_hexController _ensureVisibilityOfLocation:addr]; for (HFRepresenter *representer in _hexController.representers) { if ([representer isKindOfClass:[HFHexTextRepresenter class]]) { [self.memoryWindow makeFirstResponder:representer.view]; break; } } } - (void)hexUpdateBank:(NSControl *)sender ignoreErrors: (bool)ignore_errors { NSString *expression = [sender stringValue]; __block uint16_t addr, bank; __block bool fail = false; NSString *error = [self captureOutputForBlock:^{ if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) { fail = true; return; } }]; if (error && !ignore_errors) { NSBeep(); [GBWarningPopover popoverWithContents:error onView:sender]; } if (fail) return; if (bank == (uint16_t) -1) { bank = addr; } uint16_t n_banks = 1; switch ([(GBMemoryByteArray *)(_hexController.byteArray) mode]) { case GBMemoryROM: { size_t rom_size; GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL); n_banks = rom_size / 0x4000; break; } case GBMemoryVRAM: n_banks = GB_is_cgb(&_gb) ? 2 : 1; break; case GBMemoryExternalRAM: { size_t ram_size; GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL); n_banks = (ram_size + 0x1FFF) / 0x2000; break; } case GBMemoryRAM: n_banks = GB_is_cgb(&_gb) ? 8 : 1; break; case GBMemoryEntireSpace: break; } bank %= n_banks; [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:bank]; _statusRep.bankForDescription = bank; [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]]; [_hexController reloadData]; } - (IBAction)hexUpdateBank:(NSControl *)sender { [self hexUpdateBank:sender ignoreErrors:false]; } - (IBAction)hexUpdateSpace:(NSPopUpButtonCell *)sender { self.memoryBankItem.enabled = [sender indexOfSelectedItem] != GBMemoryEntireSpace; [_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(0, 0)]]]; GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(_hexController.byteArray); [byteArray setMode:(GB_memory_mode_t)[sender indexOfSelectedItem]]; uint16_t bank = -1; switch ((GB_memory_mode_t)[sender indexOfSelectedItem]) { case GBMemoryEntireSpace: _statusRep.baseAddress = _lineRep.valueOffset = 0; break; case GBMemoryROM: _statusRep.baseAddress = _lineRep.valueOffset = 0; GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, &bank); break; case GBMemoryVRAM: _statusRep.baseAddress = _lineRep.valueOffset = 0x8000; GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_VRAM, NULL, &bank); break; case GBMemoryExternalRAM: _statusRep.baseAddress = _lineRep.valueOffset = 0xA000; GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, NULL, &bank); break; case GBMemoryRAM: _statusRep.baseAddress = _lineRep.valueOffset = 0xC000; GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_RAM, NULL, &bank); break; } byteArray.selectedBank = bank; _statusRep.bankForDescription = bank; [self.memoryBankInput setStringValue:(bank == (uint16_t)-1)? @"" : [NSString stringWithFormat:@"$%x", byteArray.selectedBank]]; [_hexController reloadData]; for (NSView *view in self.memoryView.subviews) { [view setNeedsDisplay:true]; } } - (GB_gameboy_t *) gameboy { return &_gb; } + (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName { return true; } - (void)cameraRequestUpdate { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @try { if (!_cameraSession) { if (@available(macOS 10.14, *)) { switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) { case AVAuthorizationStatusAuthorized: break; case AVAuthorizationStatusNotDetermined: { [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { [self cameraRequestUpdate]; }]; return; } case AVAuthorizationStatusDenied: case AVAuthorizationStatusRestricted: GB_camera_updated(&_gb); return; } } NSError *error; AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error]; CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions([[device activeFormat] formatDescription]); if (!input) { GB_camera_updated(&_gb); return; } double ratio = MAX(130.0 / dimensions.width, 114.0 / dimensions.height); _cameraOutput = [[AVCaptureStillImageOutput alloc] init]; /* Greyscale is not widely supported, so we use YUV, whose first element is the brightness. */ [_cameraOutput setOutputSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kYUVSPixelFormat), (id)kCVPixelBufferWidthKey: @(round(dimensions.width * ratio)), (id)kCVPixelBufferHeightKey: @(round(dimensions.height * ratio)),}]; _cameraSession = [AVCaptureSession new]; _cameraSession.sessionPreset = AVCaptureSessionPresetPhoto; [_cameraSession addInput: input]; [_cameraSession addOutput: _cameraOutput]; [_cameraSession startRunning]; _cameraConnection = [_cameraOutput connectionWithMediaType: AVMediaTypeVideo]; } [_cameraOutput captureStillImageAsynchronouslyFromConnection: _cameraConnection completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError *error) { if (error) { GB_camera_updated(&_gb); } else { if (_cameraImage) { CVBufferRelease(_cameraImage); _cameraImage = NULL; } _cameraImage = CVBufferRetain(CMSampleBufferGetImageBuffer(sampleBuffer)); /* We only need the actual buffer, no need to ever unlock it. */ CVPixelBufferLockBaseAddress(_cameraImage, 0); } GB_camera_updated(&_gb); }]; } @catch (NSException *exception) { /* I have not tested camera support on many devices, so we catch exceptions just in case. */ GB_camera_updated(&_gb); } }); } - (uint8_t)cameraGetPixelAtX:(unsigned)x andY:(unsigned)y { if (!_cameraImage) { return 0; } uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(_cameraImage); size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_cameraImage); unsigned offsetX = (CVPixelBufferGetWidth(_cameraImage) - 128) / 2; unsigned offsetY = (CVPixelBufferGetHeight(_cameraImage) - 112) / 2; uint8_t ret = baseAddress[(x + offsetX) * 2 + (y + offsetY) * bytesPerRow]; return ret; } - (IBAction)toggleTilesetGrid:(NSButton *)sender { if (sender.state) { self.tilesetImageView.horizontalGrids = @[ [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8], [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:128], ]; self.tilesetImageView.verticalGrids = @[ [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8], [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:64], ]; self.tilemapImageView.horizontalGrids = @[ [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8], ]; self.tilemapImageView.verticalGrids = @[ [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8], ]; } else { self.tilesetImageView.horizontalGrids = nil; self.tilesetImageView.verticalGrids = nil; self.tilemapImageView.horizontalGrids = nil; self.tilemapImageView.verticalGrids = nil; } } - (IBAction)toggleScrollingDisplay:(NSButton *)sender { self.tilemapImageView.displayScrollRect = sender.state; } - (IBAction)vramTabChanged:(NSSegmentedControl *)sender { [self.vramTabView selectTabViewItemAtIndex:[sender selectedSegment]]; [self reloadVRAMData:sender]; [self.vramTabView.selectedTabViewItem.view addSubview:self.gridButton]; self.gridButton.hidden = [sender selectedSegment] >= 2; NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height; CGRect window_rect = self.vramWindow.frame; window_rect.origin.y += window_rect.size.height; switch ([sender selectedSegment]) { case 0: case 2: window_rect.size.height = 384 + height_diff + 48; break; case 1: window_rect.size.height = 512 + height_diff + 48; break; case 3: window_rect.size.height = 24 * 16 + height_diff; break; default: break; } window_rect.origin.y -= window_rect.size.height; [self.vramWindow setFrame:window_rect display:true animate:true]; } - (void)mouseDidLeaveImageView:(GBImageView *)view { self.vramStatusLabel.stringValue = @""; } - (void)imageView:(GBImageView *)view mouseMovedToX:(NSUInteger)x Y:(NSUInteger)y { if (view == self.tilesetImageView) { uint8_t bank = x >= 128? 1 : 0; x &= 127; uint16_t tile = x / 8 + y / 8 * 16; self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10]; } else if (view == self.tilemapImageView) { uint16_t map_offset = x / 8 + y / 8 * 32; uint16_t map_base = 0x1800; GB_map_type_t map_type = (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem; GB_tileset_type_t tileset_type = (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem; uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC]; uint8_t *vram = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL); if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & GB_LCDC_BG_MAP)) { map_base = 0x1C00; } if (tileset_type == GB_TILESET_AUTO) { tileset_type = (lcdc & GB_LCDC_TILE_SEL)? GB_TILESET_8800 : GB_TILESET_8000; } uint8_t tile = vram[map_base + map_offset]; uint16_t tile_address = 0; if (tileset_type == GB_TILESET_8000) { tile_address = 0x8000 + tile * 0x10; } else { tile_address = 0x9000 + (int8_t)tile * 0x10; } if (GB_is_cgb(&_gb)) { uint8_t attributes = vram[map_base + map_offset + 0x2000]; self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)", tile, attributes & 0x8? 1 : 0, tile_address, 0x8000 + map_base + map_offset, (attributes & 0x80) ? 'P' : '-', (attributes & 0x40) ? 'V' : '-', (attributes & 0x20) ? 'H' : '-', attributes & 0x8? 1 : 0, attributes & 0x7 ]; } else { self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x ($%04x) at map address $%04x", tile, tile_address, 0x8000 + map_base + map_offset ]; } } } - (GB_oam_info_t *)oamInfo { return _oamInfo; } - (IBAction)showVRAMViewer:(id)sender { [self.vramWindow makeKeyAndOrderFront:sender]; [self reloadVRAMData: nil]; } - (void)printImage:(uint32_t *)imageBytes height:(unsigned) height topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin exposure:(unsigned) exposure { uint32_t paddedImage[160 * (topMargin + height + bottomMargin)]; memset(paddedImage, 0xFF, sizeof(paddedImage)); memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0])); if (!self.printerFeedWindow.isVisible) { _currentPrinterImageData = [[NSMutableData alloc] init]; } [_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)]; /* UI related code must run on main thread. */ dispatch_async(dispatch_get_main_queue(), ^{ [_printerSpinner startAnimation:nil]; self.feedImageView.image = [Document imageFromData:_currentPrinterImageData width:160 height:_currentPrinterImageData.length / 160 / sizeof(imageBytes[0]) scale:2.0]; NSRect frame = self.printerFeedWindow.frame; double oldHeight = frame.size.height; frame.size = self.feedImageView.image.size; [self.printerFeedWindow setContentMaxSize:frame.size]; frame.size.height += self.printerFeedWindow.frame.size.height - self.printerFeedWindow.contentView.frame.size.height; frame.origin.y -= frame.size.height - oldHeight; [self.printerFeedWindow setFrame:frame display:false animate: self.printerFeedWindow.isVisible]; [self.printerFeedWindow orderFront:NULL]; }); } - (void)printDone { dispatch_async(dispatch_get_main_queue(), ^{ [_printerSpinner stopAnimation:nil]; }); } - (void)printDocument:(id)sender { if (self.feedImageView.image.size.height == 0) { NSBeep(); return; } NSImageView *view = [[NSImageView alloc] initWithFrame:(NSRect){{0,0}, self.feedImageView.image.size}]; view.image = self.feedImageView.image; [[NSPrintOperation printOperationWithView:view] runOperationModalForWindow:self.printerFeedWindow delegate:nil didRunSelector:NULL contextInfo:NULL]; } - (IBAction)savePrinterFeed:(id)sender { bool shouldResume = _running; [self stop]; NSSavePanel *savePanel = [NSSavePanel savePanel]; [savePanel setAllowedFileTypes:@[@"png"]]; [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [savePanel orderOut:self]; CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL context:nil hints:nil]; NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef]; [imageRep setSize:(NSSize){160, self.feedImageView.image.size.height / 2}]; NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; [data writeToURL:savePanel.URL atomically:false]; [self.printerFeedWindow setIsVisible:false]; } if (shouldResume) { [self start]; } }]; } - (IBAction)disconnectAllAccessories:(id)sender { [self disconnectLinkCable]; [self performAtomicBlock:^{ GB_disconnect_serial(&_gb); }]; } - (IBAction)connectPrinter:(id)sender { [self disconnectLinkCable]; [self performAtomicBlock:^{ GB_connect_printer(&_gb, printImage, printDone); }]; } - (IBAction)connectWorkboy:(id)sender { [self disconnectLinkCable]; [self performAtomicBlock:^{ GB_connect_workboy(&_gb, setWorkboyTime, getWorkboyTime); }]; } - (void)setFileURL:(NSURL *)fileURL { [super setFileURL:fileURL]; if (@available(macOS 11.0, *)) { self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent]; self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent]; self.vramWindow.subtitle = [self.fileURL.path lastPathComponent]; } else { self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]]; self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [self.fileURL.path lastPathComponent]]; self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [self.fileURL.path lastPathComponent]]; } } - (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview; { if ([[splitView arrangedSubviews] lastObject] == subview) { return true; } return false; } - (CGFloat)splitView:(GBSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex { return 600; } - (CGFloat)splitView:(GBSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex { return splitView.frame.size.width - 321; } - (BOOL)splitView:(GBSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view { if ([[splitView arrangedSubviews] lastObject] == view) { return false; } return true; } - (void)splitViewDidResizeSubviews:(NSNotification *)notification { GBSplitView *splitview = notification.object; if ([[[splitview arrangedSubviews] firstObject] frame].size.width < 600) { [splitview setPosition:600 ofDividerAtIndex:0]; } } - (IBAction)showCheats:(id)sender { [self.cheatsWindow makeKeyAndOrderFront:nil]; } - (IBAction)showCheatSearch:(id)sender { if (!_cheatSearchController) { _cheatSearchController = [GBCheatSearchController controllerWithDocument:self]; } [_cheatSearchController.window makeKeyAndOrderFront:sender]; } - (IBAction)toggleCheats:(id)sender { GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb)); } - (void)disconnectLinkCable { bool wasRunning = self->_running; Document *partner = _master ?: _slave; if (partner) { wasRunning |= partner->_running; [self stop]; partner->_master = nil; partner->_slave = nil; _master = nil; _slave = nil; if (wasRunning) { [partner start]; [self start]; } GB_set_turbo_mode(&_gb, false, false); GB_set_turbo_mode(&partner->_gb, false, false); GB_set_turbo_cap(&_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]); GB_set_turbo_cap(&partner->_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]); } } - (void)connectLinkCable:(NSMenuItem *)sender { [self disconnectAllAccessories:sender]; Document *partner = [sender representedObject]; [partner disconnectAllAccessories:sender]; bool wasRunning = self->_running; [self stop]; [partner stop]; GB_set_turbo_mode(&partner->_gb, true, true); _slave = partner; partner->_master = self; GB_set_turbo_cap(&partner->_gb, 0); _linkOffset = 0; GB_set_serial_transfer_bit_start_callback(&_gb, _linkCableBitStart); GB_set_serial_transfer_bit_start_callback(&partner->_gb, _linkCableBitStart); GB_set_serial_transfer_bit_end_callback(&_gb, _linkCableBitEnd); GB_set_serial_transfer_bit_end_callback(&partner->_gb, _linkCableBitEnd); if (wasRunning) { [self start]; } } - (void)_linkCableBitStart:(bool)bit { _linkCableBit = bit; } -(bool)_linkCableBitEnd { bool ret = GB_serial_get_data_bit(&self.partner->_gb); GB_serial_set_data_bit(&self.partner->_gb, _linkCableBit); return ret; } - (void)infraredStateChanged:(bool)state { if (self.partner) { GB_set_infrared_input(&self.partner->_gb, state); } } -(Document *)partner { return _slave ?: _master; } - (bool)isSlave { return _master; } - (GB_gameboy_t *)gb { return &_gb; } - (NSImage *)takeScreenshot { NSImage *ret = nil; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]) { ret = [_view renderToImage]; } if (!ret) { ret = [Document imageFromData:[NSData dataWithBytesNoCopy:_view.currentBuffer length:GB_get_screen_width(&_gb) * GB_get_screen_height(&_gb) * 4 freeWhenDone:false] width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb) scale:1.0]; } return ret; } - (NSString *)screenshotFilename { NSDate *date = [NSDate date]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.dateStyle = NSDateFormatterLongStyle; dateFormatter.timeStyle = NSDateFormatterMediumStyle; return [[NSString stringWithFormat:@"%@ – %@.png", self.fileURL.lastPathComponent.stringByDeletingPathExtension, [dateFormatter stringFromDate:date]] stringByReplacingOccurrencesOfString:@":" withString:@"."]; // Gotta love Mac OS Classic } - (IBAction)saveScreenshot:(id)sender { NSString *folder = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBScreenshotFolder"]; BOOL isDirectory = false; if (folder) { [[NSFileManager defaultManager] fileExistsAtPath:folder isDirectory:&isDirectory]; } if (!folder) { bool shouldResume = _running; [self stop]; NSOpenPanel *openPanel = [NSOpenPanel openPanel]; openPanel.canChooseFiles = false; openPanel.canChooseDirectories = true; openPanel.message = @"Choose a folder for screenshots"; [openPanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [[NSUserDefaults standardUserDefaults] setObject:openPanel.URL.path forKey:@"GBScreenshotFolder"]; [self saveScreenshot:sender]; } if (shouldResume) { [self start]; } }]; return; } NSImage *image = [self takeScreenshot]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.dateStyle = NSDateFormatterLongStyle; dateFormatter.timeStyle = NSDateFormatterMediumStyle; NSString *filename = [self screenshotFilename]; filename = [folder stringByAppendingPathComponent:filename]; unsigned i = 2; while ([[NSFileManager defaultManager] fileExistsAtPath:filename]) { filename = [[filename stringByDeletingPathExtension] stringByAppendingFormat:@" %d.png", i++]; } NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject; NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; [data writeToFile:filename atomically:false]; [self.osdView displayText:@"Screenshot saved"]; } - (IBAction)saveScreenshotAs:(id)sender { bool shouldResume = _running; [self stop]; NSImage *image = [self takeScreenshot]; NSSavePanel *savePanel = [NSSavePanel savePanel]; [savePanel setNameFieldStringValue:[self screenshotFilename]]; [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [savePanel orderOut:self]; NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject; NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; [data writeToURL:savePanel.URL atomically:false]; [[NSUserDefaults standardUserDefaults] setObject:savePanel.URL.path.stringByDeletingLastPathComponent forKey:@"GBScreenshotFolder"]; } if (shouldResume) { [self start]; } }]; [self.osdView displayText:@"Screenshot saved"]; } - (IBAction)copyScreenshot:(id)sender { NSImage *image = [self takeScreenshot]; [[NSPasteboard generalPasteboard] clearContents]; [[NSPasteboard generalPasteboard] writeObjects:@[image]]; [self.osdView displayText:@"Screenshot copied"]; } - (IBAction)toggleDisplayBackground:(id)sender { GB_set_background_rendering_disabled(&_gb, !GB_is_background_rendering_disabled(&_gb)); } - (IBAction)toggleDisplayObjects:(id)sender { GB_set_object_rendering_disabled(&_gb, !GB_is_object_rendering_disabled(&_gb)); } - (IBAction)newCartridgeInstance:(id)sender { bool shouldResume = _running; [self stop]; NSSavePanel *savePanel = [NSSavePanel savePanel]; [savePanel setAllowedFileTypes:@[@"gbcart"]]; [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [savePanel orderOut:self]; NSString *romPath = self.romPath; [[NSFileManager defaultManager] trashItemAtURL:savePanel.URL resultingItemURL:nil error:nil]; [[NSFileManager defaultManager] createDirectoryAtURL:savePanel.URL withIntermediateDirectories:false attributes:nil error:nil]; [[NSString stringWithFormat:@"%@\n%@\n%@", [romPath pathRelativeToDirectory:savePanel.URL.path], romPath, [[NSURL fileURLWithPath:romPath].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")] ] writeToURL:[savePanel.URL URLByAppendingPathComponent:@"rom.gbl"] atomically:false encoding:NSUTF8StringEncoding error:nil]; [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:savePanel.URL display:true completionHandler:nil]; } if (shouldResume) { [self start]; } }]; } - (IBAction)toggleAudioRecording:(id)sender { bool shouldResume = _running; [self stop]; if (_isRecordingAudio) { _isRecordingAudio = false; int error = GB_stop_audio_recording(&_gb); if (error) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:[NSString stringWithFormat:@"Could not finalize recording: %s", strerror(error)]]; [alert setAlertStyle:NSAlertStyleCritical]; [alert runModal]; } else { [self.osdView displayText:@"Audio recording ended"]; } if (shouldResume) { [self start]; } return; } _audioSavePanel = [NSSavePanel savePanel]; if (!self.audioRecordingAccessoryView) { [[NSBundle mainBundle] loadNibNamed:@"AudioRecordingAccessoryView" owner:self topLevelObjects:nil]; } _audioSavePanel.accessoryView = self.audioRecordingAccessoryView; [self audioFormatChanged:self.audioFormatButton]; [_audioSavePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { [_audioSavePanel orderOut:self]; int error = GB_start_audio_recording(&_gb, _audioSavePanel.URL.fileSystemRepresentation, self.audioFormatButton.selectedTag); if (error) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:[NSString stringWithFormat:@"Could not start recording: %s", strerror(error)]]; [alert setAlertStyle:NSAlertStyleCritical]; [alert runModal]; } else { [self.osdView displayText:@"Audio recording started"]; _isRecordingAudio = true; } } if (shouldResume) { [self start]; } _audioSavePanel = nil; }]; } - (IBAction)audioFormatChanged:(NSPopUpButton *)sender { switch ((GB_audio_format_t)sender.selectedTag) { case GB_AUDIO_FORMAT_RAW: _audioSavePanel.allowedFileTypes = @[@"raw", @"pcm"]; break; case GB_AUDIO_FORMAT_AIFF: _audioSavePanel.allowedFileTypes = @[@"aiff", @"aif", @"aifc"]; break; case GB_AUDIO_FORMAT_WAV: _audioSavePanel.allowedFileTypes = @[@"wav"]; break; } } - (IBAction)toggleAudioChannel:(NSMenuItem *)sender { GB_set_channel_muted(&_gb, sender.tag, !GB_is_channel_muted(&_gb, sender.tag)); } - (IBAction)cartSwap:(id)sender { bool wasRunning = _running; if (wasRunning) { [self stop]; } [[NSDocumentController sharedDocumentController] beginOpenPanelWithCompletionHandler:^(NSArray *urls) { if (urls.count == 1) { bool ok = true; for (Document *document in [NSDocumentController sharedDocumentController].documents) { if (document == self) continue; if ([document.fileURL isEqual:urls.firstObject]) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:[NSString stringWithFormat:@"‘%@’ is already open in another window. Close ‘%@’ before hot swapping it into this instance.", urls.firstObject.lastPathComponent, urls.firstObject.lastPathComponent]]; [alert setAlertStyle:NSAlertStyleCritical]; [alert runModal]; ok = false; break; } } if (ok) { GB_save_battery(&_gb, self.savPath.UTF8String); self.fileURL = urls.firstObject; [self loadROM]; } } if (wasRunning) { [self start]; } }]; } - (IBAction)reloadROM:(id)sender { bool wasRunning = _running; if (wasRunning) { [self stop]; } [self loadROM]; if (wasRunning) { [self start]; } } - (void)updateDebuggerButtons { bool updateContinue = false; if (@available(macOS 10.10, *)) { if ([self.consoleInput.placeholderAttributedString.string isEqualToString:self.debuggerContinueButton.alternateTitle]) { [self.debuggerContinueButton mouseExited:nil]; updateContinue = true; } } if (self.isPaused) { self.debuggerContinueButton.toolTip = self.debuggerContinueButton.title = @"Continue"; self.debuggerContinueButton.alternateTitle = @"continue"; self.debuggerContinueButton.imagePosition = NSImageOnly; if (@available(macOS 10.14, *)) { self.debuggerContinueButton.contentTintColor = nil; } self.debuggerContinueButton.image = [NSImage imageNamed:@"ContinueTemplate"]; self.debuggerNextButton.enabled = true; self.debuggerStepButton.enabled = true; self.debuggerFinishButton.enabled = true; self.debuggerBackstepButton.enabled = true; } else { self.debuggerContinueButton.toolTip = self.debuggerContinueButton.title = @"Interrupt"; self.debuggerContinueButton.alternateTitle = @"interrupt"; self.debuggerContinueButton.imagePosition = NSImageOnly; if (@available(macOS 10.14, *)) { self.debuggerContinueButton.contentTintColor = [NSColor controlAccentColor]; } self.debuggerContinueButton.image = [NSImage imageNamed:@"InterruptTemplate"]; self.debuggerNextButton.enabled = false; self.debuggerStepButton.enabled = false; self.debuggerFinishButton.enabled = false; self.debuggerBackstepButton.enabled = false; } if (updateContinue) { [self.debuggerContinueButton mouseEntered:nil]; } } - (IBAction)debuggerButtonPressed:(NSButton *)sender { [self queueDebuggerCommand:sender.alternateTitle]; } + (NSArray *)readableTypes { NSMutableSet *set = [NSMutableSet setWithArray:[super readableTypes]]; for (NSString *type in @[@"gb", @"gbc", @"isx", @"gbs"]) { [set addObject:(__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)type, NULL)]; } return [set allObjects]; } @end