#import #import #import #import "GBAudioClient.h" #import "Document.h" #import "AppDelegate.h" #import "HexFiend/HexFiend.h" #import "GBMemoryByteArray.h" #import "GBWarningPopover.h" #import "GBCheatWindowController.h" #import "GBTerminalTextFieldCell.h" #import "BigSurToolbar.h" #import "GBPaletteEditorController.h" #import "GBObjectView.h" #import "GBPaletteView.h" @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!!! */ enum model { MODEL_NONE, MODEL_DMG, MODEL_CGB, MODEL_AGB, MODEL_SGB, MODEL_MGB, }; @interface Document () { NSMutableAttributedString *pending_console_output; NSRecursiveLock *console_output_lock; NSTimer *console_output_timer; NSTimer *hex_timer; bool fullScreen; bool in_sync_input; HFController *hex_controller; NSString *lastConsoleInput; HFLineCountingRepresenter *lineRep; CVImageBufferRef cameraImage; AVCaptureSession *cameraSession; AVCaptureConnection *cameraConnection; AVCaptureStillImageOutput *cameraOutput; GB_oam_info_t _oamInfo[40]; NSMutableData *currentPrinterImageData; enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy, GBAccessoryLinkCable} accessory; bool rom_warning_issued; NSMutableString *capturedOutput; bool logToSideView; bool shouldClearSideView; enum model current_model; 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; } @property GBAudioClient *audioClient; - (void) vblank; - (void) log: (const char *) log withAttributes: (GB_log_attributes) attributes; - (char *) getDebuggerInput; - (char *) getAsyncDebuggerInput; - (void) cameraRequestUpdate; - (uint8_t) cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y; - (void) printImage:(uint32_t *)image height:(unsigned) height topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin exposure:(unsigned) exposure; - (void) gotNewSample:(GB_sample_t *)sample; - (void) rumbleChanged:(double)amp; - (void) loadBootROM:(GB_boot_rom_t)type; - (void)linkCableBitStart:(bool)bit; - (bool)linkCableBitEnd; - (void)infraredStateChanged:(bool)state; @end 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 vblank]; } static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes 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 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]; } @implementation Document { GB_gameboy_t gb; volatile bool running; volatile bool stopping; NSConditionLock *has_debugger_input; NSMutableArray *debugger_input_queue; } - (instancetype)init { self = [super init]; if (self) { has_debugger_input = [[NSConditionLock alloc] initWithCondition:0]; debugger_input_queue = [[NSMutableArray alloc] init]; console_output_lock = [[NSRecursiveLock alloc] init]; audioLock = [[NSCondition alloc] init]; _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; } return self; } - (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"]; } - (GB_model_t)internalModel { switch (current_model) { case MODEL_DMG: return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"]; case MODEL_NONE: 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_AGB; } } - (void) updatePalette { GB_set_palette(&gb, [GBPaletteEditorController userPalette]); } - (void) updateBorderMode { borderModeChanged = true; } - (void) updateRumbleMode { GB_set_rumble_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRumbleMode"]); } - (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_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); GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]); GB_set_light_temperature(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]); GB_set_interference_volume(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]); GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); [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_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]); GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]); GB_set_rtc_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]); GB_apu_set_sample_callback(&gb, audioCallback); GB_set_rumble_callback(&gb, rumbleCallback); GB_set_infrared_callback(&gb, infraredStateChanged); [self updateRumbleMode]; } - (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) vblank { if (_gbsVisualizer) { dispatch_async(dispatch_get_main_queue(), ^{ [_gbsVisualizer setNeedsDisplay:true]; }); } [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:0.125]]; } 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)); audioBufferPosition = 0; } 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]; } hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:true]; [[NSRunLoop mainRunLoop] addTimer:hex_timer 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)]; } } 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); } } } [self postRun]; stopping = false; } - (void)postRun { [hex_timer invalidate]; [audioLock lock]; memset(audioBuffer, 0, (audioBufferSize - audioBufferPosition) * sizeof(*audioBuffer)); audioBufferPosition = audioBufferNeeded; [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 { self.gbsPlayPauseButton.state = true; self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0; if (master) { [master start]; return; } if (running) return; running = true; [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start]; } - (void) stop { 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); } - (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_AGB] = @"agb_boot", }; GB_load_boot_rom(&gb, [[self bootROMPathForName:names[type]] UTF8String]); } - (IBAction)reset:(id)sender { [self stop]; size_t old_width = GB_get_screen_width(&gb); if ([sender tag] != MODEL_NONE) { current_model = (enum model)[sender tag]; } GB_switch_model_and_reset(&gb, [self internalModel]); if (old_width != GB_get_screen_width(&gb)) { [self.view screenSizeChanged]; } [self updateMinSize]; if ([sender tag] != 0) { /* User explictly selected a model, save the preference */ [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_DMG forKey:@"EmulateDMG"]; [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_SGB forKey:@"EmulateSGB"]; [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_AGB forKey:@"EmulateAGB"]; [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_MGB forKey:@"EmulateMGB"]; } /* Reload the ROM, SAV and SYM files */ [self loadROM]; [self start]; if (hex_controller) { /* Verify bank sanity, especially when switching models. */ [(GBMemoryByteArray *)(hex_controller.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); } } - (void)windowControllerDidLoadNib:(NSWindowController *)aController { [super windowControllerDidLoadNib:aController]; // Interface Builder bug? [self.consoleWindow setContentSize:self.consoleWindow.minSize]; /* 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 = [NSFont userFixedPitchFontOfSize:12]; 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]; self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]]; self.debuggerSplitView.dividerColor = [NSColor clearColor]; if (@available(macOS 11.0, *)) { self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded; self.printerFeedWindow.toolbarStyle = NSWindowToolbarStyleUnifiedCompact; [self.printerFeedWindow.toolbar removeItemAtIndex:1]; self.printerFeedWindow.toolbar.items.firstObject.image = [NSImage imageWithSystemSymbolName:@"square.and.arrow.down" accessibilityDescription:@"Save"]; self.printerFeedWindow.toolbar.items.lastObject.image = [NSImage imageWithSystemSymbolName:@"printer" accessibilityDescription:@"Print"]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateHighpassFilter) name:@"GBHighpassFilterChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateColorCorrectionMode) name:@"GBColorCorrectionChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateLightTemperature) name:@"GBLightTemperatureChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateInterferenceVolume) name:@"GBInterferenceVolumeChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateFrameBlendingMode) name:@"GBFrameBlendingModeChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatePalette) name:@"GBColorPaletteChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateBorderMode) name:@"GBBorderModeChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateRumbleMode) name:@"GBRumbleModeChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateRewindLength) name:@"GBRewindLengthChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateRTCMode) name:@"GBRTCModeChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dmgModelChanged) name:@"GBDMGModelChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sgbModelChanged) name:@"GBSGBModelChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cgbModelChanged) name:@"GBCGBModelChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateVolume) name:@"GBVolumeChanged" object:nil]; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) { current_model = MODEL_DMG; } else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateSGB"]) { current_model = MODEL_SGB; } else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateMGB"]) { current_model = MODEL_MGB; } else { current_model = [[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateAGB"]? MODEL_AGB : MODEL_CGB; } [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 { hex_controller = [[HFController alloc] init]; [hex_controller setBytesPerColumn:1]; [hex_controller setEditMode:HFOverwriteMode]; [hex_controller 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]; HFStatusBarRepresenter *statusRep = [[HFStatusBarRepresenter alloc] init]; lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal; /* Add all our reps to the controller. */ [hex_controller addRepresenter:layoutRep]; [hex_controller addRepresenter:hexRep]; [hex_controller addRepresenter:asciiRep]; [hex_controller addRepresenter:scrollRep]; [hex_controller addRepresenter:lineRep]; [hex_controller 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.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; } } - (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; } - (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]); } GB_load_battery(&gb, self.savPath.UTF8String); GB_load_cheats(&gb, self.chtPath.UTF8String); [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 && !rom_warning_issued) { rom_warning_issued = true; [GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow]; } return ret; } - (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]; [self.consoleWindow close]; [self.memoryWindow close]; [self.vramWindow close]; [self.printerFeedWindow close]; [self.cheatsWindow close]; [super close]; } - (IBAction) interrupt:(id)sender { [self log:"^C\n"]; GB_debugger_break(&gb); [self start]; [self.consoleWindow makeKeyAndOrderFront:nil]; [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)validateUserInterfaceItem:(id)anItem { if ([anItem action] == @selector(mute:)) { [(NSMenuItem *)anItem setState:!_audioClient.isPlaying]; } else if ([anItem action] == @selector(togglePause:)) { if (master) { [(NSMenuItem *)anItem setState:(!master->running) || (GB_debugger_is_stopped(&gb)) || (GB_debugger_is_stopped(&gb))]; } [(NSMenuItem *)anItem setState:(!running) || (GB_debugger_is_stopped(&gb))]; return !GB_debugger_is_stopped(&gb); } else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE) { [(NSMenuItem *)anItem setState:anItem.tag == current_model]; } else if ([anItem action] == @selector(interrupt:)) { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { return false; } } else if ([anItem action] == @selector(disconnectAllAccessories:)) { [(NSMenuItem *)anItem setState:accessory == GBAccessoryNone]; } else if ([anItem action] == @selector(connectPrinter:)) { [(NSMenuItem *)anItem setState:accessory == GBAccessoryPrinter]; } else if ([anItem action] == @selector(connectWorkboy:)) { [(NSMenuItem *)anItem setState:accessory == GBAccessoryWorkboy]; } 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…"]; } return [super validateUserInterfaceItem:anItem]; } - (void) windowWillEnterFullScreen:(NSNotification *)notification { fullScreen = true; self.view.mouseHidingEnabled = running; } - (void) windowWillExitFullScreen:(NSNotification *)notification { fullScreen = false; self.view.mouseHidingEnabled = false; } - (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame { if (fullScreen) { return newFrame; } size_t width = GB_get_screen_width(&gb), height = GB_get_screen_height(&gb); NSRect rect = window.contentView.frame; unsigned titlebarSize = window.contentView.superview.frame.size.height - rect.size.height; unsigned step = width / [[window screen] backingScaleFactor]; rect.size.width = floor(rect.size.width / step) * step + step; rect.size.height = rect.size.width * height / width + titlebarSize; if (rect.size.width > newFrame.size.width) { rect.size.width = width; rect.size.height = height + titlebarSize; } else if (rect.size.height > newFrame.size.height) { rect.size.width = width; rect.size.height = height + titlebarSize; } rect.origin = window.frame.origin; rect.origin.y -= rect.size.height - window.frame.size.height; return rect; } - (void) appendPendingOutput { [console_output_lock lock]; if (shouldClearSideView) { shouldClearSideView = false; [self.debuggerSideView setString:@""]; } if (pending_console_output) { NSTextView *textView = logToSideView? self.debuggerSideView : self.consoleOutput; [hex_controller reloadData]; [self reloadVRAMData: nil]; [textView.textStorage appendAttributedString:pending_console_output]; [textView scrollToEndOfDocument:nil]; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { [self.consoleWindow orderFront:nil]; } pending_console_output = nil; } [console_output_lock unlock]; } - (void) log: (const char *) string withAttributes: (GB_log_attributes) attributes { NSString *nsstring = @(string); // For ref-counting if (capturedOutput) { [capturedOutput appendString:nsstring]; return; } NSFont *font = [NSFont userFixedPitchFontOfSize:12]; 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}]; [console_output_lock lock]; if (!pending_console_output) { pending_console_output = attributed; } else { [pending_console_output appendAttributedString:attributed]; } if (![console_output_timer isValid]) { console_output_timer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:false]; [[NSRunLoop mainRunLoop] addTimer:console_output_timer forMode:NSDefaultRunLoopMode]; } [console_output_lock unlock]; /* Make sure mouse is not hidden while debugging */ self.view.mouseHidingEnabled = false; } - (IBAction)showConsoleWindow:(id)sender { [self.consoleWindow orderBack:nil]; } - (IBAction)consoleInput:(NSTextField *)sender { NSString *line = [sender stringValue]; if ([line isEqualToString:@""] && lastConsoleInput) { line = lastConsoleInput; } else if (line) { lastConsoleInput = line; } else { line = @""; } if (!in_sync_input) { [self log:">"]; } [self log:[line UTF8String]]; [self log:"\n"]; [has_debugger_input lock]; [debugger_input_queue addObject:line]; [has_debugger_input unlockWithCondition:1]; [sender setStringValue:@""]; } - (void) interruptDebugInputRead { [has_debugger_input lock]; [debugger_input_queue addObject:[NSNull null]]; [has_debugger_input unlockWithCondition:1]; } - (void) updateSideView { if (!GB_debugger_is_stopped(&gb)) { return; } if (![NSThread isMainThread]) { dispatch_sync(dispatch_get_main_queue(), ^{ [self updateSideView]; }); return; } [console_output_lock lock]; shouldClearSideView = true; [self appendPendingOutput]; logToSideView = true; [console_output_lock 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); } } [console_output_lock lock]; [self appendPendingOutput]; logToSideView = false; [console_output_lock unlock]; } - (char *) getDebuggerInput { bool isPlaying = _audioClient.isPlaying; if (isPlaying) { [_audioClient stop]; } [audioLock lock]; [audioLock signal]; [audioLock unlock]; [self updateSideView]; [self log:">"]; in_sync_input = true; [has_debugger_input lockWhenCondition:1]; NSString *input = [debugger_input_queue firstObject]; [debugger_input_queue removeObjectAtIndex:0]; [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; in_sync_input = 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:@""]; } }); if (isPlaying) { [_audioClient start]; } if ((id) input == [NSNull null]) { return NULL; } return strdup([input UTF8String]); } - (char *) getAsyncDebuggerInput { [has_debugger_input lock]; NSString *input = [debugger_input_queue firstObject]; if (input) { [debugger_input_queue removeObjectAtIndex:0]; } [has_debugger_input unlockWithCondition:[debugger_input_queue 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]; } - (uint8_t) readMemory:(uint16_t)addr { while (!GB_is_inited(&gb)); return GB_safe_read_memory(&gb, addr); } - (void) writeMemory:(uint16_t)addr value:(uint8_t)value { while (!GB_is_inited(&gb)); GB_write_memory(&gb, addr, value); } - (void) performAtomicBlock: (void (^)())block { while (!GB_is_inited(&gb)); bool was_running = running && !GB_debugger_is_stopped(&gb); if (master) { was_running |= master->running; } if (was_running) { [self stop]; } block(); if (was_running) { [self start]; } } - (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) { [hex_controller 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 (!hex_controller) { [self initMemoryView]; } [self.memoryWindow makeKeyAndOrderFront:sender]; } - (IBAction)hexGoTo:(id)sender { NSString *error = [self captureOutputForBlock:^{ uint16_t addr; if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, NULL)) { return; } addr -= lineRep.valueOffset; if (addr >= hex_controller.byteArray.length) { GB_log(&gb, "Value $%04x is out of range.\n", addr); return; } [hex_controller setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]]; [hex_controller _ensureVisibilityOfLocation:addr]; [self.memoryWindow makeFirstResponder:self.memoryView.subviews[0].subviews[0]]; }]; if (error) { NSBeep(); [GBWarningPopover popoverWithContents:error onView:sender]; } } - (void)hexUpdateBank:(NSControl *)sender ignoreErrors: (bool)ignore_errors { NSString *error = [self captureOutputForBlock:^{ uint16_t addr, bank; if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, &bank)) { return; } if (bank == (uint16_t) -1) { bank = addr; } uint16_t n_banks = 1; switch ([(GBMemoryByteArray *)(hex_controller.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; [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]]; [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:bank]; [hex_controller reloadData]; }]; if (error && !ignore_errors) { NSBeep(); [GBWarningPopover popoverWithContents:error onView:sender]; } } - (IBAction)hexUpdateBank:(NSControl *)sender { [self hexUpdateBank:sender ignoreErrors:false]; } - (IBAction)hexUpdateSpace:(NSPopUpButtonCell *)sender { self.memoryBankItem.enabled = [sender indexOfSelectedItem] != GBMemoryEntireSpace; GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(hex_controller.byteArray); [byteArray setMode:(GB_memory_mode_t)[sender indexOfSelectedItem]]; uint16_t bank; switch ((GB_memory_mode_t)[sender indexOfSelectedItem]) { case GBMemoryEntireSpace: case GBMemoryROM: lineRep.valueOffset = 0; GB_get_direct_access(&gb, GB_DIRECT_ACCESS_ROM, NULL, &bank); byteArray.selectedBank = bank; break; case GBMemoryVRAM: lineRep.valueOffset = 0x8000; GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, &bank); byteArray.selectedBank = bank; break; case GBMemoryExternalRAM: lineRep.valueOffset = 0xA000; GB_get_direct_access(&gb, GB_DIRECT_ACCESS_CART_RAM, NULL, &bank); byteArray.selectedBank = bank; break; case GBMemoryRAM: lineRep.valueOffset = 0xC000; GB_get_direct_access(&gb, GB_DIRECT_ACCESS_RAM, NULL, &bank); byteArray.selectedBank = bank; break; } [self.memoryBankInput setStringValue:[NSString stringWithFormat:@"$%x", byteArray.selectedBank]]; [hex_controller reloadData]; [self.memoryView 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 formats] firstObject] formatDescription]); if (!input) { GB_camera_updated(&gb); return; } 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: @(MAX(128, 112 * dimensions.width / dimensions.height)), (id)kCVPixelBufferHeightKey: @(MAX(112, 128 * dimensions.height / dimensions.width)),}]; 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:(uint8_t)x andY:(uint8_t) y { if (!cameraImage) { return 0; } uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(cameraImage); size_t bytesPerRow = CVPixelBufferGetBytesPerRow(cameraImage); uint8_t offsetX = (CVPixelBufferGetWidth(cameraImage) - 128) / 2; uint8_t 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 & 0x08)) { map_base = 0x1C00; } if (tileset_type == GB_TILESET_AUTO) { tileset_type = (lcdc & 0x10)? 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(), ^{ self.feedImageView.image = [Document imageFromData:currentPrinterImageData width:160 height:currentPrinterImageData.length / 160 / sizeof(imageBytes[0]) scale:2.0]; NSRect frame = self.printerFeedWindow.frame; 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; [self.printerFeedWindow setFrame:frame display:false animate: self.printerFeedWindow.isVisible]; [self.printerFeedWindow orderFront:NULL]; }); } - (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:^{ accessory = GBAccessoryNone; GB_disconnect_serial(&gb); }]; } - (IBAction)connectPrinter:(id)sender { [self disconnectLinkCable]; [self performAtomicBlock:^{ accessory = GBAccessoryPrinter; GB_connect_printer(&gb, printImage); }]; } - (IBAction)connectWorkboy:(id)sender { [self disconnectLinkCable]; [self performAtomicBlock:^{ accessory = GBAccessoryWorkboy; GB_connect_workboy(&gb, setWorkboyTime, getWorkboyTime); }]; } - (void) updateVolume { _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; } - (void) updateHighpassFilter { if (GB_is_inited(&gb)) { GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]); } } - (void) updateColorCorrectionMode { if (GB_is_inited(&gb)) { GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]); } } - (void) updateLightTemperature { if (GB_is_inited(&gb)) { GB_set_light_temperature(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]); } } - (void) updateInterferenceVolume { if (GB_is_inited(&gb)) { GB_set_interference_volume(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]); } } - (void) updateFrameBlendingMode { self.view.frameBlendingMode = (GB_frame_blending_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; } - (void) updateRewindLength { [self performAtomicBlock:^{ if (GB_is_inited(&gb)) { GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]); } }]; } - (void) updateRTCMode { if (GB_is_inited(&gb)) { GB_set_rtc_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]); } } - (void)dmgModelChanged { modelsChanging = true; if (current_model == MODEL_DMG) { [self reset:nil]; } modelsChanging = false; } - (void)sgbModelChanged { modelsChanging = true; if (current_model == MODEL_SGB) { [self reset:nil]; } modelsChanging = false; } - (void)cgbModelChanged { modelsChanging = true; if (current_model == MODEL_CGB) { [self reset:nil]; } modelsChanging = false; } - (void)setFileURL:(NSURL *)fileURL { [super setFileURL:fileURL]; self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[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]; } /* NSSplitView renders its separator without the proper vibrancy, so we made it transparent and move an NSBox-based separator that renders properly so it acts like the split view's separator. */ NSRect rect = self.debuggerVerticalLine.frame; rect.origin.x = [[[splitview arrangedSubviews] firstObject] frame].size.width - 1; self.debuggerVerticalLine.frame = rect; } - (IBAction)showCheats:(id)sender { [self.cheatsWindow makeKeyAndOrderFront:nil]; } - (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); partner->accessory = GBAccessoryNone; accessory = GBAccessoryNone; } } - (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; linkOffset = 0; partner->accessory = GBAccessoryLinkCable; accessory = GBAccessoryLinkCable; 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]; [ret lockFocus]; NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0, ret.size.width, ret.size.height)]; [ret unlockFocus]; ret = [[NSImage alloc] initWithSize:ret.size]; [ret addRepresentation:bitmapRep]; } 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; } } @end