SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
Cocoa/Document.m
1 #import <AVFoundation/AVFoundation.h>
2 #import <CoreAudio/CoreAudio.h>
3 #import <Core/gb.h>
4 #import "GBAudioClient.h"
5 #import "Document.h"
6 #import "GBApp.h"
7 #import "HexFiend/HexFiend.h"
8 #import "GBMemoryByteArray.h"
9 #import "GBWarningPopover.h"
10 #import "GBCheatWindowController.h"
11 #import "GBTerminalTextFieldCell.h"
12 #import "BigSurToolbar.h"
13 #import "GBPaletteEditorController.h"
14 #import "GBCheatSearchController.h"
15 #import "GBObjectView.h"
16 #import "GBPaletteView.h"
17 #import "GBHexStatusBarRepresenter.h"
18 #import "NSObject+DefaultsObserver.h"
19
20 #define likely(x) GB_likely(x)
21 #define unlikely(x) GB_unlikely(x)
22
23 @implementation NSString (relativePath)
24
25 - (NSString *)pathRelativeToDirectory:(NSString *)directory
26 {
27 NSMutableArray<NSString *> *baseComponents = [[directory pathComponents] mutableCopy];
28 NSMutableArray<NSString *> *selfComponents = [[self pathComponents] mutableCopy];
29
30 while (baseComponents.count) {
31 if (![baseComponents.firstObject isEqualToString:selfComponents.firstObject]) {
32 break;
33 }
34
35 [baseComponents removeObjectAtIndex:0];
36 [selfComponents removeObjectAtIndex:0];
37 }
38 while (baseComponents.count) {
39 [baseComponents removeObjectAtIndex:0];
40 [selfComponents insertObject:@".." atIndex:0];
41 }
42 return [selfComponents componentsJoinedByString:@"/"];
43 }
44
45 @end
46
47 #define GB_MODEL_PAL_BIT_OLD 0x1000
48
49 /* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */
50 /* Todo: Split into category files! This is so messy!!! */
51
52
53 @interface Document ()
54 @property GBAudioClient *audioClient;
55 @end
56
57 @implementation Document
58 {
59 GB_gameboy_t _gb;
60 volatile bool _running;
61 volatile bool _stopping;
62 NSConditionLock *_hasDebuggerInput;
63 NSMutableArray *_debuggerInputQueue;
64
65 NSMutableAttributedString *_pendingConsoleOutput;
66 NSRecursiveLock *_consoleOutputLock;
67 NSTimer *_consoleOutputTimer;
68 NSTimer *_hexTimer;
69
70 NSTimer *_batterySaveTimer;
71 bool _dirtyBattery;
72
73 bool _fullScreen;
74 bool _inSyncInput;
75 NSString *_debuggerCommandWhilePaused;
76 HFController *_hexController;
77
78 NSString *_lastConsoleInput;
79 HFLineCountingRepresenter *_lineRep;
80 GBHexStatusBarRepresenter *_statusRep;
81
82 CVImageBufferRef _cameraImage;
83 AVCaptureSession *_cameraSession;
84 AVCaptureConnection *_cameraConnection;
85 AVCaptureStillImageOutput *_cameraOutput;
86
87 GB_oam_info_t _oamInfo[40];
88
89 NSMutableData *_currentPrinterImageData;
90
91 bool _romWarningIssued;
92
93 NSMutableString *_capturedOutput;
94 bool _logToSideView;
95 bool _shouldClearSideView;
96 enum model _currentModel;
97 bool _usesAutoModel;
98
99 bool _rewind;
100 bool _modelsChanging;
101
102 NSCondition *_audioLock;
103 GB_sample_t *_audioBuffer;
104 size_t _audioBufferSize;
105 size_t _audioBufferPosition;
106 size_t _audioBufferNeeded;
107 double _volume;
108
109 bool _borderModeChanged;
110
111 /* Link cable*/
112 Document *_master;
113 Document *_slave;
114 signed _linkOffset;
115 bool _linkCableBit;
116
117 NSSavePanel *_audioSavePanel;
118 bool _isRecordingAudio;
119
120 void (^ volatile _pendingAtomicBlock)();
121
122 NSDate *_fileModificationTime;
123 __weak NSThread *_emulationThread;
124
125 GBCheatSearchController *_cheatSearchController;
126
127 bool _romModified;
128 }
129
130 static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type)
131 {
132 Document *self = (__bridge Document *)GB_get_user_data(gb);
133 [self loadBootROM: type];
134 }
135
136 static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
137 {
138 Document *self = (__bridge Document *)GB_get_user_data(gb);
139 [self vblankWithType:type];
140 }
141
142 static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes)
143 {
144 Document *self = (__bridge Document *)GB_get_user_data(gb);
145 [self log:string withAttributes: attributes];
146 }
147
148 static char *consoleInput(GB_gameboy_t *gb)
149 {
150 Document *self = (__bridge Document *)GB_get_user_data(gb);
151 return [self getDebuggerInput];
152 }
153
154 static char *asyncConsoleInput(GB_gameboy_t *gb)
155 {
156 Document *self = (__bridge Document *)GB_get_user_data(gb);
157 char *ret = [self getAsyncDebuggerInput];
158 return ret;
159 }
160
161 static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
162 {
163 return (r << 0) | (g << 8) | (b << 16) | 0xFF000000;
164 }
165
166 static void cameraRequestUpdate(GB_gameboy_t *gb)
167 {
168 Document *self = (__bridge Document *)GB_get_user_data(gb);
169 [self cameraRequestUpdate];
170 }
171
172 static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y)
173 {
174 Document *self = (__bridge Document *)GB_get_user_data(gb);
175 return [self cameraGetPixelAtX:x andY:y];
176 }
177
178 static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height,
179 uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure)
180 {
181 Document *self = (__bridge Document *)GB_get_user_data(gb);
182 [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure];
183 }
184
185 static void printDone(GB_gameboy_t *gb)
186 {
187 Document *self = (__bridge Document *)GB_get_user_data(gb);
188 [self printDone];
189 }
190
191 static void setWorkboyTime(GB_gameboy_t *gb, time_t t)
192 {
193 [[NSUserDefaults standardUserDefaults] setInteger:time(NULL) - t forKey:@"GBWorkboyTimeOffset"];
194 }
195
196 static time_t getWorkboyTime(GB_gameboy_t *gb)
197 {
198 return time(NULL) - [[NSUserDefaults standardUserDefaults] integerForKey:@"GBWorkboyTimeOffset"];
199 }
200
201 static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
202 {
203 Document *self = (__bridge Document *)GB_get_user_data(gb);
204 [self gotNewSample:sample];
205 }
206
207 static void rumbleCallback(GB_gameboy_t *gb, double amp)
208 {
209 Document *self = (__bridge Document *)GB_get_user_data(gb);
210 [self rumbleChanged:amp];
211 }
212
213
214 static void _linkCableBitStart(GB_gameboy_t *gb, bool bit_to_send)
215 {
216 Document *self = (__bridge Document *)GB_get_user_data(gb);
217 [self _linkCableBitStart:bit_to_send];
218 }
219
220 static bool _linkCableBitEnd(GB_gameboy_t *gb)
221 {
222 Document *self = (__bridge Document *)GB_get_user_data(gb);
223 return [self _linkCableBitEnd];
224 }
225
226 static void infraredStateChanged(GB_gameboy_t *gb, bool on)
227 {
228 Document *self = (__bridge Document *)GB_get_user_data(gb);
229 [self infraredStateChanged:on];
230 }
231
232 static void debuggerReloadCallback(GB_gameboy_t *gb)
233 {
234 Document *self = (__bridge Document *)GB_get_user_data(gb);
235 dispatch_sync(dispatch_get_main_queue(), ^{
236 bool wasRunning = self->_running;
237 self->_running = false; // Hack for output capture
238 [self loadROM];
239 self->_running = wasRunning;
240 GB_reset(gb);
241 });
242 }
243
244 - (instancetype)init
245 {
246 self = [super init];
247 if (self) {
248 _hasDebuggerInput = [[NSConditionLock alloc] initWithCondition:0];
249 _debuggerInputQueue = [[NSMutableArray alloc] init];
250 _consoleOutputLock = [[NSRecursiveLock alloc] init];
251 _audioLock = [[NSCondition alloc] init];
252 _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"];
253 }
254 return self;
255 }
256
257 - (GB_model_t)internalModel
258 {
259 switch (_currentModel) {
260 case MODEL_DMG:
261 return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"];
262
263 case MODEL_NONE:
264 case MODEL_QUICK_RESET:
265 case MODEL_AUTO:
266 case MODEL_CGB:
267 return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"];
268
269 case MODEL_SGB: {
270 GB_model_t model = (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"];
271 if (model == (GB_MODEL_SGB | GB_MODEL_PAL_BIT_OLD)) {
272 model = GB_MODEL_SGB_PAL;
273 }
274 return model;
275 }
276
277 case MODEL_MGB:
278 return GB_MODEL_MGB;
279
280 case MODEL_AGB:
281 return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBAGBModel"];
282 }
283 }
284
285 - (void)updatePalette
286 {
287 GB_set_palette(&_gb, [GBPaletteEditorController userPalette]);
288 }
289
290 - (void)initCommon
291 {
292 GB_init(&_gb, [self internalModel]);
293 GB_set_user_data(&_gb, (__bridge void *)(self));
294 GB_set_boot_rom_load_callback(&_gb, (GB_boot_rom_load_callback_t)boot_rom_load);
295 GB_set_vblank_callback(&_gb, (GB_vblank_callback_t) vblank);
296 GB_set_enable_skipped_frame_vblank_callbacks(&_gb, true);
297 GB_set_log_callback(&_gb, (GB_log_callback_t) consoleLog);
298 GB_set_input_callback(&_gb, (GB_input_callback_t) consoleInput);
299 GB_set_async_input_callback(&_gb, (GB_input_callback_t) asyncConsoleInput);
300 [self updatePalette];
301 GB_set_rgb_encode_callback(&_gb, rgbEncode);
302 GB_set_camera_get_pixel_callback(&_gb, cameraGetPixel);
303 GB_set_camera_update_request_callback(&_gb, cameraRequestUpdate);
304 GB_apu_set_sample_callback(&_gb, audioCallback);
305 GB_set_rumble_callback(&_gb, rumbleCallback);
306 GB_set_infrared_callback(&_gb, infraredStateChanged);
307 GB_debugger_set_reload_callback(&_gb, debuggerReloadCallback);
308
309 GB_gameboy_t *gb = &_gb;
310 __unsafe_unretained Document *weakSelf = self;
311
312 [self observeStandardDefaultsKey:@"GBColorCorrection" withBlock:^(NSNumber *value) {
313 GB_set_color_correction_mode(gb, value.unsignedIntValue);
314 }];
315
316 [self observeStandardDefaultsKey:@"GBLightTemperature" withBlock:^(NSNumber *value) {
317 GB_set_light_temperature(gb, value.doubleValue);
318 }];
319
320 [self observeStandardDefaultsKey:@"GBInterferenceVolume" withBlock:^(NSNumber *value) {
321 GB_set_interference_volume(gb, value.doubleValue);
322 }];
323
324 GB_set_border_mode(&_gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
325 [self observeStandardDefaultsKey:@"GBBorderMode" withBlock:^(NSNumber *value) {
326 weakSelf->_borderModeChanged = true;
327 }];
328
329 [self observeStandardDefaultsKey:@"GBHighpassFilter" withBlock:^(NSNumber *value) {
330 GB_set_highpass_filter_mode(gb, value.unsignedIntValue);
331 }];
332
333 [self observeStandardDefaultsKey:@"GBRewindLength" withBlock:^(NSNumber *value) {
334 [weakSelf performAtomicBlock:^{
335 GB_set_rewind_length(gb, value.unsignedIntValue);
336 }];
337 }];
338
339 [self observeStandardDefaultsKey:@"GBRTCMode" withBlock:^(NSNumber *value) {
340 GB_set_rtc_mode(gb, value.unsignedIntValue);
341 }];
342
343 [self observeStandardDefaultsKey:@"GBRumbleMode" withBlock:^(NSNumber *value) {
344 GB_set_rumble_mode(gb, value.unsignedIntValue);
345 }];
346
347 [self observeStandardDefaultsKey:@"GBDebuggerFont" withBlock:^(NSString *value) {
348 [weakSelf updateFonts];
349 }];
350
351 [self observeStandardDefaultsKey:@"GBDebuggerFontSize" withBlock:^(NSString *value) {
352 [weakSelf updateFonts];
353 }];
354
355 [self observeStandardDefaultsKey:@"GBTurboCap" withBlock:^(NSNumber *value) {
356 if (!weakSelf->_master) {
357 GB_set_turbo_cap(gb, value.doubleValue);
358 }
359 }];
360 }
361
362 - (void)updateMinSize
363 {
364 self.mainWindow.contentMinSize = NSMakeSize(GB_get_screen_width(&_gb), GB_get_screen_height(&_gb));
365 if (self.mainWindow.contentView.bounds.size.width < GB_get_screen_width(&_gb) ||
366 self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&_gb)) {
367 [self.mainWindow zoom:nil];
368 }
369 self.osdView.usesSGBScale = GB_get_screen_width(&_gb) == 256;
370 }
371
372 - (void)vblankWithType:(GB_vblank_type_t)type
373 {
374 if (type == GB_VBLANK_TYPE_SKIPPED_FRAME) {
375 double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb);
376 [_cpuView addSample:frameUsage];
377 return;
378 }
379
380 if (_gbsVisualizer) {
381 dispatch_async(dispatch_get_main_queue(), ^{
382 [_gbsVisualizer setNeedsDisplay:true];
383 });
384 }
385
386 double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb);
387 [_cpuView addSample:frameUsage];
388
389 if (self.consoleWindow.visible) {
390 double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
391 dispatch_async(dispatch_get_main_queue(), ^{
392 [_cpuView setNeedsDisplay:true];
393 _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
394 });
395 }
396
397 if (type != GB_VBLANK_TYPE_REPEAT) {
398 [self.view flip];
399 if (_borderModeChanged) {
400 dispatch_sync(dispatch_get_main_queue(), ^{
401 size_t previous_width = GB_get_screen_width(&_gb);
402 GB_set_border_mode(&_gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
403 if (GB_get_screen_width(&_gb) != previous_width) {
404 [self.view screenSizeChanged];
405 [self updateMinSize];
406 }
407 });
408 _borderModeChanged = false;
409 }
410 GB_set_pixels_output(&_gb, self.view.pixels);
411 }
412
413 if (self.vramWindow.isVisible) {
414 dispatch_async(dispatch_get_main_queue(), ^{
415 [self reloadVRAMData: nil];
416 });
417 }
418
419 self.view.mouseHidingEnabled = (_mainWindow.styleMask & NSWindowStyleMaskFullScreen) && !_consoleWindow.visible && !_memoryWindow.visible && !_vramWindow.visible && !_printerFeedWindow.visible && !_cheatsWindow.visible;
420
421 if (self.view.isRewinding) {
422 _rewind = true;
423 [self.osdView displayText:@"Rewinding…"];
424 }
425 }
426
427 - (void)gotNewSample:(GB_sample_t *)sample
428 {
429 if (_gbsVisualizer) {
430 [_gbsVisualizer addSample:sample];
431 }
432 [_audioLock lock];
433 if (_audioClient.isPlaying) {
434 if (_audioBufferPosition == _audioBufferSize) {
435 if (_audioBufferSize >= 0x4000) {
436 _audioBufferPosition = 0;
437 [_audioLock unlock];
438 return;
439 }
440
441 if (_audioBufferSize == 0) {
442 _audioBufferSize = 512;
443 }
444 else {
445 _audioBufferSize += _audioBufferSize >> 2;
446 }
447 _audioBuffer = realloc(_audioBuffer, sizeof(*sample) * _audioBufferSize);
448 }
449 if (_volume != 1) {
450 sample->left *= _volume;
451 sample->right *= _volume;
452 }
453 _audioBuffer[_audioBufferPosition++] = *sample;
454 }
455 if (_audioBufferPosition == _audioBufferNeeded) {
456 [_audioLock signal];
457 _audioBufferNeeded = 0;
458 }
459 [_audioLock unlock];
460 }
461
462 - (void)rumbleChanged:(double)amp
463 {
464 [_view setRumble:amp];
465 }
466
467 - (void)preRun
468 {
469 GB_set_pixels_output(&_gb, self.view.pixels);
470 GB_set_sample_rate(&_gb, 96000);
471 _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) {
472 [_audioLock lock];
473
474 if (_audioBufferPosition < nFrames) {
475 _audioBufferNeeded = nFrames;
476 [_audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:(double)(_audioBufferNeeded - _audioBufferPosition) / sampleRate]];
477 _audioBufferNeeded = 0;
478 }
479
480 if (_stopping || GB_debugger_is_stopped(&_gb)) {
481 memset(buffer, 0, nFrames * sizeof(*buffer));
482 [_audioLock unlock];
483 return;
484 }
485
486 if (_audioBufferPosition < nFrames) {
487 // Not enough audio
488 memset(buffer, 0, (nFrames - _audioBufferPosition) * sizeof(*buffer));
489 memcpy(buffer, _audioBuffer, _audioBufferPosition * sizeof(*buffer));
490 // Do not reset the audio position to avoid more underflows
491 }
492 else if (_audioBufferPosition < nFrames + 4800) {
493 memcpy(buffer, _audioBuffer, nFrames * sizeof(*buffer));
494 memmove(_audioBuffer, _audioBuffer + nFrames, (_audioBufferPosition - nFrames) * sizeof(*buffer));
495 _audioBufferPosition = _audioBufferPosition - nFrames;
496 }
497 else {
498 memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer));
499 _audioBufferPosition = 0;
500 }
501 [_audioLock unlock];
502 } andSampleRate:96000];
503 if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) {
504 [_audioClient start];
505 }
506 _hexTimer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:true];
507 [[NSRunLoop mainRunLoop] addTimer:_hexTimer forMode:NSDefaultRunLoopMode];
508
509 _batterySaveTimer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(batteryTimerExpired) userInfo:nil repeats:true];
510 [[NSRunLoop mainRunLoop] addTimer:_batterySaveTimer forMode:NSDefaultRunLoopMode];
511
512 /* Clear pending alarms, don't play alarms while playing */
513 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
514 NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
515 for (NSUserNotification *notification in [center scheduledNotifications]) {
516 if ([notification.identifier isEqualToString:self.fileURL.path]) {
517 [center removeScheduledNotification:notification];
518 break;
519 }
520 }
521
522 for (NSUserNotification *notification in [center deliveredNotifications]) {
523 if ([notification.identifier isEqualToString:self.fileURL.path]) {
524 [center removeDeliveredNotification:notification];
525 break;
526 }
527 }
528 }
529 }
530
531 static unsigned *multiplication_table_for_frequency(unsigned frequency)
532 {
533 unsigned *ret = malloc(sizeof(*ret) * 0x100);
534 for (unsigned i = 0; i < 0x100; i++) {
535 ret[i] = i * frequency;
536 }
537 return ret;
538 }
539
540 - (void)run
541 {
542 assert(!_master);
543 [self preRun];
544 if (_slave) {
545 [_slave preRun];
546 unsigned *masterTable = multiplication_table_for_frequency(GB_get_clock_rate(&_gb));
547 unsigned *slaveTable = multiplication_table_for_frequency(GB_get_clock_rate(&_slave->_gb));
548 while (_running) {
549 if (_linkOffset <= 0) {
550 _linkOffset += slaveTable[GB_run(&_gb)];
551 }
552 else {
553 _linkOffset -= masterTable[GB_run(&_slave->_gb)];
554 }
555 if (unlikely(_pendingAtomicBlock)) {
556 _pendingAtomicBlock();
557 _pendingAtomicBlock = nil;
558 }
559 }
560 free(masterTable);
561 free(slaveTable);
562 [_slave postRun];
563 }
564 else {
565 while (_running) {
566 if (_rewind) {
567 _rewind = false;
568 GB_rewind_pop(&_gb);
569 if (!GB_rewind_pop(&_gb)) {
570 _rewind = self.view.isRewinding;
571 }
572 }
573 else {
574 GB_run(&_gb);
575 }
576 if (unlikely(_pendingAtomicBlock)) {
577 _pendingAtomicBlock();
578 _pendingAtomicBlock = nil;
579 }
580 }
581 }
582 [self postRun];
583 _stopping = false;
584 }
585
586 - (void)postRun
587 {
588 [_hexTimer invalidate];
589 [_batterySaveTimer invalidate];
590 [_audioLock lock];
591 _audioBufferPosition = _audioBufferNeeded = 0;
592 [_audioLock signal];
593 [_audioLock unlock];
594 [_audioClient stop];
595 _audioClient = nil;
596 self.view.mouseHidingEnabled = false;
597 GB_save_battery(&_gb, self.savPath.UTF8String);
598 GB_save_cheats(&_gb, self.chtPath.UTF8String);
599 unsigned time_to_alarm = GB_time_to_alarm(&_gb);
600
601 if (time_to_alarm) {
602 [NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate];
603 NSUserNotification *notification = [[NSUserNotification alloc] init];
604 NSString *friendlyName = [[self.fileURL lastPathComponent] stringByDeletingPathExtension];
605 NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil];
606 friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""];
607 friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
608
609 notification.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName];
610 notification.informativeText = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName];
611 notification.identifier = self.fileURL.path;
612 notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm];
613 notification.soundName = NSUserNotificationDefaultSoundName;
614 [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification];
615 [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"];
616 }
617 [_view setRumble:0];
618 }
619
620 - (void) start
621 {
622 if (!self.mainWindow) {
623 NSLog(@"Bug, reference leak to Document %@", self);
624 return;
625 }
626 dispatch_async(dispatch_get_main_queue(), ^{
627 [self updateDebuggerButtons];
628 [_slave updateDebuggerButtons];
629 });
630 self.gbsPlayPauseButton.state = true;
631 self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0;
632 if (_master) {
633 [_master start];
634 return;
635 }
636 if (_running) return;
637 _running = true;
638 NSThread *emulationThraed = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
639 _emulationThread = emulationThraed;
640 [emulationThraed start];
641 }
642
643 - (void) stop
644 {
645 dispatch_async(dispatch_get_main_queue(), ^{
646 [self updateDebuggerButtons];
647 [_slave updateDebuggerButtons];
648 });
649 self.gbsPlayPauseButton.state = false;
650 if (_master) {
651 if (!_master->_running) return;
652 GB_debugger_set_disabled(&_gb, true);
653 if (GB_debugger_is_stopped(&_gb)) {
654 [self interruptDebugInputRead];
655 }
656 [_master stop];
657 GB_debugger_set_disabled(&_gb, false);
658 return;
659 }
660 if (!_running) return;
661 GB_debugger_set_disabled(&_gb, true);
662 if (GB_debugger_is_stopped(&_gb)) {
663 [self interruptDebugInputRead];
664 }
665 [_audioLock lock];
666 _stopping = true;
667 [_audioLock signal];
668 [_audioLock unlock];
669 _running = false;
670 while (_stopping) {
671 [_audioLock lock];
672 [_audioLock signal];
673 [_audioLock unlock];
674 }
675 GB_debugger_set_disabled(&_gb, false);
676 }
677
678 - (NSString *)bootROMPathForName:(NSString *)name
679 {
680 NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"];
681 if (url) {
682 NSString *path = [url path];
683 path = [path stringByAppendingPathComponent:name];
684 path = [path stringByAppendingPathExtension:@"bin"];
685 if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
686 return path;
687 }
688 }
689
690 return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"];
691 }
692
693 - (void)loadBootROM: (GB_boot_rom_t)type
694 {
695 static NSString *const names[] = {
696 [GB_BOOT_ROM_DMG_0] = @"dmg0_boot",
697 [GB_BOOT_ROM_DMG] = @"dmg_boot",
698 [GB_BOOT_ROM_MGB] = @"mgb_boot",
699 [GB_BOOT_ROM_SGB] = @"sgb_boot",
700 [GB_BOOT_ROM_SGB2] = @"sgb2_boot",
701 [GB_BOOT_ROM_CGB_0] = @"cgb0_boot",
702 [GB_BOOT_ROM_CGB] = @"cgb_boot",
703 [GB_BOOT_ROM_CGB_E] = @"cgbE_boot",
704 [GB_BOOT_ROM_AGB_0] = @"agb0_boot",
705 [GB_BOOT_ROM_AGB] = @"agb_boot",
706 };
707 NSString *name = names[type];
708 NSString *path = [self bootROMPathForName:name];
709 /* These boot types are not commonly available, and they are indentical
710 from an emulator perspective, so fall back to the more common variants
711 if they can't be found. */
712 if (!path && type == GB_BOOT_ROM_CGB_E) {
713 [self loadBootROM:GB_BOOT_ROM_CGB];
714 return;
715 }
716 if (!path && type == GB_BOOT_ROM_AGB_0) {
717 [self loadBootROM:GB_BOOT_ROM_AGB];
718 return;
719 }
720 GB_load_boot_rom(&_gb, [path UTF8String]);
721 }
722
723 - (enum model)bestModelForROM
724 {
725 uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL);
726 if (!rom) return MODEL_CGB;
727
728 if (rom[0x143] & 0x80) { // Has CGB features
729 return MODEL_CGB;
730 }
731 if (rom[0x146] == 3) { // Has SGB features
732 return MODEL_SGB;
733 }
734
735 if (rom[0x14B] == 1) { // Nintendo-licensed (most likely has boot ROM palettes)
736 return MODEL_CGB;
737 }
738
739 if (rom[0x14B] == 0x33 &&
740 rom[0x144] == '0' &&
741 rom[0x145] == '1') { // Ditto
742 return MODEL_CGB;
743 }
744
745 return MODEL_DMG;
746 }
747
748 - (IBAction)reset:(id)sender
749 {
750 [self stop];
751 size_t old_width = GB_get_screen_width(&_gb);
752
753 if ([sender tag] > MODEL_NONE) {
754 /* User explictly selected a model, save the preference */
755 _currentModel = (enum model)[sender tag];
756 _usesAutoModel = _currentModel == MODEL_AUTO;
757 [[NSUserDefaults standardUserDefaults] setInteger:_currentModel forKey:@"GBEmulatedModel"];
758 }
759
760 /* Reload the ROM, SAV and SYM files */
761 [self loadROM];
762
763 if ([sender tag] == MODEL_QUICK_RESET) {
764 GB_quick_reset(&_gb);
765 }
766 else {
767 GB_switch_model_and_reset(&_gb, [self internalModel]);
768 }
769
770 if (old_width != GB_get_screen_width(&_gb)) {
771 [self.view screenSizeChanged];
772 }
773 [self updateMinSize];
774
775 [self start];
776 if (_gbsTracks) {
777 [self changeGBSTrack:sender];
778 }
779
780 if (_hexController) {
781 /* Verify bank sanity, especially when switching models. */
782 [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:0];
783 [self hexUpdateBank:self.memoryBankInput ignoreErrors:true];
784 }
785
786 char title[17];
787 GB_get_rom_title(&_gb, title);
788 [self.osdView displayText:[NSString stringWithFormat:@"SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&_gb)]];
789 }
790
791 - (IBAction)togglePause:(id)sender
792 {
793 if (_master) {
794 [_master togglePause:sender];
795 return;
796 }
797 if (_running) {
798 [self stop];
799 }
800 else {
801 [self start];
802 }
803 }
804
805 - (void)dealloc
806 {
807 [_cameraSession stopRunning];
808 self.view.gb = NULL;
809 GB_free(&_gb);
810 if (_cameraImage) {
811 CVBufferRelease(_cameraImage);
812 }
813 if (_audioBuffer) {
814 free(_audioBuffer);
815 }
816 }
817
818 - (void)batteryTimerExpired
819 {
820 if (_dirtyBattery && !GB_get_battery_dirty(&_gb)) {
821 GB_save_battery(&_gb, self.savPath.UTF8String);
822 }
823
824 _dirtyBattery = GB_get_battery_dirty(&_gb);
825 GB_clear_battery_dirty(&_gb);
826 }
827
828 - (NSFont *)debuggerFontOfSize:(unsigned)size
829 {
830 if (!size) {
831 size = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"];
832 }
833
834 bool retry = false;
835
836 again:;
837 NSString *selectedFont = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBDebuggerFont"];
838 if (@available(macOS 10.15, *)) {
839 if ([selectedFont isEqual:@"SF Mono"]) {
840 return [NSFont monospacedSystemFontOfSize:size weight:NSFontWeightRegular];
841 }
842 }
843
844 NSFont *ret = [NSFont fontWithName:selectedFont size:size];
845 if (ret) return ret;
846
847 if (retry) {
848 return [NSFont userFixedPitchFontOfSize:size];
849 }
850 [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBDebuggerFont"];
851 retry = true;
852 goto again;
853 }
854
855 - (void)updateFonts
856 {
857 _hexController.font = [self debuggerFontOfSize:12];
858 [self.paletteView reloadData:self];
859 [self.objectView reloadData:self];
860
861 NSFont *newFont = [self debuggerFontOfSize:0];
862 NSFont *newBoldFont = [[NSFontManager sharedFontManager] convertFont:newFont toHaveTrait:NSBoldFontMask];
863 self.debuggerSideViewInput.font = newFont;
864
865 unsigned inputHeight = MAX(ceil([@" " sizeWithAttributes:@{
866 NSFontAttributeName: newFont
867 }].height) + 6, 26);
868
869
870 NSRect frame = _consoleInput.frame;
871 unsigned oldHeight = frame.size.height;
872 frame.size.height = inputHeight;
873 _consoleInput.frame = frame;
874
875 frame = _debugBar.frame;
876 frame.origin.y += (signed)(inputHeight - oldHeight);
877 _debugBar.frame = frame;
878
879 frame = _debuggerScrollView.frame;
880 frame.origin.y += (signed)(inputHeight - oldHeight);
881 frame.size.height -= (signed)(inputHeight - oldHeight);
882 _debuggerScrollView.frame = frame;
883
884 _consoleInput.font = newFont;
885
886 for (NSTextView *view in @[_debuggerSideView, _consoleOutput]) {
887 NSMutableAttributedString *newString = view.attributedString.mutableCopy;
888 [view.attributedString enumerateAttribute:NSFontAttributeName
889 inRange:NSMakeRange(0, view.attributedString.length)
890 options:0
891 usingBlock:^(NSFont *value, NSRange range, BOOL *stop) {
892 if ([[NSFontManager sharedFontManager] fontNamed:value.fontName hasTraits:NSBoldFontMask]) {
893 [newString addAttributes:@{
894 NSFontAttributeName: newBoldFont
895 } range:range];
896 }
897 else {
898 [newString addAttributes:@{
899 NSFontAttributeName: newFont
900 } range:range];
901 }
902 }];
903 [view.textStorage setAttributedString:newString];
904 }
905 [_consoleOutput scrollToEndOfDocument:nil];
906 }
907
908 - (void)windowControllerDidLoadNib:(NSWindowController *)aController
909 {
910 [super windowControllerDidLoadNib:aController];
911 // Interface Builder bug?
912 [self.consoleWindow setContentSize:self.consoleWindow.frame.size];
913 /* Close Open Panels, if any */
914 for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
915 if ([window isKindOfClass:[NSOpenPanel class]]) {
916 [(NSOpenPanel *)window cancel:self];
917 }
918 }
919
920 NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init];
921 [paragraph_style setLineSpacing:2];
922
923 self.debuggerSideViewInput.font = [self debuggerFontOfSize:0];
924 self.debuggerSideViewInput.textColor = [NSColor whiteColor];
925 self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style;
926 [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"];
927 ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &_gb;
928 [[NSNotificationCenter defaultCenter] addObserver:self
929 selector:@selector(updateSideView)
930 name:NSTextDidChangeNotification
931 object:self.debuggerSideViewInput];
932
933 self.consoleOutput.textContainerInset = NSMakeSize(4, 4);
934 [self.view becomeFirstResponder];
935 self.view.frameBlendingMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"];
936 CGRect window_frame = self.mainWindow.frame;
937 window_frame.size.width = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowWidth"],
938 window_frame.size.width);
939 window_frame.size.height = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowHeight"],
940 window_frame.size.height);
941 [self.mainWindow setFrame:window_frame display:true];
942 self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised;
943
944 NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height;
945 CGRect vram_window_rect = self.vramWindow.frame;
946 vram_window_rect.size.height = 384 + height_diff + 48;
947 [self.vramWindow setFrame:vram_window_rect display:true animate:false];
948
949
950 if (@available(macOS 11.0, *)) {
951 self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent];
952 self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent];
953 self.vramWindow.subtitle = [self.fileURL.path lastPathComponent];
954 }
955 else {
956 self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]];
957 self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [self.fileURL.path lastPathComponent]];
958 self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [self.fileURL.path lastPathComponent]];
959 }
960
961 self.consoleWindow.level = NSNormalWindowLevel;
962
963 self.debuggerSplitView.dividerColor = self.debuggerVerticalLine.borderColor;
964 [self.debuggerVerticalLine removeFromSuperview]; // No longer used, just there for the color
965 if (@available(macOS 11.0, *)) {
966 self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded;
967 self.printerFeedWindow.toolbarStyle = NSWindowToolbarStyleUnifiedCompact;
968 self.printerFeedWindow.toolbar.items[1].image =
969 [NSImage imageWithSystemSymbolName:@"square.and.arrow.down"
970 accessibilityDescription:@"Save"];
971 self.printerFeedWindow.toolbar.items[2].image =
972 [NSImage imageWithSystemSymbolName:@"printer"
973 accessibilityDescription:@"Print"];
974 self.printerFeedWindow.toolbar.items[1].bordered = false;
975 self.printerFeedWindow.toolbar.items[2].bordered = false;
976 }
977 else {
978 NSToolbarItem *spinner = self.printerFeedWindow.toolbar.items[0];
979 [self.printerFeedWindow.toolbar removeItemAtIndex:0];
980 [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:spinner.itemIdentifier atIndex:2];
981 [self.printerFeedWindow.toolbar removeItemAtIndex:1];
982 [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:NSToolbarPrintItemIdentifier
983 atIndex:1];
984 [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier
985 atIndex:2];
986 }
987
988
989 [[NSNotificationCenter defaultCenter] addObserver:self
990 selector:@selector(updatePalette)
991 name:@"GBColorPaletteChanged"
992 object:nil];
993
994 __unsafe_unretained Document *weakSelf = self;
995 [self observeStandardDefaultsKey:@"GBFrameBlendingMode"
996 withBlock:^(NSNumber *value) {
997 weakSelf.view.frameBlendingMode = (GB_frame_blending_mode_t)value.unsignedIntValue;
998 }];
999
1000 [self observeStandardDefaultsKey:@"GBDMGModel" withBlock:^(id newValue) {
1001 weakSelf->_modelsChanging = true;
1002 if (weakSelf->_currentModel == MODEL_DMG) {
1003 [weakSelf reset:nil];
1004 }
1005 weakSelf->_modelsChanging = false;
1006 }];
1007
1008 [self observeStandardDefaultsKey:@"GBSGBModel" withBlock:^(id newValue) {
1009 weakSelf->_modelsChanging = true;
1010 if (weakSelf->_currentModel == MODEL_SGB) {
1011 [weakSelf reset:nil];
1012 }
1013 weakSelf->_modelsChanging = false;
1014 }];
1015
1016 [self observeStandardDefaultsKey:@"GBCGBModel" withBlock:^(id newValue) {
1017 weakSelf->_modelsChanging = true;
1018 if (weakSelf->_currentModel == MODEL_CGB) {
1019 [weakSelf reset:nil];
1020 }
1021 weakSelf->_modelsChanging = false;
1022 }];
1023
1024 [self observeStandardDefaultsKey:@"GBAGBModel" withBlock:^(id newValue) {
1025 weakSelf->_modelsChanging = true;
1026 if (weakSelf->_currentModel == MODEL_AGB) {
1027 [weakSelf reset:nil];
1028 }
1029 weakSelf->_modelsChanging = false;
1030 }];
1031
1032
1033 [self observeStandardDefaultsKey:@"GBVolume" withBlock:^(id newValue) {
1034 weakSelf->_volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"];
1035 }];
1036
1037
1038 _currentModel = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBEmulatedModel"];
1039 _usesAutoModel = _currentModel == MODEL_AUTO;
1040
1041 [self initCommon];
1042 self.view.gb = &_gb;
1043 self.view.osdView = _osdView;
1044 [self.view screenSizeChanged];
1045 if ([self loadROM]) {
1046 _mainWindow.alphaValue = 0; // Hack hack ugly hack
1047 dispatch_async(dispatch_get_main_queue(), ^{
1048 [self close];
1049 });
1050 }
1051 else {
1052 [self reset:nil];
1053 }
1054 }
1055
1056 - (void)initMemoryView
1057 {
1058 _hexController = [[HFController alloc] init];
1059 _hexController.font = [self debuggerFontOfSize:12];
1060 [_hexController setBytesPerColumn:1];
1061 [_hexController setEditMode:HFOverwriteMode];
1062
1063 [_hexController setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]];
1064
1065 /* 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. */
1066 HFLayoutRepresenter *layoutRep = [[HFLayoutRepresenter alloc] init];
1067 HFHexTextRepresenter *hexRep = [[HFHexTextRepresenter alloc] init];
1068 HFStringEncodingTextRepresenter *asciiRep = [[HFStringEncodingTextRepresenter alloc] init];
1069 HFVerticalScrollerRepresenter *scrollRep = [[HFVerticalScrollerRepresenter alloc] init];
1070 _lineRep = [[HFLineCountingRepresenter alloc] init];
1071 _statusRep = [[GBHexStatusBarRepresenter alloc] init];
1072 _statusRep.gb = &_gb;
1073 _statusRep.bankForDescription = -1;
1074
1075 _lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal;
1076
1077 /* Add all our reps to the controller. */
1078 [_hexController addRepresenter:layoutRep];
1079 [_hexController addRepresenter:hexRep];
1080 [_hexController addRepresenter:asciiRep];
1081 [_hexController addRepresenter:scrollRep];
1082 [_hexController addRepresenter:_lineRep];
1083 [_hexController addRepresenter:_statusRep];
1084
1085 /* Tell the layout rep which reps it should lay out. */
1086 [layoutRep addRepresenter:hexRep];
1087 [layoutRep addRepresenter:scrollRep];
1088 [layoutRep addRepresenter:asciiRep];
1089 [layoutRep addRepresenter:_lineRep];
1090 [layoutRep addRepresenter:_statusRep];
1091
1092
1093 [(NSView *)[hexRep view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
1094
1095 /* Grab the layout rep's view and stick it into our container. */
1096 NSView *layoutView = [layoutRep view];
1097 NSRect layoutViewFrame = self.memoryView.frame;
1098 [layoutView setFrame:layoutViewFrame];
1099 [layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin];
1100 [self.memoryView addSubview:layoutView];
1101 self.memoryView = layoutView;
1102
1103 CGSize contentSize = _memoryWindow.contentView.frame.size;
1104 while (_hexController.bytesPerLine < 16) {
1105 contentSize.width += 4;
1106 [_memoryWindow setContentSize:contentSize];
1107 }
1108 while (_hexController.bytesPerLine > 16) {
1109 contentSize.width -= 4;
1110 [_memoryWindow setContentSize:contentSize];
1111 }
1112
1113 self.memoryBankItem.enabled = false;
1114 }
1115
1116 - (NSString *)windowNibName
1117 {
1118 // Override returning the nib file name of the document
1119 // 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.
1120 return @"Document";
1121 }
1122
1123 - (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type
1124 {
1125 return true;
1126 }
1127
1128 - (IBAction)changeGBSTrack:(id)sender
1129 {
1130 if (!_running) {
1131 [self start];
1132 }
1133 [self performAtomicBlock:^{
1134 GB_gbs_switch_track(&_gb, self.gbsTracks.indexOfSelectedItem);
1135 }];
1136 }
1137 - (IBAction)gbsNextPrevPushed:(id)sender
1138 {
1139 if (self.gbsNextPrevButton.selectedSegment == 0) {
1140 // Previous
1141 if (self.gbsTracks.indexOfSelectedItem == 0) {
1142 [self.gbsTracks selectItemAtIndex:self.gbsTracks.numberOfItems - 1];
1143 }
1144 else {
1145 [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem - 1];
1146 }
1147 }
1148 else {
1149 // Next
1150 if (self.gbsTracks.indexOfSelectedItem == self.gbsTracks.numberOfItems - 1) {
1151 [self.gbsTracks selectItemAtIndex: 0];
1152 }
1153 else {
1154 [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem + 1];
1155 }
1156 }
1157 [self changeGBSTrack:sender];
1158 }
1159
1160 - (void)prepareGBSInterface: (GB_gbs_info_t *)info
1161 {
1162 GB_set_rendering_disabled(&_gb, true);
1163 _view = nil;
1164 for (NSView *view in [_mainWindow.contentView.subviews copy]) {
1165 [view removeFromSuperview];
1166 }
1167 if (@available(macOS 11, *)) {
1168 [[NSBundle mainBundle] loadNibNamed:@"GBS11" owner:self topLevelObjects:nil];
1169 }
1170 else {
1171 [[NSBundle mainBundle] loadNibNamed:@"GBS" owner:self topLevelObjects:nil];
1172 }
1173 [_mainWindow setContentSize:self.gbsPlayerView.bounds.size];
1174 _mainWindow.styleMask &= ~NSWindowStyleMaskResizable;
1175 dispatch_async(dispatch_get_main_queue(), ^{ // Cocoa is weird, no clue why it's needed
1176 [_mainWindow standardWindowButton:NSWindowZoomButton].enabled = false;
1177 });
1178 [_mainWindow.contentView addSubview:self.gbsPlayerView];
1179 _mainWindow.movableByWindowBackground = true;
1180 [_mainWindow setContentBorderThickness:24 forEdge:NSRectEdgeMinY];
1181
1182 self.gbsTitle.stringValue = [NSString stringWithCString:info->title encoding:NSISOLatin1StringEncoding] ?: @"GBS Player";
1183 self.gbsAuthor.stringValue = [NSString stringWithCString:info->author encoding:NSISOLatin1StringEncoding] ?: @"Unknown Composer";
1184 NSString *copyright = [NSString stringWithCString:info->copyright encoding:NSISOLatin1StringEncoding];
1185 if (copyright) {
1186 copyright = [@"©" stringByAppendingString:copyright];
1187 }
1188 self.gbsCopyright.stringValue = copyright ?: @"Missing copyright information";
1189 for (unsigned i = 0; i < info->track_count; i++) {
1190 [self.gbsTracks addItemWithTitle:[NSString stringWithFormat:@"Track %u", i + 1]];
1191 }
1192 [self.gbsTracks selectItemAtIndex:info->first_track];
1193 self.gbsPlayPauseButton.image.template = true;
1194 self.gbsPlayPauseButton.alternateImage.template = true;
1195 self.gbsRewindButton.image.template = true;
1196 for (unsigned i = 0; i < 2; i++) {
1197 [self.gbsNextPrevButton imageForSegment:i].template = true;
1198 }
1199
1200 if (!_audioClient.isPlaying) {
1201 [_audioClient start];
1202 }
1203
1204 if (@available(macOS 10.10, *)) {
1205 _mainWindow.titlebarAppearsTransparent = true;
1206 }
1207
1208 if (@available(macOS 26.0, *)) {
1209 // There's a new minimum width for segmented controls in Solarium
1210 NSRect frame = _gbsNextPrevButton.frame;
1211 frame.origin.x -= 16;
1212 _gbsNextPrevButton.frame = frame;
1213
1214 frame = _gbsTracks.frame;
1215 frame.size.width -= 16;
1216 _gbsTracks.frame = frame;
1217 }
1218 }
1219
1220 - (bool)isCartContainer
1221 {
1222 return [self.fileName.pathExtension.lowercaseString isEqualToString:@"gbcart"];
1223 }
1224
1225 - (NSString *)savPath
1226 {
1227 if (self.isCartContainer) {
1228 return [self.fileName stringByAppendingPathComponent:@"battery.sav"];
1229 }
1230
1231 return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path;
1232 }
1233
1234 - (NSString *)chtPath
1235 {
1236 if (self.isCartContainer) {
1237 return [self.fileName stringByAppendingPathComponent:@"cheats.cht"];
1238 }
1239
1240 return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path;
1241 }
1242
1243 - (NSString *)saveStatePath:(unsigned)index
1244 {
1245 if (self.isCartContainer) {
1246 return [self.fileName stringByAppendingPathComponent:[NSString stringWithFormat:@"state.s%u", index]];
1247 }
1248 return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%u", index]].path;
1249 }
1250
1251 - (NSString *)romPath
1252 {
1253 NSString *fileName = self.fileName;
1254 if (self.isCartContainer) {
1255 NSArray *paths = [[NSString stringWithContentsOfFile:[fileName stringByAppendingPathComponent:@"rom.gbl"]
1256 encoding:NSUTF8StringEncoding
1257 error:nil] componentsSeparatedByString:@"\n"];
1258 fileName = nil;
1259 bool needsRebuild = false;
1260 for (NSString *path in paths) {
1261 NSURL *url = [NSURL URLWithString:path relativeToURL:self.fileURL];
1262 if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
1263 if (fileName && ![fileName isEqualToString:url.path]) {
1264 needsRebuild = true;
1265 break;
1266 }
1267 fileName = url.path;
1268 }
1269 else {
1270 needsRebuild = true;
1271 }
1272 }
1273 if (fileName && needsRebuild) {
1274 [[NSString stringWithFormat:@"%@\n%@\n%@",
1275 [fileName pathRelativeToDirectory:self.fileName],
1276 fileName,
1277 [[NSURL fileURLWithPath:fileName].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")]]
1278 writeToFile:[self.fileName stringByAppendingPathComponent:@"rom.gbl"]
1279 atomically:false
1280 encoding:NSUTF8StringEncoding
1281 error:nil];
1282 }
1283 }
1284
1285 return fileName;
1286 }
1287
1288 static bool is_path_writeable(const char *path)
1289 {
1290 if (!access(path, W_OK)) return true;
1291 int fd = creat(path, 0644);
1292 if (fd == -1) return false;
1293 close(fd);
1294 unlink(path);
1295 return true;
1296 }
1297
1298 - (int)loadROM
1299 {
1300 __block int ret = 0;
1301 NSString *fileName = self.romPath;
1302 if (!fileName) {
1303 NSAlert *alert = [[NSAlert alloc] init];
1304 [alert setMessageText:@"Could not locate the ROM referenced by this Game Boy Cartridge"];
1305 [alert setAlertStyle:NSAlertStyleCritical];
1306 [alert runModal];
1307 return 1;
1308 }
1309
1310 NSString *rom_warnings = [self captureOutputForBlock:^{
1311 if (!_romModified) {
1312 GB_debugger_clear_symbols(&_gb);
1313 if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"isx"]) {
1314 ret = GB_load_isx(&_gb, fileName.UTF8String);
1315 if (!self.isCartContainer) {
1316 GB_load_battery(&_gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"ram"].path.UTF8String);
1317 }
1318 }
1319 else if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"gbs"]) {
1320 __block GB_gbs_info_t info;
1321 ret = GB_load_gbs(&_gb, fileName.UTF8String, &info);
1322 [self prepareGBSInterface:&info];
1323 }
1324 else {
1325 ret = GB_load_rom(&_gb, [fileName UTF8String]);
1326 }
1327 }
1328 if (GB_save_battery_size(&_gb)) {
1329 if (!is_path_writeable(self.savPath.UTF8String)) {
1330 GB_log(&_gb, "The save path for this ROM is not writeable, progress will not be saved.\n");
1331 }
1332 }
1333 GB_load_battery(&_gb, self.savPath.UTF8String);
1334 GB_load_cheats(&_gb, self.chtPath.UTF8String, true);
1335 dispatch_async(dispatch_get_main_queue(), ^{
1336 [self.cheatWindowController cheatsUpdated];
1337 });
1338 GB_debugger_load_symbol_file(&_gb, [[[NSBundle mainBundle] pathForResource:@"registers" ofType:@"sym"] UTF8String]);
1339 GB_debugger_load_symbol_file(&_gb, [[fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sym"].UTF8String);
1340 }];
1341 if (ret) {
1342 NSAlert *alert = [[NSAlert alloc] init];
1343 [alert setMessageText:rom_warnings?: @"Could not load ROM"];
1344 [alert setAlertStyle:NSAlertStyleCritical];
1345 [alert runModal];
1346 }
1347 else if (rom_warnings && !_romWarningIssued) {
1348 _romWarningIssued = true;
1349 [GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow];
1350 }
1351 _fileModificationTime = [[NSFileManager defaultManager] attributesOfItemAtPath:fileName error:nil][NSFileModificationDate];
1352 if (_usesAutoModel) {
1353 _currentModel = [self bestModelForROM];
1354 }
1355 return ret;
1356 }
1357
1358 - (void)showWindows
1359 {
1360 if (GB_is_inited(&_gb)) {
1361 if (![_fileModificationTime isEqualToDate:[[NSFileManager defaultManager] attributesOfItemAtPath:self.fileName error:nil][NSFileModificationDate]]) {
1362 [self reset:nil];
1363 }
1364 }
1365 [super showWindows];
1366 }
1367
1368 - (void)close
1369 {
1370 [self disconnectLinkCable];
1371 if (!self.gbsPlayerView) {
1372 [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"];
1373 [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"];
1374 }
1375 [self stop];
1376 [_consoleOutputLock lock];
1377 [_consoleOutputTimer invalidate];
1378 [_consoleOutputLock unlock];
1379 [_consoleWindow close];
1380 _consoleWindow = nil;
1381 [_memoryWindow close];
1382 _memoryWindow = nil;
1383 [_vramWindow close];
1384 _vramWindow = nil;
1385 [_printerFeedWindow close];
1386 _printerFeedWindow = nil;
1387 [_cheatsWindow close];
1388 _cheatsWindow = nil;
1389 [_cheatSearchController.window close];
1390 _cheatSearchController.window = nil;
1391 [super close];
1392 }
1393
1394 - (IBAction) interrupt:(id)sender
1395 {
1396 [self log:"^C\n"];
1397 GB_debugger_break(&_gb);
1398 [self start];
1399 [self.consoleWindow makeKeyAndOrderFront:nil];
1400 double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
1401 _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
1402 [self.consoleInput becomeFirstResponder];
1403 }
1404
1405 - (IBAction)mute:(id)sender
1406 {
1407 if (_audioClient.isPlaying) {
1408 [_audioClient stop];
1409 }
1410 else {
1411 [_audioClient start];
1412 if (_volume == 0) {
1413 [GBWarningPopover popoverWithContents:@"Warning: Volume is set to to zero in the preferences panel" onWindow:self.mainWindow];
1414 }
1415 }
1416 [[NSUserDefaults standardUserDefaults] setBool:!_audioClient.isPlaying forKey:@"Mute"];
1417 }
1418
1419 - (bool) isPaused
1420 {
1421 if (self.partner) {
1422 return !self.partner->_running || GB_debugger_is_stopped(&_gb) || GB_debugger_is_stopped(&self.partner->_gb);
1423 }
1424 return (!_running) || GB_debugger_is_stopped(&_gb);
1425 }
1426
1427 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem
1428 {
1429 if ([anItem action] == @selector(mute:)) {
1430 if (_running) {
1431 [(NSMenuItem *)anItem setState:!_audioClient.isPlaying];
1432 }
1433 else {
1434 [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]];
1435 }
1436 }
1437 else if ([anItem action] == @selector(togglePause:)) {
1438 [(NSMenuItem *)anItem setState:self.isPaused];
1439 return !GB_debugger_is_stopped(&_gb);
1440 }
1441 else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE && anItem.tag != MODEL_QUICK_RESET) {
1442 [(NSMenuItem *)anItem setState:(anItem.tag == _currentModel) || (anItem.tag == MODEL_AUTO && _usesAutoModel)];
1443 }
1444 else if ([anItem action] == @selector(interrupt:)) {
1445 if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
1446 return false;
1447 }
1448 }
1449 else if ([anItem action] == @selector(disconnectAllAccessories:)) {
1450 [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_NONE && !self.partner];
1451 }
1452 else if ([anItem action] == @selector(connectPrinter:)) {
1453 [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_PRINTER];
1454 }
1455 else if ([anItem action] == @selector(connectWorkboy:)) {
1456 [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_WORKBOY];
1457 }
1458 else if ([anItem action] == @selector(connectLinkCable:)) {
1459 [(NSMenuItem *)anItem setState:[(NSMenuItem *)anItem representedObject] == _master ||
1460 [(NSMenuItem *)anItem representedObject] == _slave];
1461 }
1462 else if ([anItem action] == @selector(toggleCheats:)) {
1463 [(NSMenuItem *)anItem setState:GB_cheats_enabled(&_gb)];
1464 }
1465 else if ([anItem action] == @selector(toggleDisplayBackground:)) {
1466 [(NSMenuItem *)anItem setState:!GB_is_background_rendering_disabled(&_gb)];
1467 }
1468 else if ([anItem action] == @selector(toggleDisplayObjects:)) {
1469 [(NSMenuItem *)anItem setState:!GB_is_object_rendering_disabled(&_gb)];
1470 }
1471 else if ([anItem action] == @selector(toggleAudioRecording:)) {
1472 [(NSMenuItem *)anItem setTitle:_isRecordingAudio? @"Stop Audio Recording" : @"Start Audio Recording…"];
1473 }
1474 else if ([anItem action] == @selector(toggleAudioChannel:)) {
1475 [(NSMenuItem *)anItem setState:!GB_is_channel_muted(&_gb, [anItem tag])];
1476 }
1477 else if ([anItem action] == @selector(increaseWindowSize:)) {
1478 return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionIncrease];
1479 }
1480 else if ([anItem action] == @selector(decreaseWindowSize:)) {
1481 return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionDecrease];
1482 }
1483 else if ([anItem action] == @selector(reloadROM:)) {
1484 return !_gbsTracks;
1485 }
1486 else if ([anItem action] == @selector(saveDocument:)) {
1487 return _romModified;
1488 }
1489 else if ([anItem action] == @selector(saveDocumentAs:)) {
1490 return _romModified && !self.isCartContainer;
1491 }
1492
1493 return [super validateUserInterfaceItem:anItem];
1494 }
1495
1496
1497 - (void) windowWillEnterFullScreen:(NSNotification *)notification
1498 {
1499 _fullScreen = true;
1500 self.view.mouseHidingEnabled = _running;
1501 }
1502
1503 - (void) windowWillExitFullScreen:(NSNotification *)notification
1504 {
1505 _fullScreen = false;
1506 self.view.mouseHidingEnabled = false;
1507 }
1508
1509 enum GBWindowResizeAction
1510 {
1511 GBWindowResizeActionZoom,
1512 GBWindowResizeActionIncrease,
1513 GBWindowResizeActionDecrease,
1514 };
1515
1516 - (bool)newRect:(NSRect *)rect forWindow:(NSWindow *)window action:(enum GBWindowResizeAction)action
1517 {
1518 if (_fullScreen) return false;
1519 if (!rect) {
1520 rect = alloca(sizeof(*rect));
1521 }
1522
1523 size_t width = GB_get_screen_width(&_gb),
1524 height = GB_get_screen_height(&_gb);
1525
1526 *rect = window.contentView.frame;
1527
1528 unsigned titlebarSize = window.contentView.superview.frame.size.height - rect->size.height;
1529
1530 unsigned stepX = width / [[window screen] backingScaleFactor];
1531 unsigned stepY = height / [[window screen] backingScaleFactor];
1532
1533 if (action == GBWindowResizeActionDecrease) {
1534 if (rect->size.width <= width || rect->size.height <= height) {
1535 return false;
1536 }
1537 }
1538
1539 typeof(floor) *roundFunc = action == GBWindowResizeActionDecrease? ceil : floor;
1540 unsigned currentFactor = MIN(roundFunc(rect->size.width / stepX), roundFunc(rect->size.height / stepY));
1541
1542 rect->size.width = currentFactor * stepX;
1543 rect->size.height = currentFactor * stepY + titlebarSize;
1544
1545 if (action == GBWindowResizeActionDecrease) {
1546 rect->size.width -= stepX;
1547 rect->size.height -= stepY;
1548 }
1549 else {
1550 rect->size.width += stepX;
1551 rect->size.height += stepY;
1552 }
1553
1554 NSRect maxRect = [_mainWindow screen].visibleFrame;
1555
1556 if (rect->size.width > maxRect.size.width ||
1557 rect->size.height > maxRect.size.height) {
1558 if (action == GBWindowResizeActionIncrease) {
1559 return false;
1560 }
1561 rect->size.width = width;
1562 rect->size.height = height + titlebarSize;
1563 }
1564
1565 rect->origin = window.frame.origin;
1566 if (action == GBWindowResizeActionZoom) {
1567 rect->origin.y -= rect->size.height - window.frame.size.height;
1568 }
1569 else {
1570 rect->origin.y -= (rect->size.height - window.frame.size.height) / 2;
1571 rect->origin.x -= (rect->size.width - window.frame.size.width) / 2;
1572 }
1573
1574 if (rect->origin.x < maxRect.origin.x) {
1575 rect->origin.x = maxRect.origin.x;
1576 }
1577
1578 if (rect->origin.y < maxRect.origin.y) {
1579 rect->origin.y = maxRect.origin.y;
1580 }
1581
1582 if (rect->origin.x + rect->size.width > maxRect.origin.x + maxRect.size.width) {
1583 rect->origin.x = maxRect.origin.x + maxRect.size.width - rect->size.width;
1584 }
1585
1586 if (rect->origin.y + rect->size.height > maxRect.origin.y + maxRect.size.height) {
1587 rect->origin.y = maxRect.origin.y + maxRect.size.height - rect->size.height;
1588 }
1589
1590 return true;
1591 }
1592
1593 - (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame
1594 {
1595 if (_fullScreen) {
1596 return newFrame;
1597 }
1598 [self newRect:&newFrame forWindow:window action:GBWindowResizeActionZoom];
1599 return newFrame;
1600 }
1601
1602
1603 - (IBAction)increaseWindowSize:(id)sender
1604 {
1605 NSRect rect;
1606 if ([self newRect:&rect forWindow:_mainWindow action:GBWindowResizeActionIncrease]) {
1607 [_mainWindow setFrame:rect display:true animate:true];
1608 }
1609 }
1610
1611 - (IBAction)decreaseWindowSize:(id)sender
1612 {
1613 NSRect rect;
1614 if ([self newRect:&rect forWindow:_mainWindow action:GBWindowResizeActionDecrease]) {
1615 [_mainWindow setFrame:rect display:true animate:true];
1616 }
1617 }
1618
1619 - (void)appendPendingOutput
1620 {
1621 [_consoleOutputLock lock];
1622 if (_shouldClearSideView) {
1623 _shouldClearSideView = false;
1624 [self.debuggerSideView setString:@""];
1625 }
1626 if (_pendingConsoleOutput) {
1627 NSTextView *textView = _logToSideView? self.debuggerSideView : self.consoleOutput;
1628
1629 [_hexController reloadData];
1630 [self reloadVRAMData: nil];
1631
1632 [textView.textStorage appendAttributedString:_pendingConsoleOutput];
1633 if (!_logToSideView) {
1634 [textView scrollToEndOfDocument:nil];
1635 }
1636 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
1637 [self.consoleWindow orderFront:nil];
1638 /* Make sure mouse is not hidden while debugging */
1639 self.view.mouseHidingEnabled = false;
1640 }
1641 _pendingConsoleOutput = nil;
1642 }
1643 [_consoleOutputLock unlock];
1644 }
1645
1646 - (void)log:(const char *)string withAttributes:(GB_log_attributes_t)attributes
1647 {
1648 NSString *nsstring = @(string); // For ref-counting
1649 if (_capturedOutput) {
1650 [_capturedOutput appendString:nsstring];
1651 return;
1652 }
1653
1654
1655 NSFont *font = [self debuggerFontOfSize:0];
1656 NSUnderlineStyle underline = NSUnderlineStyleNone;
1657 if (attributes & GB_LOG_BOLD) {
1658 font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask];
1659 }
1660
1661 if (attributes & GB_LOG_UNDERLINE_MASK) {
1662 underline = (attributes & GB_LOG_UNDERLINE_MASK) == GB_LOG_DASHED_UNDERLINE? NSUnderlinePatternDot | NSUnderlineStyleSingle : NSUnderlineStyleSingle;
1663 }
1664
1665 NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init];
1666 [paragraph_style setLineSpacing:2];
1667 NSMutableAttributedString *attributed =
1668 [[NSMutableAttributedString alloc] initWithString:nsstring
1669 attributes:@{NSFontAttributeName: font,
1670 NSForegroundColorAttributeName: [NSColor whiteColor],
1671 NSUnderlineStyleAttributeName: @(underline),
1672 NSParagraphStyleAttributeName: paragraph_style}];
1673
1674 [_consoleOutputLock lock];
1675 if (!_pendingConsoleOutput) {
1676 _pendingConsoleOutput = attributed;
1677 }
1678 else {
1679 [_pendingConsoleOutput appendAttributedString:attributed];
1680 }
1681
1682 if (![_consoleOutputTimer isValid]) {
1683 _consoleOutputTimer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:false];
1684 [[NSRunLoop mainRunLoop] addTimer:_consoleOutputTimer forMode:NSDefaultRunLoopMode];
1685 }
1686
1687 [_consoleOutputLock unlock];
1688 }
1689
1690 - (IBAction)showConsoleWindow:(id)sender
1691 {
1692 [self.consoleWindow orderFront:nil];
1693 double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
1694 _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
1695 }
1696
1697 - (void)queueDebuggerCommand:(NSString *)command
1698 {
1699 if (!_master && !_running && !GB_debugger_is_stopped(&_gb)) {
1700 _debuggerCommandWhilePaused = command;
1701 GB_debugger_break(&_gb);
1702 [self start];
1703 return;
1704 }
1705
1706 if (!_inSyncInput) {
1707 [self log:">"];
1708 }
1709 [self log:[command UTF8String]];
1710 [self log:"\n"];
1711 [_hasDebuggerInput lock];
1712 [_debuggerInputQueue addObject:command];
1713 [_hasDebuggerInput unlockWithCondition:1];
1714 }
1715
1716 - (IBAction)consoleInput:(NSTextField *)sender
1717 {
1718 NSString *line = [sender stringValue];
1719 if ([line isEqualToString:@""] && _lastConsoleInput) {
1720 line = _lastConsoleInput;
1721 }
1722 else if (line) {
1723 _lastConsoleInput = line;
1724 }
1725 else {
1726 line = @"";
1727 }
1728
1729 [self queueDebuggerCommand: line];
1730
1731 [sender setStringValue:@""];
1732 }
1733
1734 - (void) interruptDebugInputRead
1735 {
1736 [_hasDebuggerInput lock];
1737 [_debuggerInputQueue addObject:[NSNull null]];
1738 [_hasDebuggerInput unlockWithCondition:1];
1739 }
1740
1741 - (void) updateSideView
1742 {
1743 if (!GB_debugger_is_stopped(&_gb)) {
1744 return;
1745 }
1746
1747 if (![NSThread isMainThread]) {
1748 dispatch_sync(dispatch_get_main_queue(), ^{
1749 [self updateSideView];
1750 });
1751 return;
1752 }
1753
1754 [_consoleOutputLock lock];
1755 _shouldClearSideView = true;
1756 [self appendPendingOutput];
1757 _logToSideView = true;
1758 [_consoleOutputLock unlock];
1759
1760 for (NSString *line in [self.debuggerSideViewInput.string componentsSeparatedByString:@"\n"]) {
1761 NSString *stripped = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
1762 if ([stripped length]) {
1763 char *dupped = strdup([stripped UTF8String]);
1764 GB_attributed_log(&_gb, GB_LOG_BOLD, "%s:\n", dupped);
1765 GB_debugger_execute_command(&_gb, dupped);
1766 GB_log(&_gb, "\n");
1767 free(dupped);
1768 }
1769 }
1770
1771 [_consoleOutputLock lock];
1772 [self appendPendingOutput];
1773 _logToSideView = false;
1774 [_consoleOutputLock unlock];
1775 }
1776
1777 - (char *)getDebuggerInput
1778 {
1779 bool isPlaying = _audioClient.isPlaying;
1780 if (isPlaying) {
1781 [_audioClient stop];
1782 }
1783 [_audioLock lock];
1784 [_audioLock signal];
1785 [_audioLock unlock];
1786 _inSyncInput = true;
1787 [self updateSideView];
1788 dispatch_async(dispatch_get_main_queue(), ^{
1789 [self updateDebuggerButtons];
1790 });
1791 [self.partner updateDebuggerButtons];
1792 [self log:">"];
1793 if (_debuggerCommandWhilePaused) {
1794 NSString *command = _debuggerCommandWhilePaused;
1795 _debuggerCommandWhilePaused = nil;
1796 dispatch_async(dispatch_get_main_queue(), ^{
1797 [self queueDebuggerCommand:command];
1798 });
1799 }
1800 [_hasDebuggerInput lockWhenCondition:1];
1801 NSString *input = [_debuggerInputQueue firstObject];
1802 [_debuggerInputQueue removeObjectAtIndex:0];
1803 [_hasDebuggerInput unlockWithCondition:[_debuggerInputQueue count] != 0];
1804 _inSyncInput = false;
1805 _shouldClearSideView = true;
1806 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC / 10)), dispatch_get_main_queue(), ^{
1807 if (_shouldClearSideView) {
1808 _shouldClearSideView = false;
1809 [self.debuggerSideView setString:@""];
1810 }
1811 [self updateDebuggerButtons];
1812 [self.partner updateDebuggerButtons];
1813 });
1814 if (isPlaying) {
1815 [_audioClient start];
1816 }
1817 if ((id) input == [NSNull null]) {
1818 return NULL;
1819 }
1820 return strdup([input UTF8String]);
1821 }
1822
1823 - (char *) getAsyncDebuggerInput
1824 {
1825 [_hasDebuggerInput lock];
1826 NSString *input = [_debuggerInputQueue firstObject];
1827 if (input) {
1828 [_debuggerInputQueue removeObjectAtIndex:0];
1829 }
1830 [_hasDebuggerInput unlockWithCondition:[_debuggerInputQueue count] != 0];
1831 if ((id)input == [NSNull null]) {
1832 return NULL;
1833 }
1834 return input? strdup([input UTF8String]): NULL;
1835 }
1836
1837 - (IBAction)saveState:(id)sender
1838 {
1839 bool __block success = false;
1840 [self performAtomicBlock:^{
1841 success = GB_save_state(&_gb, [self saveStatePath:[sender tag]].UTF8String) == 0;
1842 }];
1843
1844 if (!success) {
1845 [GBWarningPopover popoverWithContents:@"Failed to write save state." onWindow:self.mainWindow];
1846 NSBeep();
1847 }
1848 else {
1849 [self.osdView displayText:@"State saved"];
1850 }
1851 }
1852
1853 - (int)loadStateFile:(const char *)path noErrorOnNotFound:(bool)noErrorOnFileNotFound;
1854 {
1855 int __block result = false;
1856 NSString *error =
1857 [self captureOutputForBlock:^{
1858 result = GB_load_state(&_gb, path);
1859 }];
1860
1861 if (result == ENOENT && noErrorOnFileNotFound) {
1862 return ENOENT;
1863 }
1864
1865 if (result) {
1866 NSBeep();
1867 }
1868 else {
1869 [self.osdView displayText:@"State loaded"];
1870 }
1871 if (error) {
1872 [GBWarningPopover popoverWithContents:error onWindow:self.mainWindow];
1873 }
1874 return result;
1875 }
1876
1877 - (IBAction)loadState:(id)sender
1878 {
1879 int ret = [self loadStateFile:[self saveStatePath:[sender tag]].UTF8String noErrorOnNotFound:true];
1880 if (ret == ENOENT && !self.isCartContainer) {
1881 [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"sn%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:false];
1882 }
1883 }
1884
1885 - (IBAction)clearConsole:(id)sender
1886 {
1887 [self.consoleOutput setString:@""];
1888 }
1889
1890 - (void)log:(const char *)log
1891 {
1892 [self log:log withAttributes:0];
1893 }
1894
1895 - (void)performAtomicBlock: (void (^)())block
1896 {
1897 while (!GB_is_inited(&_gb));
1898 bool isRunning = _running && !GB_debugger_is_stopped(&_gb);
1899 if (_master) {
1900 isRunning |= _master->_running;
1901 }
1902 if (!isRunning) {
1903 block();
1904 return;
1905 }
1906
1907 if (_master) {
1908 [_master performAtomicBlock:block];
1909 return;
1910 }
1911
1912 if ([NSThread currentThread] == _emulationThread) {
1913 block();
1914 return;
1915 }
1916
1917 _pendingAtomicBlock = block;
1918 while (_pendingAtomicBlock);
1919 }
1920
1921 - (NSString *)captureOutputForBlock: (void (^)())block
1922 {
1923 _capturedOutput = [[NSMutableString alloc] init];
1924 [self performAtomicBlock:block];
1925 NSString *ret = [_capturedOutput stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
1926 _capturedOutput = nil;
1927 return [ret length]? ret : nil;
1928 }
1929
1930 + (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale
1931 {
1932
1933 NSImage *ret = [[NSImage alloc] initWithSize:NSMakeSize(width * scale, height * scale)];
1934 NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
1935 pixelsWide:width
1936 pixelsHigh:height
1937 bitsPerSample:8
1938 samplesPerPixel:3
1939 hasAlpha:false
1940 isPlanar:false
1941 colorSpaceName:NSDeviceRGBColorSpace
1942 bitmapFormat:0
1943 bytesPerRow:4 * width
1944 bitsPerPixel:32];
1945 memcpy(rep.bitmapData, data.bytes, data.length);
1946 [ret addRepresentation:rep];
1947 return ret;
1948 }
1949
1950 - (void) reloadMemoryView
1951 {
1952 if (self.memoryWindow.isVisible) {
1953 [_hexController reloadData];
1954 }
1955 if (_cheatSearchController.window.isVisible) {
1956 if ([_cheatSearchController.tableView editedColumn] != 2) {
1957 [_cheatSearchController.tableView reloadData];
1958 }
1959 }
1960 }
1961
1962 - (IBAction) reloadVRAMData: (id) sender
1963 {
1964 if (self.vramWindow.isVisible) {
1965 uint8_t *io_regs = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_IO, NULL, NULL);
1966 switch ([self.vramTabView.tabViewItems indexOfObject:self.vramTabView.selectedTabViewItem]) {
1967 case 0:
1968 /* Tileset */
1969 {
1970 GB_palette_type_t palette_type = GB_PALETTE_NONE;
1971 NSUInteger palette_menu_index = self.tilesetPaletteButton.indexOfSelectedItem;
1972 if (palette_menu_index) {
1973 palette_type = palette_menu_index > 8? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND;
1974 }
1975 size_t bufferLength = 256 * 192 * 4;
1976 NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength];
1977 data.length = bufferLength;
1978 GB_draw_tileset(&_gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 1) & 7);
1979
1980 self.tilesetImageView.image = [Document imageFromData:data width:256 height:192 scale:1.0];
1981 self.tilesetImageView.layer.magnificationFilter = kCAFilterNearest;
1982 }
1983 break;
1984
1985 case 1:
1986 /* Tilemap */
1987 {
1988 GB_palette_type_t palette_type = GB_PALETTE_NONE;
1989 NSUInteger palette_menu_index = self.tilemapPaletteButton.indexOfSelectedItem;
1990 if (palette_menu_index > 1) {
1991 palette_type = palette_menu_index > 9? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND;
1992 }
1993 else if (palette_menu_index == 1) {
1994 palette_type = GB_PALETTE_AUTO;
1995 }
1996
1997 size_t bufferLength = 256 * 256 * 4;
1998 NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength];
1999 data.length = bufferLength;
2000 GB_draw_tilemap(&_gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 2) & 7,
2001 (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem,
2002 (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem);
2003
2004 self.tilemapImageView.scrollRect = NSMakeRect(io_regs[GB_IO_SCX],
2005 io_regs[GB_IO_SCY],
2006 160, 144);
2007 self.tilemapImageView.image = [Document imageFromData:data width:256 height:256 scale:1.0];
2008 self.tilemapImageView.layer.magnificationFilter = kCAFilterNearest;
2009 }
2010 break;
2011
2012 case 2:
2013 /* OAM */
2014 {
2015 _oamCount = GB_get_oam_info(&_gb, _oamInfo, &_oamHeight);
2016 dispatch_async(dispatch_get_main_queue(), ^{
2017 [self.objectView reloadData:self];
2018 });
2019 }
2020 break;
2021
2022 case 3:
2023 /* Palettes */
2024 {
2025 dispatch_async(dispatch_get_main_queue(), ^{
2026 [self.paletteView reloadData:self];
2027 });
2028 }
2029 break;
2030 }
2031 }
2032 }
2033
2034 - (IBAction) showMemory:(id)sender
2035 {
2036 if (!_hexController) {
2037 [self initMemoryView];
2038 }
2039 [self.memoryWindow makeKeyAndOrderFront:sender];
2040 }
2041
2042 - (IBAction)hexGoTo:(id)sender
2043 {
2044 NSString *expression = [sender stringValue];
2045 __block uint16_t addr = 0;
2046 __block uint16_t bank = 0;
2047 __block bool fail = false;
2048 NSString *error = [self captureOutputForBlock:^{
2049 if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) {
2050 fail = true;
2051 }
2052 }];
2053
2054 if (error) {
2055 NSBeep();
2056 [GBWarningPopover popoverWithContents:error onView:sender];
2057 }
2058 if (fail) return;
2059
2060
2061 if (bank != (typeof(bank))-1) {
2062 GB_memory_mode_t mode = [(GBMemoryByteArray *)(_hexController.byteArray) mode];
2063 if (addr < 0x4000) {
2064 if (bank == 0) {
2065 if (mode != GBMemoryROM && mode != GBMemoryEntireSpace) {
2066 mode = GBMemoryEntireSpace;
2067 }
2068 }
2069 else {
2070 addr |= 0x4000;
2071 mode = GBMemoryROM;
2072 }
2073 }
2074 else if (addr < 0x8000) {
2075 mode = GBMemoryROM;
2076 }
2077 else if (addr < 0xA000) {
2078 mode = GBMemoryVRAM;
2079 }
2080 else if (addr < 0xC000) {
2081 mode = GBMemoryExternalRAM;
2082 }
2083 else if (addr < 0xD000) {
2084 if (mode != GBMemoryRAM && mode != GBMemoryEntireSpace) {
2085 mode = GBMemoryEntireSpace;
2086 }
2087 }
2088 else if (addr < 0xE000) {
2089 mode = GBMemoryRAM;
2090 }
2091 else {
2092 mode = GBMemoryEntireSpace;
2093 }
2094 [_memorySpaceButton selectItemAtIndex:mode];
2095 [self hexUpdateSpace:_memorySpaceButton.cell];
2096 [_memoryBankInput setStringValue:[NSString stringWithFormat:@"$%02x", bank]];
2097 [self hexUpdateBank:_memoryBankInput];
2098 }
2099 addr -= _lineRep.valueOffset;
2100 if (addr >= _hexController.byteArray.length) {
2101 GB_log(&_gb, "Value $%04x is out of range.\n", addr);
2102 return;
2103 }
2104
2105 [_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]];
2106 [_hexController _ensureVisibilityOfLocation:addr];
2107 for (HFRepresenter *representer in _hexController.representers) {
2108 if ([representer isKindOfClass:[HFHexTextRepresenter class]]) {
2109 [self.memoryWindow makeFirstResponder:representer.view];
2110 break;
2111 }
2112 }
2113 }
2114
2115 - (void)hexUpdateBank:(NSControl *)sender ignoreErrors: (bool)ignore_errors
2116 {
2117 NSString *expression = [sender stringValue];
2118 __block uint16_t addr, bank;
2119 __block bool fail = false;
2120 NSString *error = [self captureOutputForBlock:^{
2121 if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) {
2122 fail = true;
2123 return;
2124 }
2125 }];
2126
2127 if (error && !ignore_errors) {
2128 NSBeep();
2129 [GBWarningPopover popoverWithContents:error onView:sender];
2130 }
2131
2132 if (fail) return;
2133
2134 if (bank == (uint16_t) -1) {
2135 bank = addr;
2136 }
2137
2138 uint16_t n_banks = 1;
2139 switch ([(GBMemoryByteArray *)(_hexController.byteArray) mode]) {
2140 case GBMemoryROM: {
2141 size_t rom_size;
2142 GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL);
2143 n_banks = rom_size / 0x4000;
2144 break;
2145 }
2146 case GBMemoryVRAM:
2147 n_banks = GB_is_cgb(&_gb) ? 2 : 1;
2148 break;
2149 case GBMemoryExternalRAM: {
2150 size_t ram_size;
2151 GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL);
2152 n_banks = (ram_size + 0x1FFF) / 0x2000;
2153 break;
2154 }
2155 case GBMemoryRAM:
2156 n_banks = GB_is_cgb(&_gb) ? 8 : 1;
2157 break;
2158 case GBMemoryEntireSpace:
2159 break;
2160 }
2161
2162 bank %= n_banks;
2163
2164 [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:bank];
2165 _statusRep.bankForDescription = bank;
2166 [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]];
2167 [_hexController reloadData];
2168 }
2169
2170 - (IBAction)hexUpdateBank:(NSControl *)sender
2171 {
2172 [self hexUpdateBank:sender ignoreErrors:false];
2173 }
2174
2175 - (IBAction)hexUpdateSpace:(NSPopUpButtonCell *)sender
2176 {
2177 self.memoryBankItem.enabled = [sender indexOfSelectedItem] != GBMemoryEntireSpace;
2178 [_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(0, 0)]]];
2179 GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(_hexController.byteArray);
2180 [byteArray setMode:(GB_memory_mode_t)[sender indexOfSelectedItem]];
2181 uint16_t bank = -1;
2182 switch ((GB_memory_mode_t)[sender indexOfSelectedItem]) {
2183 case GBMemoryEntireSpace:
2184 _statusRep.baseAddress = _lineRep.valueOffset = 0;
2185 break;
2186 case GBMemoryROM:
2187 _statusRep.baseAddress = _lineRep.valueOffset = 0;
2188 GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, &bank);
2189 break;
2190 case GBMemoryVRAM:
2191 _statusRep.baseAddress = _lineRep.valueOffset = 0x8000;
2192 GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_VRAM, NULL, &bank);
2193 break;
2194 case GBMemoryExternalRAM:
2195 _statusRep.baseAddress = _lineRep.valueOffset = 0xA000;
2196 GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, NULL, &bank);
2197 break;
2198 case GBMemoryRAM:
2199 _statusRep.baseAddress = _lineRep.valueOffset = 0xC000;
2200 GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_RAM, NULL, &bank);
2201 break;
2202 }
2203 byteArray.selectedBank = bank;
2204 _statusRep.bankForDescription = bank;
2205 [self.memoryBankInput setStringValue:(bank == (uint16_t)-1)? @"" :
2206 [NSString stringWithFormat:@"$%x", byteArray.selectedBank]];
2207
2208 [_hexController reloadData];
2209 for (NSView *view in self.memoryView.subviews) {
2210 [view setNeedsDisplay:true];
2211 }
2212 }
2213
2214 - (GB_gameboy_t *) gameboy
2215 {
2216 return &_gb;
2217 }
2218
2219 + (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName
2220 {
2221 return true;
2222 }
2223
2224 - (void)cameraRequestUpdate
2225 {
2226 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
2227 @try {
2228 if (!_cameraSession) {
2229 if (@available(macOS 10.14, *)) {
2230 switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
2231 case AVAuthorizationStatusAuthorized:
2232 break;
2233 case AVAuthorizationStatusNotDetermined: {
2234 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
2235 [self cameraRequestUpdate];
2236 }];
2237 return;
2238 }
2239 case AVAuthorizationStatusDenied:
2240 case AVAuthorizationStatusRestricted:
2241 GB_camera_updated(&_gb);
2242 return;
2243 }
2244 }
2245
2246 NSError *error;
2247 AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
2248 AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error];
2249 CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions([[device activeFormat] formatDescription]);
2250
2251 if (!input) {
2252 GB_camera_updated(&_gb);
2253 return;
2254 }
2255
2256 double ratio = MAX(130.0 / dimensions.width, 114.0 / dimensions.height);
2257
2258 _cameraOutput = [[AVCaptureStillImageOutput alloc] init];
2259 /* Greyscale is not widely supported, so we use YUV, whose first element is the brightness. */
2260 [_cameraOutput setOutputSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kYUVSPixelFormat),
2261 (id)kCVPixelBufferWidthKey: @(round(dimensions.width * ratio)),
2262 (id)kCVPixelBufferHeightKey: @(round(dimensions.height * ratio)),}];
2263
2264
2265 _cameraSession = [AVCaptureSession new];
2266 _cameraSession.sessionPreset = AVCaptureSessionPresetPhoto;
2267
2268 [_cameraSession addInput: input];
2269 [_cameraSession addOutput: _cameraOutput];
2270 [_cameraSession startRunning];
2271 _cameraConnection = [_cameraOutput connectionWithMediaType: AVMediaTypeVideo];
2272 }
2273
2274 [_cameraOutput captureStillImageAsynchronouslyFromConnection: _cameraConnection completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError *error) {
2275 if (error) {
2276 GB_camera_updated(&_gb);
2277 }
2278 else {
2279 if (_cameraImage) {
2280 CVBufferRelease(_cameraImage);
2281 _cameraImage = NULL;
2282 }
2283 _cameraImage = CVBufferRetain(CMSampleBufferGetImageBuffer(sampleBuffer));
2284 /* We only need the actual buffer, no need to ever unlock it. */
2285 CVPixelBufferLockBaseAddress(_cameraImage, 0);
2286 }
2287
2288 GB_camera_updated(&_gb);
2289 }];
2290 }
2291 @catch (NSException *exception) {
2292 /* I have not tested camera support on many devices, so we catch exceptions just in case. */
2293 GB_camera_updated(&_gb);
2294 }
2295 });
2296 }
2297
2298 - (uint8_t)cameraGetPixelAtX:(unsigned)x andY:(unsigned)y
2299 {
2300 if (!_cameraImage) {
2301 return 0;
2302 }
2303
2304 uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(_cameraImage);
2305 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_cameraImage);
2306 unsigned offsetX = (CVPixelBufferGetWidth(_cameraImage) - 128) / 2;
2307 unsigned offsetY = (CVPixelBufferGetHeight(_cameraImage) - 112) / 2;
2308 uint8_t ret = baseAddress[(x + offsetX) * 2 + (y + offsetY) * bytesPerRow];
2309
2310 return ret;
2311 }
2312
2313 - (IBAction)toggleTilesetGrid:(NSButton *)sender
2314 {
2315 if (sender.state) {
2316 self.tilesetImageView.horizontalGrids = @[
2317 [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
2318 [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:128],
2319
2320 ];
2321 self.tilesetImageView.verticalGrids = @[
2322 [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
2323 [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:64],
2324 ];
2325 self.tilemapImageView.horizontalGrids = @[
2326 [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
2327 ];
2328 self.tilemapImageView.verticalGrids = @[
2329 [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
2330 ];
2331 }
2332 else {
2333 self.tilesetImageView.horizontalGrids = nil;
2334 self.tilesetImageView.verticalGrids = nil;
2335 self.tilemapImageView.horizontalGrids = nil;
2336 self.tilemapImageView.verticalGrids = nil;
2337 }
2338 }
2339
2340 - (IBAction)toggleScrollingDisplay:(NSButton *)sender
2341 {
2342 self.tilemapImageView.displayScrollRect = sender.state;
2343 }
2344
2345 - (IBAction)vramTabChanged:(NSSegmentedControl *)sender
2346 {
2347 [self.vramTabView selectTabViewItemAtIndex:[sender selectedSegment]];
2348 [self reloadVRAMData:sender];
2349 [self.vramTabView.selectedTabViewItem.view addSubview:self.gridButton];
2350 self.gridButton.hidden = [sender selectedSegment] >= 2;
2351
2352 NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height;
2353 CGRect window_rect = self.vramWindow.frame;
2354 window_rect.origin.y += window_rect.size.height;
2355 switch ([sender selectedSegment]) {
2356 case 0:
2357 case 2:
2358 window_rect.size.height = 384 + height_diff + 48;
2359 break;
2360 case 1:
2361 window_rect.size.height = 512 + height_diff + 48;
2362 break;
2363 case 3:
2364 window_rect.size.height = 24 * 16 + height_diff;
2365 break;
2366
2367 default:
2368 break;
2369 }
2370 window_rect.origin.y -= window_rect.size.height;
2371 [self.vramWindow setFrame:window_rect display:true animate:true];
2372 }
2373
2374 - (void)mouseDidLeaveImageView:(GBImageView *)view
2375 {
2376 self.vramStatusLabel.stringValue = @"";
2377 }
2378
2379 - (void)imageView:(GBImageView *)view mouseMovedToX:(NSUInteger)x Y:(NSUInteger)y
2380 {
2381 if (view == self.tilesetImageView) {
2382 uint8_t bank = x >= 128? 1 : 0;
2383 x &= 127;
2384 uint16_t tile = x / 8 + y / 8 * 16;
2385 self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10];
2386 }
2387 else if (view == self.tilemapImageView) {
2388 uint16_t map_offset = x / 8 + y / 8 * 32;
2389 uint16_t map_base = 0x1800;
2390 GB_map_type_t map_type = (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem;
2391 GB_tileset_type_t tileset_type = (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem;
2392 uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC];
2393 uint8_t *vram = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL);
2394
2395 if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & GB_LCDC_BG_MAP)) {
2396 map_base = 0x1C00;
2397 }
2398
2399 if (tileset_type == GB_TILESET_AUTO) {
2400 tileset_type = (lcdc & GB_LCDC_TILE_SEL)? GB_TILESET_8800 : GB_TILESET_8000;
2401 }
2402
2403 uint8_t tile = vram[map_base + map_offset];
2404 uint16_t tile_address = 0;
2405 if (tileset_type == GB_TILESET_8000) {
2406 tile_address = 0x8000 + tile * 0x10;
2407 }
2408 else {
2409 tile_address = 0x9000 + (int8_t)tile * 0x10;
2410 }
2411
2412 if (GB_is_cgb(&_gb)) {
2413 uint8_t attributes = vram[map_base + map_offset + 0x2000];
2414 self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)",
2415 tile,
2416 attributes & 0x8? 1 : 0,
2417 tile_address,
2418 0x8000 + map_base + map_offset,
2419 (attributes & 0x80) ? 'P' : '-',
2420 (attributes & 0x40) ? 'V' : '-',
2421 (attributes & 0x20) ? 'H' : '-',
2422 attributes & 0x8? 1 : 0,
2423 attributes & 0x7
2424 ];
2425 }
2426 else {
2427 self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x ($%04x) at map address $%04x",
2428 tile,
2429 tile_address,
2430 0x8000 + map_base + map_offset
2431 ];
2432 }
2433
2434 }
2435 }
2436
2437 - (GB_oam_info_t *)oamInfo
2438 {
2439 return _oamInfo;
2440 }
2441
2442 - (IBAction)showVRAMViewer:(id)sender
2443 {
2444 [self.vramWindow makeKeyAndOrderFront:sender];
2445 [self reloadVRAMData: nil];
2446 }
2447
2448 - (void)printImage:(uint32_t *)imageBytes height:(unsigned) height
2449 topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
2450 exposure:(unsigned) exposure
2451 {
2452 uint32_t paddedImage[160 * (topMargin + height + bottomMargin)];
2453 memset(paddedImage, 0xFF, sizeof(paddedImage));
2454 memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0]));
2455 if (!self.printerFeedWindow.isVisible) {
2456 _currentPrinterImageData = [[NSMutableData alloc] init];
2457 }
2458 [_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)];
2459 /* UI related code must run on main thread. */
2460 dispatch_async(dispatch_get_main_queue(), ^{
2461 [_printerSpinner startAnimation:nil];
2462 self.feedImageView.image = [Document imageFromData:_currentPrinterImageData
2463 width:160
2464 height:_currentPrinterImageData.length / 160 / sizeof(imageBytes[0])
2465 scale:2.0];
2466 NSRect frame = self.printerFeedWindow.frame;
2467 double oldHeight = frame.size.height;
2468 frame.size = self.feedImageView.image.size;
2469 [self.printerFeedWindow setContentMaxSize:frame.size];
2470 frame.size.height += self.printerFeedWindow.frame.size.height - self.printerFeedWindow.contentView.frame.size.height;
2471 frame.origin.y -= frame.size.height - oldHeight;
2472 [self.printerFeedWindow setFrame:frame display:false animate: self.printerFeedWindow.isVisible];
2473 [self.printerFeedWindow orderFront:NULL];
2474 });
2475
2476 }
2477
2478 - (void)printDone
2479 {
2480 dispatch_async(dispatch_get_main_queue(), ^{
2481 [_printerSpinner stopAnimation:nil];
2482 });
2483 }
2484
2485 - (void)printDocument:(id)sender
2486 {
2487 if (self.feedImageView.image.size.height == 0) {
2488 NSBeep(); return;
2489 }
2490 NSImageView *view = [[NSImageView alloc] initWithFrame:(NSRect){{0,0}, self.feedImageView.image.size}];
2491 view.image = self.feedImageView.image;
2492 [[NSPrintOperation printOperationWithView:view] runOperationModalForWindow:self.printerFeedWindow delegate:nil didRunSelector:NULL contextInfo:NULL];
2493 }
2494
2495 - (IBAction)savePrinterFeed:(id)sender
2496 {
2497 bool shouldResume = _running;
2498 [self stop];
2499 NSSavePanel *savePanel = [NSSavePanel savePanel];
2500 [savePanel setAllowedFileTypes:@[@"png"]];
2501 [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) {
2502 if (result == NSModalResponseOK) {
2503 [savePanel orderOut:self];
2504 CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL
2505 context:nil
2506 hints:nil];
2507 NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
2508 [imageRep setSize:(NSSize){160, self.feedImageView.image.size.height / 2}];
2509 NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
2510 [data writeToURL:savePanel.URL atomically:false];
2511 [self.printerFeedWindow setIsVisible:false];
2512 }
2513 if (shouldResume) {
2514 [self start];
2515 }
2516 }];
2517 }
2518
2519 - (IBAction)disconnectAllAccessories:(id)sender
2520 {
2521 [self disconnectLinkCable];
2522 [self performAtomicBlock:^{
2523 GB_disconnect_serial(&_gb);
2524 }];
2525 }
2526
2527 - (IBAction)connectPrinter:(id)sender
2528 {
2529 [self disconnectLinkCable];
2530 [self performAtomicBlock:^{
2531 GB_connect_printer(&_gb, printImage, printDone);
2532 }];
2533 }
2534
2535 - (IBAction)connectWorkboy:(id)sender
2536 {
2537 [self disconnectLinkCable];
2538 [self performAtomicBlock:^{
2539 GB_connect_workboy(&_gb, setWorkboyTime, getWorkboyTime);
2540 }];
2541 }
2542
2543 - (void)setFileURL:(NSURL *)fileURL
2544 {
2545 [super setFileURL:fileURL];
2546 if (@available(macOS 11.0, *)) {
2547 self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent];
2548 self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent];
2549 self.vramWindow.subtitle = [self.fileURL.path lastPathComponent];
2550 }
2551 else {
2552 self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]];
2553 self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [self.fileURL.path lastPathComponent]];
2554 self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [self.fileURL.path lastPathComponent]];
2555 }
2556 }
2557
2558 - (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview;
2559 {
2560 if ([[splitView arrangedSubviews] lastObject] == subview) {
2561 return true;
2562 }
2563 return false;
2564 }
2565
2566 - (CGFloat)splitView:(GBSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex
2567 {
2568 return 600;
2569 }
2570
2571 - (CGFloat)splitView:(GBSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex
2572 {
2573 return splitView.frame.size.width - 321;
2574 }
2575
2576 - (BOOL)splitView:(GBSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view
2577 {
2578 if ([[splitView arrangedSubviews] lastObject] == view) {
2579 return false;
2580 }
2581 return true;
2582 }
2583
2584 - (void)splitViewDidResizeSubviews:(NSNotification *)notification
2585 {
2586 GBSplitView *splitview = notification.object;
2587 if ([[[splitview arrangedSubviews] firstObject] frame].size.width < 600) {
2588 [splitview setPosition:600 ofDividerAtIndex:0];
2589 }
2590 }
2591
2592 - (IBAction)showCheats:(id)sender
2593 {
2594 [self.cheatsWindow makeKeyAndOrderFront:nil];
2595 }
2596
2597 - (IBAction)showCheatSearch:(id)sender
2598 {
2599 if (!_cheatSearchController) {
2600 _cheatSearchController = [GBCheatSearchController controllerWithDocument:self];
2601 }
2602 [_cheatSearchController.window makeKeyAndOrderFront:sender];
2603 }
2604
2605 - (IBAction)toggleCheats:(id)sender
2606 {
2607 GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb));
2608 }
2609
2610 - (void)disconnectLinkCable
2611 {
2612 bool wasRunning = self->_running;
2613 Document *partner = _master ?: _slave;
2614 if (partner) {
2615 wasRunning |= partner->_running;
2616 [self stop];
2617 partner->_master = nil;
2618 partner->_slave = nil;
2619 _master = nil;
2620 _slave = nil;
2621 if (wasRunning) {
2622 [partner start];
2623 [self start];
2624 }
2625 GB_set_turbo_mode(&_gb, false, false);
2626 GB_set_turbo_mode(&partner->_gb, false, false);
2627 GB_set_turbo_cap(&_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]);
2628 GB_set_turbo_cap(&partner->_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]);
2629 }
2630 }
2631
2632 - (void)connectLinkCable:(NSMenuItem *)sender
2633 {
2634 [self disconnectAllAccessories:sender];
2635 Document *partner = [sender representedObject];
2636 [partner disconnectAllAccessories:sender];
2637
2638 bool wasRunning = self->_running;
2639 [self stop];
2640 [partner stop];
2641 GB_set_turbo_mode(&partner->_gb, true, true);
2642 _slave = partner;
2643 partner->_master = self;
2644 GB_set_turbo_cap(&partner->_gb, 0);
2645 _linkOffset = 0;
2646 GB_set_serial_transfer_bit_start_callback(&_gb, _linkCableBitStart);
2647 GB_set_serial_transfer_bit_start_callback(&partner->_gb, _linkCableBitStart);
2648 GB_set_serial_transfer_bit_end_callback(&_gb, _linkCableBitEnd);
2649 GB_set_serial_transfer_bit_end_callback(&partner->_gb, _linkCableBitEnd);
2650 if (wasRunning) {
2651 [self start];
2652 }
2653 }
2654
2655 - (void)_linkCableBitStart:(bool)bit
2656 {
2657 _linkCableBit = bit;
2658 }
2659
2660 -(bool)_linkCableBitEnd
2661 {
2662 bool ret = GB_serial_get_data_bit(&self.partner->_gb);
2663 GB_serial_set_data_bit(&self.partner->_gb, _linkCableBit);
2664 return ret;
2665 }
2666
2667 - (void)infraredStateChanged:(bool)state
2668 {
2669 if (self.partner) {
2670 GB_set_infrared_input(&self.partner->_gb, state);
2671 }
2672 }
2673
2674 -(Document *)partner
2675 {
2676 return _slave ?: _master;
2677 }
2678
2679 - (bool)isSlave
2680 {
2681 return _master;
2682 }
2683
2684 - (GB_gameboy_t *)gb
2685 {
2686 return &_gb;
2687 }
2688
2689 - (NSImage *)takeScreenshot
2690 {
2691 NSImage *ret = nil;
2692 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]) {
2693 ret = [_view renderToImage];
2694 }
2695 if (!ret) {
2696 ret = [Document imageFromData:[NSData dataWithBytesNoCopy:_view.currentBuffer
2697 length:GB_get_screen_width(&_gb) * GB_get_screen_height(&_gb) * 4
2698 freeWhenDone:false]
2699 width:GB_get_screen_width(&_gb)
2700 height:GB_get_screen_height(&_gb)
2701 scale:1.0];
2702 }
2703 return ret;
2704 }
2705
2706 - (NSString *)screenshotFilename
2707 {
2708 NSDate *date = [NSDate date];
2709 NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
2710 dateFormatter.dateStyle = NSDateFormatterLongStyle;
2711 dateFormatter.timeStyle = NSDateFormatterMediumStyle;
2712 return [[NSString stringWithFormat:@"%@ – %@.png",
2713 self.fileURL.lastPathComponent.stringByDeletingPathExtension,
2714 [dateFormatter stringFromDate:date]] stringByReplacingOccurrencesOfString:@":" withString:@"."]; // Gotta love Mac OS Classic
2715
2716 }
2717
2718 - (IBAction)saveScreenshot:(id)sender
2719 {
2720 NSString *folder = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBScreenshotFolder"];
2721 BOOL isDirectory = false;
2722 if (folder) {
2723 [[NSFileManager defaultManager] fileExistsAtPath:folder isDirectory:&isDirectory];
2724 }
2725 if (!folder) {
2726 bool shouldResume = _running;
2727 [self stop];
2728 NSOpenPanel *openPanel = [NSOpenPanel openPanel];
2729 openPanel.canChooseFiles = false;
2730 openPanel.canChooseDirectories = true;
2731 openPanel.message = @"Choose a folder for screenshots";
2732 [openPanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
2733 if (result == NSModalResponseOK) {
2734 [[NSUserDefaults standardUserDefaults] setObject:openPanel.URL.path
2735 forKey:@"GBScreenshotFolder"];
2736 [self saveScreenshot:sender];
2737 }
2738 if (shouldResume) {
2739 [self start];
2740 }
2741
2742 }];
2743 return;
2744 }
2745 NSImage *image = [self takeScreenshot];
2746 NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
2747 dateFormatter.dateStyle = NSDateFormatterLongStyle;
2748 dateFormatter.timeStyle = NSDateFormatterMediumStyle;
2749 NSString *filename = [self screenshotFilename];
2750 filename = [folder stringByAppendingPathComponent:filename];
2751 unsigned i = 2;
2752 while ([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
2753 filename = [[filename stringByDeletingPathExtension] stringByAppendingFormat:@" %d.png", i++];
2754 }
2755
2756 NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject;
2757 NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
2758 [data writeToFile:filename atomically:false];
2759 [self.osdView displayText:@"Screenshot saved"];
2760 }
2761
2762 - (IBAction)saveScreenshotAs:(id)sender
2763 {
2764 bool shouldResume = _running;
2765 [self stop];
2766 NSImage *image = [self takeScreenshot];
2767 NSSavePanel *savePanel = [NSSavePanel savePanel];
2768 [savePanel setNameFieldStringValue:[self screenshotFilename]];
2769 [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
2770 if (result == NSModalResponseOK) {
2771 [savePanel orderOut:self];
2772 NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject;
2773 NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
2774 [data writeToURL:savePanel.URL atomically:false];
2775 [[NSUserDefaults standardUserDefaults] setObject:savePanel.URL.path.stringByDeletingLastPathComponent
2776 forKey:@"GBScreenshotFolder"];
2777 }
2778 if (shouldResume) {
2779 [self start];
2780 }
2781 }];
2782 [self.osdView displayText:@"Screenshot saved"];
2783 }
2784
2785 - (IBAction)copyScreenshot:(id)sender
2786 {
2787 NSImage *image = [self takeScreenshot];
2788 [[NSPasteboard generalPasteboard] clearContents];
2789 [[NSPasteboard generalPasteboard] writeObjects:@[image]];
2790 [self.osdView displayText:@"Screenshot copied"];
2791 }
2792
2793 - (IBAction)toggleDisplayBackground:(id)sender
2794 {
2795 GB_set_background_rendering_disabled(&_gb, !GB_is_background_rendering_disabled(&_gb));
2796 }
2797
2798 - (IBAction)toggleDisplayObjects:(id)sender
2799 {
2800 GB_set_object_rendering_disabled(&_gb, !GB_is_object_rendering_disabled(&_gb));
2801 }
2802
2803 - (IBAction)newCartridgeInstance:(id)sender
2804 {
2805 bool shouldResume = _running;
2806 [self stop];
2807 NSSavePanel *savePanel = [NSSavePanel savePanel];
2808 [savePanel setAllowedFileTypes:@[@"gbcart"]];
2809 [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
2810 if (result == NSModalResponseOK) {
2811 [savePanel orderOut:self];
2812 NSString *romPath = self.romPath;
2813 [[NSFileManager defaultManager] trashItemAtURL:savePanel.URL resultingItemURL:nil error:nil];
2814 [[NSFileManager defaultManager] createDirectoryAtURL:savePanel.URL withIntermediateDirectories:false attributes:nil error:nil];
2815 [[NSString stringWithFormat:@"%@\n%@\n%@",
2816 [romPath pathRelativeToDirectory:savePanel.URL.path],
2817 romPath,
2818 [[NSURL fileURLWithPath:romPath].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")]
2819 ] writeToURL:[savePanel.URL URLByAppendingPathComponent:@"rom.gbl"] atomically:false encoding:NSUTF8StringEncoding error:nil];
2820 [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:savePanel.URL display:true completionHandler:nil];
2821 }
2822 if (shouldResume) {
2823 [self start];
2824 }
2825 }];
2826 }
2827
2828 - (IBAction)toggleAudioRecording:(id)sender
2829 {
2830
2831 bool shouldResume = _running;
2832 [self stop];
2833 if (_isRecordingAudio) {
2834 _isRecordingAudio = false;
2835 int error = GB_stop_audio_recording(&_gb);
2836 if (error) {
2837 NSAlert *alert = [[NSAlert alloc] init];
2838 [alert setMessageText:[NSString stringWithFormat:@"Could not finalize recording: %s", strerror(error)]];
2839 [alert setAlertStyle:NSAlertStyleCritical];
2840 [alert runModal];
2841 }
2842 else {
2843 [self.osdView displayText:@"Audio recording ended"];
2844 }
2845 if (shouldResume) {
2846 [self start];
2847 }
2848 return;
2849 }
2850 _audioSavePanel = [NSSavePanel savePanel];
2851 if (!self.audioRecordingAccessoryView) {
2852 [[NSBundle mainBundle] loadNibNamed:@"AudioRecordingAccessoryView" owner:self topLevelObjects:nil];
2853 }
2854 _audioSavePanel.accessoryView = self.audioRecordingAccessoryView;
2855 [self audioFormatChanged:self.audioFormatButton];
2856
2857 [_audioSavePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
2858 if (result == NSModalResponseOK) {
2859 [_audioSavePanel orderOut:self];
2860 int error = GB_start_audio_recording(&_gb, _audioSavePanel.URL.fileSystemRepresentation, self.audioFormatButton.selectedTag);
2861 if (error) {
2862 NSAlert *alert = [[NSAlert alloc] init];
2863 [alert setMessageText:[NSString stringWithFormat:@"Could not start recording: %s", strerror(error)]];
2864 [alert setAlertStyle:NSAlertStyleCritical];
2865 [alert runModal];
2866 }
2867 else {
2868 [self.osdView displayText:@"Audio recording started"];
2869 _isRecordingAudio = true;
2870 }
2871 }
2872 if (shouldResume) {
2873 [self start];
2874 }
2875 _audioSavePanel = nil;
2876 }];
2877 }
2878
2879 - (IBAction)audioFormatChanged:(NSPopUpButton *)sender
2880 {
2881 switch ((GB_audio_format_t)sender.selectedTag) {
2882 case GB_AUDIO_FORMAT_RAW:
2883 _audioSavePanel.allowedFileTypes = @[@"raw", @"pcm"];
2884 break;
2885 case GB_AUDIO_FORMAT_AIFF:
2886 _audioSavePanel.allowedFileTypes = @[@"aiff", @"aif", @"aifc"];
2887 break;
2888 case GB_AUDIO_FORMAT_WAV:
2889 _audioSavePanel.allowedFileTypes = @[@"wav"];
2890 break;
2891 }
2892 }
2893
2894 - (IBAction)toggleAudioChannel:(NSMenuItem *)sender
2895 {
2896 GB_set_channel_muted(&_gb, sender.tag, !GB_is_channel_muted(&_gb, sender.tag));
2897 }
2898
2899 - (IBAction)cartSwap:(id)sender
2900 {
2901 bool wasRunning = _running;
2902 if (wasRunning) {
2903 [self stop];
2904 }
2905 [[NSDocumentController sharedDocumentController] beginOpenPanelWithCompletionHandler:^(NSArray<NSURL *> *urls) {
2906 if (urls.count == 1) {
2907 bool ok = true;
2908 for (Document *document in [NSDocumentController sharedDocumentController].documents) {
2909 if (document == self) continue;
2910 if ([document.fileURL isEqual:urls.firstObject]) {
2911 NSAlert *alert = [[NSAlert alloc] init];
2912 [alert setMessageText:[NSString stringWithFormat:@"‘%@’ is already open in another window. Close ‘%@’ before hot swapping it into this instance.",
2913 urls.firstObject.lastPathComponent, urls.firstObject.lastPathComponent]];
2914 [alert setAlertStyle:NSAlertStyleCritical];
2915 [alert runModal];
2916 ok = false;
2917 break;
2918 }
2919 }
2920 if (ok) {
2921 GB_save_battery(&_gb, self.savPath.UTF8String);
2922 self.fileURL = urls.firstObject;
2923 [self loadROM];
2924 }
2925 }
2926 if (wasRunning) {
2927 [self start];
2928 }
2929 }];
2930 }
2931
2932 - (IBAction)reloadROM:(id)sender
2933 {
2934 bool wasRunning = _running;
2935 if (wasRunning) {
2936 [self stop];
2937 }
2938
2939 _romModified = false;
2940 [self updateChangeCount:NSChangeCleared];
2941 [self loadROM];
2942
2943 if (wasRunning) {
2944 [self start];
2945 }
2946 }
2947
2948 - (void)updateDebuggerButtons
2949 {
2950 bool updateContinue = false;
2951 if (@available(macOS 10.10, *)) {
2952 if ([self.consoleInput.placeholderAttributedString.string isEqualToString:self.debuggerContinueButton.alternateTitle]) {
2953 [self.debuggerContinueButton mouseExited:nil];
2954 updateContinue = true;
2955 }
2956 }
2957 if (self.isPaused) {
2958 self.debuggerContinueButton.toolTip = self.debuggerContinueButton.title = @"Continue";
2959 self.debuggerContinueButton.alternateTitle = @"continue";
2960 self.debuggerContinueButton.imagePosition = NSImageOnly;
2961 if (@available(macOS 10.14, *)) {
2962 self.debuggerContinueButton.contentTintColor = nil;
2963 }
2964 self.debuggerContinueButton.image = [NSImage imageNamed:@"ContinueTemplate"];
2965
2966 self.debuggerNextButton.enabled = true;
2967 self.debuggerStepButton.enabled = true;
2968 self.debuggerFinishButton.enabled = true;
2969 self.debuggerBackstepButton.enabled = true;
2970 }
2971 else {
2972 self.debuggerContinueButton.toolTip = self.debuggerContinueButton.title = @"Interrupt";
2973 self.debuggerContinueButton.alternateTitle = @"interrupt";
2974 self.debuggerContinueButton.imagePosition = NSImageOnly;
2975 if (@available(macOS 10.14, *)) {
2976 self.debuggerContinueButton.contentTintColor = [NSColor controlAccentColor];
2977 }
2978 self.debuggerContinueButton.image = [NSImage imageNamed:@"InterruptTemplate"];
2979
2980 self.debuggerNextButton.enabled = false;
2981 self.debuggerStepButton.enabled = false;
2982 self.debuggerFinishButton.enabled = false;
2983 self.debuggerBackstepButton.enabled = false;
2984 }
2985 if (updateContinue) {
2986 [self.debuggerContinueButton mouseEntered:nil];
2987 }
2988 }
2989
2990 - (IBAction)debuggerButtonPressed:(NSButton *)sender
2991 {
2992 [self queueDebuggerCommand:sender.alternateTitle];
2993 }
2994
2995 - (void)setROMModified
2996 {
2997 _romModified = true;
2998 [self updateChangeCount:NSChangeDone];
2999 }
3000
3001 - (BOOL)writeToFile:(NSString *)path ofType:(NSString *)type
3002 {
3003 if ([type isEqualToString:@"Game Boy Cartridge"]) {
3004 if (![[NSFileManager defaultManager] copyItemAtPath:self.fileName toPath:path error:nil]) return false;
3005 path = self.romPath;
3006 if (!path) return false;
3007 }
3008 size_t size;
3009 uint8_t *data = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, &size, NULL);
3010 return [[NSData dataWithBytesNoCopy:data length:size freeWhenDone:false] writeToFile:path atomically:true];
3011 }
3012
3013 + (NSArray<NSString *> *)readableTypes
3014 {
3015 NSMutableSet *set = [NSMutableSet setWithArray:[super readableTypes]];
3016 for (NSString *type in @[@"gb", @"gbc", @"isx", @"gbs"]) {
3017 [set addObject:(__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
3018 (__bridge CFStringRef)type,
3019 NULL)];
3020 }
3021 return [set allObjects];
3022 }
3023
3024 @end
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.