SameBoy/Cocoa/Document.m

2973 lines
104 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#import <AVFoundation/AVFoundation.h>
#import <CoreAudio/CoreAudio.h>
#import <Core/gb.h>
#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<NSString *> *baseComponents = [[directory pathComponents] mutableCopy];
NSMutableArray<NSString *> *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<NSValidatedUserInterfaceItem>)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<NSURL *> *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<NSString *> *)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