diff --git a/.gitattributes b/.gitattributes index 427cb28..2149ea1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,7 @@ +# Always use LF line endings for shaders +*.fsh text eol=lf +*.metal text eol=lf + HexFiend/* linguist-vendored *.inc linguist-language=C Core/*.h linguist-language=C diff --git a/BootROMs/cgb_boot.asm b/BootROMs/cgb_boot.asm index dc3544f..1345915 100644 --- a/BootROMs/cgb_boot.asm +++ b/BootROMs/cgb_boot.asm @@ -329,101 +329,103 @@ FirstChecksumWithDuplicate: ChecksumsEnd: PalettePerChecksum: -; | $80 means game requires DMG boot tilemap - db 0 ; Default Palette - db 4 ; ALLEY WAY - db 5 ; YAKUMAN - db 35 ; BASEBALL, (Game and Watch 2) - db 34 ; TENNIS - db 3 ; TETRIS - db 31 ; QIX - db 15 ; DR.MARIO - db 10 ; RADARMISSION - db 5 ; F1RACE - db 19 ; YOSSY NO TAMAGO - db 36 ; - db 7 | $80 ; X - db 37 ; MARIOLAND2 - db 30 ; YOSSY NO COOKIE - db 44 ; ZELDA - db 21 ; - db 32 ; - db 31 ; TETRIS FLASH - db 20 ; DONKEY KONG - db 5 ; MARIO'S PICROSS - db 33 ; - db 13 ; POKEMON RED, (GAMEBOYCAMERA G) - db 14 ; POKEMON GREEN - db 5 ; PICROSS 2 - db 29 ; YOSSY NO PANEPON - db 5 ; KIRAKIRA KIDS - db 18 ; GAMEBOY GALLERY - db 9 ; POCKETCAMERA - db 3 ; - db 2 ; BALLOON KID - db 26 ; KINGOFTHEZOO - db 25 ; DMG FOOTBALL - db 25 ; WORLD CUP - db 41 ; OTHELLO - db 42 ; SUPER RC PRO-AM - db 26 ; DYNABLASTER - db 45 ; BOY AND BLOB GB2 - db 42 ; MEGAMAN - db 45 ; STAR WARS-NOA - db 36 ; - db 38 ; WAVERACE - db 26 ; - db 42 ; LOLO2 - db 30 ; YOSHI'S COOKIE - db 41 ; MYSTIC QUEST - db 34 ; - db 34 ; TOPRANKINGTENNIS - db 5 ; MANSELL - db 42 ; MEGAMAN3 - db 6 ; SPACE INVADERS - db 5 ; GAME&WATCH - db 33 ; DONKEYKONGLAND95 - db 25 ; ASTEROIDS/MISCMD - db 42 ; STREET FIGHTER 2 - db 42 ; DEFENDER/JOUST - db 40 ; KILLERINSTINCT95 - db 2 ; TETRIS BLAST - db 16 ; PINOCCHIO - db 25 ; - db 42 ; BA.TOSHINDEN - db 42 ; NETTOU KOF 95 - db 5 ; - db 0 ; TETRIS PLUS - db 39 ; DONKEYKONGLAND 3 - db 36 ; - db 22 ; SUPER MARIOLAND - db 25 ; GOLF - db 6 ; SOLARSTRIKER - db 32 ; GBWARS - db 12 ; KAERUNOTAMENI - db 36 ; - db 11 ; POKEMON BLUE - db 39 ; DONKEYKONGLAND - db 18 ; GAMEBOY GALLERY2 - db 39 ; DONKEYKONGLAND 2 - db 24 ; KID ICARUS - db 31 ; TETRIS2 - db 50 ; - db 17 ; MOGURANYA - db 46 ; - db 6 ; GALAGA&GALAXIAN - db 27 ; BT2RAGNAROKWORLD - db 0 ; KEN GRIFFEY JR - db 47 ; - db 41 ; MAGNETIC SOCCER - db 41 ; VEGAS STAKES - db 0 ; - db 0 ; MILLI/CENTI/PEDE - db 19 ; MARIO & YOSHI - db 34 ; SOCCER - db 23 ; POKEBOM - db 18 ; G&W GALLERY - db 29 ; TETRIS ATTACK +palette_index: MACRO ; palette, flags + db ((\1) * 3) | (\2) ; | $80 means game requires DMG boot tilemap +ENDM + palette_index 0, 0 ; Default Palette + palette_index 4, 0 ; ALLEY WAY + palette_index 5, 0 ; YAKUMAN + palette_index 35, 0 ; BASEBALL, (Game and Watch 2) + palette_index 34, 0 ; TENNIS + palette_index 3, 0 ; TETRIS + palette_index 31, 0 ; QIX + palette_index 15, 0 ; DR.MARIO + palette_index 10, 0 ; RADARMISSION + palette_index 5, 0 ; F1RACE + palette_index 19, 0 ; YOSSY NO TAMAGO + palette_index 36, 0 ; + palette_index 7, $80 ; X + palette_index 37, 0 ; MARIOLAND2 + palette_index 30, 0 ; YOSSY NO COOKIE + palette_index 44, 0 ; ZELDA + palette_index 21, 0 ; + palette_index 32, 0 ; + palette_index 31, 0 ; TETRIS FLASH + palette_index 20, 0 ; DONKEY KONG + palette_index 5, 0 ; MARIO'S PICROSS + palette_index 33, 0 ; + palette_index 13, 0 ; POKEMON RED, (GAMEBOYCAMERA G) + palette_index 14, 0 ; POKEMON GREEN + palette_index 5, 0 ; PICROSS 2 + palette_index 29, 0 ; YOSSY NO PANEPON + palette_index 5, 0 ; KIRAKIRA KIDS + palette_index 18, 0 ; GAMEBOY GALLERY + palette_index 9, 0 ; POCKETCAMERA + palette_index 3, 0 ; + palette_index 2, 0 ; BALLOON KID + palette_index 26, 0 ; KINGOFTHEZOO + palette_index 25, 0 ; DMG FOOTBALL + palette_index 25, 0 ; WORLD CUP + palette_index 41, 0 ; OTHELLO + palette_index 42, 0 ; SUPER RC PRO-AM + palette_index 26, 0 ; DYNABLASTER + palette_index 45, 0 ; BOY AND BLOB GB2 + palette_index 42, 0 ; MEGAMAN + palette_index 45, 0 ; STAR WARS-NOA + palette_index 36, 0 ; + palette_index 38, 0 ; WAVERACE + palette_index 26, 0 ; + palette_index 42, 0 ; LOLO2 + palette_index 30, 0 ; YOSHI'S COOKIE + palette_index 41, 0 ; MYSTIC QUEST + palette_index 34, 0 ; + palette_index 34, 0 ; TOPRANKINGTENNIS + palette_index 5, 0 ; MANSELL + palette_index 42, 0 ; MEGAMAN3 + palette_index 6, 0 ; SPACE INVADERS + palette_index 5, 0 ; GAME&WATCH + palette_index 33, 0 ; DONKEYKONGLAND95 + palette_index 25, 0 ; ASTEROIDS/MISCMD + palette_index 42, 0 ; STREET FIGHTER 2 + palette_index 42, 0 ; DEFENDER/JOUST + palette_index 40, 0 ; KILLERINSTINCT95 + palette_index 2, 0 ; TETRIS BLAST + palette_index 16, 0 ; PINOCCHIO + palette_index 25, 0 ; + palette_index 42, 0 ; BA.TOSHINDEN + palette_index 42, 0 ; NETTOU KOF 95 + palette_index 5, 0 ; + palette_index 0, 0 ; TETRIS PLUS + palette_index 39, 0 ; DONKEYKONGLAND 3 + palette_index 36, 0 ; + palette_index 22, 0 ; SUPER MARIOLAND + palette_index 25, 0 ; GOLF + palette_index 6, 0 ; SOLARSTRIKER + palette_index 32, 0 ; GBWARS + palette_index 12, 0 ; KAERUNOTAMENI + palette_index 36, 0 ; + palette_index 11, 0 ; POKEMON BLUE + palette_index 39, 0 ; DONKEYKONGLAND + palette_index 18, 0 ; GAMEBOY GALLERY2 + palette_index 39, 0 ; DONKEYKONGLAND 2 + palette_index 24, 0 ; KID ICARUS + palette_index 31, 0 ; TETRIS2 + palette_index 50, 0 ; + palette_index 17, 0 ; MOGURANYA + palette_index 46, 0 ; + palette_index 6, 0 ; GALAGA&GALAXIAN + palette_index 27, 0 ; BT2RAGNAROKWORLD + palette_index 0, 0 ; KEN GRIFFEY JR + palette_index 47, 0 ; + palette_index 41, 0 ; MAGNETIC SOCCER + palette_index 41, 0 ; VEGAS STAKES + palette_index 0, 0 ; + palette_index 0, 0 ; MILLI/CENTI/PEDE + palette_index 19, 0 ; MARIO & YOSHI + palette_index 34, 0 ; SOCCER + palette_index 23, 0 ; POKEBOM + palette_index 18, 0 ; G&W GALLERY + palette_index 29, 0 ; TETRIS ATTACK Dups4thLetterArray: db "BEFAARBEKEK R-URAR INAILICE R" diff --git a/BootROMs/pb12.c b/BootROMs/pb12.c index 3f6d5f8..cfedf6b 100644 --- a/BootROMs/pb12.c +++ b/BootROMs/pb12.c @@ -13,6 +13,18 @@ void opts(uint8_t byte, uint8_t *options) *(options++) = byte & (byte >> 1); } +void write_all(int fd, const void *buf, size_t count) { + while (count) { + ssize_t written = write(fd, buf, count); + if (written < 0) { + fprintf(stderr, "write"); + exit(1); + } + count -= written; + buf += written; + } +} + int main() { static uint8_t source[0x4000]; @@ -76,15 +88,15 @@ int main() if (bits >= 8) { uint8_t outctl = control >> (bits - 8); assert(outctl != 1); - write(STDOUT_FILENO, &outctl, 1); - write(STDOUT_FILENO, literals, literals_size); + write_all(STDOUT_FILENO, &outctl, 1); + write_all(STDOUT_FILENO, literals, literals_size); bits -= 8; control &= (1 << bits) - 1; literals_size = 0; } } uint8_t end_byte = 1; - write(STDOUT_FILENO, &end_byte, 1); + write_all(STDOUT_FILENO, &end_byte, 1); return 0; } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..94627d1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# SameBoy Coding and Contribution Guidelines + +## Issues + +GitHub Issues are the most effective way to report a bug or request a feature in SameBoy. When reporting a bug, make sure you use the latest stable release, and make sure you mention the SameBoy frontend (Cocoa, SDL, Libretro) and operating system you're using. If you're using Linux/BSD/etc, or you build your own copy of SameBoy for another reason, give as much details as possible on your environment. + +If your bug involves a crash, please attach a crash log or a core dump. If you're using Linux/BSD/etc, or if you're using the Libretro core, please attach the `sameboy` binary (or `libretro_sameboy` library) in that case. + +If your bug is a regression, it'd be extremely helpful if you can report the the first affected version. You get extra credits if you use `git bisect` to point the exact breaking commit. + +If your bug is an emulation bug (Such as a failing test ROM), and you have access to a Game Boy you can test on, please confirm SameBoy is indeed behaving differently from hardware, and report both the emulated model and revision in SameBoy, and the hardware revision you're testing on. + +If your issue is a feature request, demonstrating use cases can help me better prioritize it. + +## Pull Requests + +To allow quicker integration into SameBoy's master branch, contributors are asked to follow SameBoy's style and coding guidelines. Keep in mind that despite the seemingly strict guidelines, all pull requests are welcome – not following the guidelines does not mean your pull request will not be accepted, but it will require manual tweaks from my side for integrating. + +### Languages and Compilers + +SameBoy's core, SDL frontend, Libretro frontend, and automatic tester (Folders `Core`, `SDL` & `OpenDialog`, `libretro`, and `Tester`; respectively) are all written in C11. The Cocoa frontend, SameBoy's fork of Hex Fiend, JoyKit and the Quick Look previewer (Folders `Cocoa`, `HexFiend`, `JoyKit` and `QuickLook`; respectively) are all written in ARC-enabled Objective-C. The SameBoot ROMs (Under `BootROMs`) are written in rgbds-flavor SM83 assembly, with build tools in C11. The shaders (inside `Shaders`) are written in a polyglot GLSL and Metal style, with a few GLSL- and Metal-specific sources. The build system uses standalone Make, in the GNU flavor. Avoid adding new languages (C++, Swift, Python, CMake...) to any of the existing sub-projects. + +SameBoy's main target compiler is Clang, but GCC is also supported when targeting Linux and Libretro. Other compilers (e.g. MSVC) are not supported, and unless there's a good reason, there's no need to go out of your way to add specific support for them. Extensions that are supported by both compilers (Such as `typeof`) may be used if it makes sense. It's OK if you can't test one of these compilers yourself; once you push a commit, the CI bot will let you know if you broke something. + +### Third Party Libraries and Tools + +Avoid adding new required dependencies; run-time and compile-time dependencies alike. Most importantly, avoid linking against GPL licensed libraries (LGPL libraries are fine), so SameBoy can retain its MIT license. + +### Spacing, Indentation and Formatting + +In all files and languages (Other than Makefiles when required), 4 spaces are used for indentation. Unix line endings (`\n`) are used exclusively, even in Windows-specific source files. (`\r` and `\t` shouldn't appear in any source file). Opening braces belong on the same line as their control flow directive, and on their own line when following a function prototype. The `else` keyword always starts on its own line. The `case` keyword is indented relative to its `switch` block, and the code inside a `case` is indented relative to its label. A control flow keyword should have a space between it and the following `(`, commas should follow a space, and operator (except `.` and `->`) should be surrounded by spaces. + +Control flow statements must use `{}`, with the exception of `if` statements that only contain a single `break`, `continue`, or trivial `return` statements. If `{}`s are omitted, the statement must be on the same line as the `if` condition. Functions that do not have any argument must be specified as `(void)`, as mandated by the C standard. The `sizeof` and `typeof` operators should be used as if they're functions (With `()`). `*`, when used to declare pointer types (including functions that return pointers), and when used to dereference a pointer, is attached to the right side (The variable name) – not to the left, and not with spaces on both sides. + +No strict limitations on a line's maximum width, but use your best judgement if you think a statement would benefit from an additional line break. + +Well formatted code example: + +``` +static void my_function(void) +{ + GB_something_t *thing = GB_function(&gb, GB_FLAG_ONE | GB_FLAG_TWO, sizeof(thing)); + if (GB_is_thing(thing)) return; + + switch (*thing) { + case GB_QUACK: + // Something + case GB_DUCK: + // Something else + } +} +``` + +Badly formatted code example: +``` +static void my_function(){ + GB_something_t* thing=GB_function(&gb , GB_FLAG_ONE|GB_FLAG_TWO , sizeof thing); + if( GB_is_thing ( thing ) ) + return; + + switch(* thing) + { + case GB_QUACK: + // Something + case GB_DUCK: + // Something else + } +} +``` + +### Other Coding Conventions + +The primitive types to be used in SameBoy are `unsigned` and `signed` (Without the `int` keyword), the `(u)int*_t` types, `char *` for UTF-8 strings, `double` for non-integer numbers, and `bool` for booleans (Including in Objective-C code, avoid `BOOL`). As long as it's not mandated by a 3rd-party API (e.g. `int` when using file descriptors), avoid using other primitive types. Use `const` whenever possible. + +Most C names should be `lower_case_snake_case`. Constants and macros use `UPPER_CASE_SNAKE_CASE`. Type definitions use a `_t` suffix. Type definitions, as well as non-static (exported) core symbols, should be prefixed with `GB_` (SameBoy's core is intended to be used as a library, so it shouldn't contaminate the global namespace without prefixes). Exported symbols that are only meant to be used by other parts of the core should still get the `GB_` prefix, but their header definition should be inside `#ifdef GB_INTERNAL`. + +For Objective-C naming conventions, use Apple's conventions (Some old Objective-C code mixes these with the C naming convention; new code should use Apple's convention exclusively). The name prefix for SameBoy classes and constants is `GB`. JoyKit's prefix is `JOY`, and Hex Fiend's prefix is `HF`. + +In all languages, prefer long, unambiguous names over short ambiguous ones. diff --git a/Cocoa/AppDelegate.h b/Cocoa/AppDelegate.h index 608a50c..8f91565 100644 --- a/Cocoa/AppDelegate.h +++ b/Cocoa/AppDelegate.h @@ -1,6 +1,6 @@ #import -@interface AppDelegate : NSObject +@interface AppDelegate : NSObject @property IBOutlet NSWindow *preferencesWindow; @property (strong) IBOutlet NSView *graphicsTab; @@ -10,6 +10,7 @@ - (IBAction)showPreferences: (id) sender; - (IBAction)toggleDeveloperMode:(id)sender; - (IBAction)switchPreferencesTab:(id)sender; +@property (weak) IBOutlet NSMenuItem *linkCableMenuItem; @end diff --git a/Cocoa/AppDelegate.m b/Cocoa/AppDelegate.m index 3404620..282105e 100644 --- a/Cocoa/AppDelegate.m +++ b/Cocoa/AppDelegate.m @@ -50,6 +50,10 @@ JOYAxes2DEmulateButtonsKey: @YES, JOYHatsEmulateButtonsKey: @YES, }]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; + } } - (IBAction)toggleDeveloperMode:(id)sender @@ -77,10 +81,29 @@ if ([anItem action] == @selector(toggleDeveloperMode:)) { [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]]; } - + + if (anItem == self.linkCableMenuItem) { + return [[NSDocumentController sharedDocumentController] documents].count > 1; + } return true; } +- (void)menuNeedsUpdate:(NSMenu *)menu +{ + NSMutableArray *items = [NSMutableArray array]; + NSDocument *currentDocument = [[NSDocumentController sharedDocumentController] currentDocument]; + + for (NSDocument *document in [[NSDocumentController sharedDocumentController] documents]) { + if (document == currentDocument) continue; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:document.displayName action:@selector(connectLinkCable:) keyEquivalent:@""]; + item.representedObject = document; + item.image = [[NSWorkspace sharedWorkspace] iconForFile:document.fileURL.path]; + [item.image setSize:NSMakeSize(16, 16)]; + [items addObject:item]; + } + menu.itemArray = items; +} + - (IBAction) showPreferences: (id) sender { NSArray *objects; @@ -101,4 +124,12 @@ return YES; } +- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification +{ + [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:YES]; +} + +- (IBAction)nop:(id)sender +{ +} @end diff --git a/Cocoa/BigSurToolbar.h b/Cocoa/BigSurToolbar.h new file mode 100644 index 0000000..ea8b370 --- /dev/null +++ b/Cocoa/BigSurToolbar.h @@ -0,0 +1,30 @@ +#import +#ifndef BigSurToolbar_h +#define BigSurToolbar_h + +/* Backport the toolbarStyle property to allow compilation with older SDKs*/ +#ifndef __MAC_10_16 +typedef NS_ENUM(NSInteger, NSWindowToolbarStyle) { + // The default value. The style will be determined by the window's given configuration + NSWindowToolbarStyleAutomatic, + // The toolbar will appear below the window title + NSWindowToolbarStyleExpanded, + // The toolbar will appear below the window title and the items in the toolbar will attempt to have equal widths when possible + NSWindowToolbarStylePreference, + // The window title will appear inline with the toolbar when visible + NSWindowToolbarStyleUnified, + // Same as NSWindowToolbarStyleUnified, but with reduced margins in the toolbar allowing more focus to be on the contents of the window + NSWindowToolbarStyleUnifiedCompact +} API_AVAILABLE(macos(11.0)); + +@interface NSWindow (toolbarStyle) +@property NSWindowToolbarStyle toolbarStyle API_AVAILABLE(macos(11.0)); +@end + +@interface NSImage (SFSymbols) ++ (instancetype)imageWithSystemSymbolName:(NSString *)symbolName accessibilityDescription:(NSString *)description API_AVAILABLE(macos(11.0)); +@end + +#endif + +#endif diff --git a/Cocoa/Document.h b/Cocoa/Document.h index 9353788..bf5d9c0 100644 --- a/Cocoa/Document.h +++ b/Cocoa/Document.h @@ -6,6 +6,7 @@ @class GBCheatWindowController; @interface Document : NSDocument +@property (readonly) GB_gameboy_t *gb; @property (strong) IBOutlet GBView *view; @property (strong) IBOutlet NSTextView *consoleOutput; @property (strong) IBOutlet NSPanel *consoleWindow; @@ -30,17 +31,18 @@ @property (strong) IBOutlet NSTableView *spritesTableView; @property (strong) IBOutlet NSPanel *printerFeedWindow; @property (strong) IBOutlet NSImageView *feedImageView; -@property (strong) IBOutlet NSButton *feedSaveButton; @property (strong) IBOutlet NSTextView *debuggerSideViewInput; @property (strong) IBOutlet NSTextView *debuggerSideView; @property (strong) IBOutlet GBSplitView *debuggerSplitView; @property (strong) IBOutlet NSBox *debuggerVerticalLine; @property (strong) IBOutlet NSPanel *cheatsWindow; @property (strong) IBOutlet GBCheatWindowController *cheatWindowController; +@property (readonly) Document *partner; +@property (readonly) bool isSlave; -(uint8_t) readMemory:(uint16_t) addr; -(void) writeMemory:(uint16_t) addr value:(uint8_t)value; -(void) performAtomicBlock: (void (^)())block; - +-(void) connectLinkCable:(NSMenuItem *)sender; @end diff --git a/Cocoa/Document.m b/Cocoa/Document.m index ff47cd9..ea7ef49 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -8,6 +8,8 @@ #include "GBMemoryByteArray.h" #include "GBWarningPopover.h" #include "GBCheatWindowController.h" +#include "GBTerminalTextFieldCell.h" +#include "BigSurToolbar.h" /* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */ /* Todo: Split into category files! This is so messy!!! */ @@ -26,6 +28,7 @@ enum model { NSMutableAttributedString *pending_console_output; NSRecursiveLock *console_output_lock; NSTimer *console_output_timer; + NSTimer *hex_timer; bool fullScreen; bool in_sync_input; @@ -45,7 +48,7 @@ enum model { bool oamUpdating; NSMutableData *currentPrinterImageData; - enum {GBAccessoryNone, GBAccessoryPrinter} accessory; + enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy, GBAccessoryLinkCable} accessory; bool rom_warning_issued; @@ -64,6 +67,12 @@ enum model { size_t audioBufferNeeded; bool borderModeChanged; + + /* Link cable*/ + Document *master; + Document *slave; + signed linkOffset; + bool linkCableBit; } @property GBAudioClient *audioClient; @@ -79,6 +88,10 @@ enum model { - (void) gotNewSample:(GB_sample_t *)sample; - (void) rumbleChanged:(double)amp; - (void) loadBootROM:(GB_boot_rom_t)type; +- (void)linkCableBitStart:(bool)bit; +- (bool)linkCableBitEnd; +- (void)infraredStateChanged:(bool)state; + @end static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) @@ -136,6 +149,16 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure]; } +static void setWorkboyTime(GB_gameboy_t *gb, time_t t) +{ + [[NSUserDefaults standardUserDefaults] setInteger:time(NULL) - t forKey:@"GBWorkboyTimeOffset"]; +} + +static time_t getWorkboyTime(GB_gameboy_t *gb) +{ + return time(NULL) - [[NSUserDefaults standardUserDefaults] integerForKey:@"GBWorkboyTimeOffset"]; +} + static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) { Document *self = (__bridge Document *)GB_get_user_data(gb); @@ -148,6 +171,26 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [self rumbleChanged:amp]; } + +static void linkCableBitStart(GB_gameboy_t *gb, bool bit_to_send) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self linkCableBitStart:bit_to_send]; +} + +static bool linkCableBitEnd(GB_gameboy_t *gb) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + return [self linkCableBitEnd]; +} + +static void infraredStateChanged(GB_gameboy_t *gb, bool on) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self infraredStateChanged:on]; +} + + @implementation Document { GB_gameboy_t gb; @@ -252,6 +295,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]); GB_apu_set_sample_callback(&gb, audioCallback); GB_set_rumble_callback(&gb, rumbleCallback); + GB_set_infrared_callback(&gb, infraredStateChanged); [self updateRumbleMode]; } @@ -323,9 +367,8 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [_view setRumble:amp]; } -- (void) run +- (void) preRun { - running = true; GB_set_pixels_output(&gb, self.view.pixels); GB_set_sample_rate(&gb, 96000); self.audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { @@ -356,20 +399,78 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) { [self.audioClient start]; } - NSTimer *hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES]; + hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode]; - while (running) { - if (rewind) { - rewind = false; - GB_rewind_pop(&gb); - if (!GB_rewind_pop(&gb)) { - rewind = self.view.isRewinding; + + /* Clear pending alarms, don't play alarms while playing */ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { + NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; + for (NSUserNotification *notification in [center scheduledNotifications]) { + if ([notification.identifier isEqualToString:self.fileName]) { + [center removeScheduledNotification:notification]; + break; } } - else { - GB_run(&gb); + + for (NSUserNotification *notification in [center deliveredNotifications]) { + if ([notification.identifier isEqualToString:self.fileName]) { + [center removeDeliveredNotification:notification]; + break; + } } } +} + +static unsigned *multiplication_table_for_frequency(unsigned frequency) +{ + unsigned *ret = malloc(sizeof(*ret) * 0x100); + for (unsigned i = 0; i < 0x100; i++) { + ret[i] = i * frequency; + } + return ret; +} + +- (void) run +{ + assert(!master); + running = true; + [self preRun]; + if (slave) { + [slave preRun]; + unsigned *masterTable = multiplication_table_for_frequency(GB_get_clock_rate(&gb)); + unsigned *slaveTable = multiplication_table_for_frequency(GB_get_clock_rate(&slave->gb)); + while (running) { + if (linkOffset <= 0) { + linkOffset += slaveTable[GB_run(&gb)]; + } + else { + linkOffset -= masterTable[GB_run(&slave->gb)]; + } + } + free(masterTable); + free(slaveTable); + [slave postRun]; + } + else { + while (running) { + if (rewind) { + rewind = false; + GB_rewind_pop(&gb); + if (!GB_rewind_pop(&gb)) { + rewind = self.view.isRewinding; + } + } + else { + GB_run(&gb); + } + } + } + [self postRun]; + stopping = false; +} + +- (void)postRun +{ [hex_timer invalidate]; [audioLock lock]; memset(audioBuffer, 0, (audioBufferSize - audioBufferPosition) * sizeof(*audioBuffer)); @@ -381,19 +482,50 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) self.view.mouseHidingEnabled = NO; GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); GB_save_cheats(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"cht"] UTF8String]); + unsigned time_to_alarm = GB_time_to_alarm(&gb); + + if (time_to_alarm) { + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate]; + NSUserNotification *notification = [[NSUserNotification alloc] init]; + NSString *friendlyName = [[self.fileName lastPathComponent] stringByDeletingPathExtension]; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil]; + friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""]; + friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + notification.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName]; + notification.informativeText = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName]; + notification.identifier = self.fileName; + notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm]; + notification.soundName = NSUserNotificationDefaultSoundName; + [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"]; + } [_view setRumble:0]; - stopping = false; } - (void) start { - if (running) return; self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSFullScreenWindowMask) != 0; + if (master) { + [master start]; + return; + } + if (running) return; [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start]; } - (void) stop { + if (master) { + if (!master->running) return; + GB_debugger_set_disabled(&gb, true); + if (GB_debugger_is_stopped(&gb)) { + [self interruptDebugInputRead]; + } + [master stop]; + GB_debugger_set_disabled(&gb, false); + return; + } if (!running) return; GB_debugger_set_disabled(&gb, true); if (GB_debugger_is_stopped(&gb)) { @@ -470,6 +602,10 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (IBAction)togglePause:(id)sender { + if (master) { + [master togglePause:sender]; + return; + } if (running) { [self stop]; } @@ -510,6 +646,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) self.debuggerSideViewInput.textColor = [NSColor whiteColor]; self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style; [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"]; + ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &gb; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateSideView) name:NSTextDidChangeNotification @@ -527,16 +664,21 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised; - [self.feedSaveButton removeFromSuperview]; self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[self.fileURL path] lastPathComponent]]; self.debuggerSplitView.dividerColor = [NSColor clearColor]; - - /* contentView.superview.subviews.lastObject is the titlebar view */ - NSView *titleView = self.printerFeedWindow.contentView.superview.subviews.lastObject; - [titleView addSubview: self.feedSaveButton]; - self.feedSaveButton.frame = (NSRect){{268, 2}, {48, 17}}; - + if (@available(macOS 11.0, *)) { + self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded; + self.printerFeedWindow.toolbarStyle = NSWindowToolbarStyleUnifiedCompact; + [self.printerFeedWindow.toolbar removeItemAtIndex:1]; + self.printerFeedWindow.toolbar.items.firstObject.image = + [NSImage imageWithSystemSymbolName:@"square.and.arrow.down" + accessibilityDescription:@"Save"]; + self.printerFeedWindow.toolbar.items.lastObject.image = + [NSImage imageWithSystemSymbolName:@"printer" + accessibilityDescription:@"Print"]; + } + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateHighpassFilter) name:@"GBHighpassFilterChanged" @@ -609,7 +751,6 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) { hex_controller = [[HFController alloc] init]; [hex_controller setBytesPerColumn:1]; - [hex_controller setFont:[NSFont userFixedPitchFontOfSize:12]]; [hex_controller setEditMode:HFOverwriteMode]; [hex_controller setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]]; @@ -695,6 +836,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (void)close { + [self disconnectLinkCable]; [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"]; [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"]; [self stop]; @@ -706,9 +848,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) { [self log:"^C\n"]; GB_debugger_break(&gb); - if (!running) { - [self start]; - } + [self start]; [self.consoleWindow makeKeyAndOrderFront:nil]; [self.consoleInput becomeFirstResponder]; } @@ -727,10 +867,13 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (BOOL)validateUserInterfaceItem:(id)anItem { if ([anItem action] == @selector(mute:)) { - [(NSMenuItem*)anItem setState:!self.audioClient.isPlaying]; + [(NSMenuItem *)anItem setState:!self.audioClient.isPlaying]; } else if ([anItem action] == @selector(togglePause:)) { - [(NSMenuItem*)anItem setState:(!running) || (GB_debugger_is_stopped(&gb))]; + if (master) { + [(NSMenuItem *)anItem setState:(!master->running) || (GB_debugger_is_stopped(&gb)) || (GB_debugger_is_stopped(&gb))]; + } + [(NSMenuItem *)anItem setState:(!running) || (GB_debugger_is_stopped(&gb))]; return !GB_debugger_is_stopped(&gb); } else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE) { @@ -747,6 +890,13 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) else if ([anItem action] == @selector(connectPrinter:)) { [(NSMenuItem*)anItem setState:accessory == GBAccessoryPrinter]; } + else if ([anItem action] == @selector(connectWorkboy:)) { + [(NSMenuItem*)anItem setState:accessory == GBAccessoryWorkboy]; + } + else if ([anItem action] == @selector(connectLinkCable:)) { + [(NSMenuItem*)anItem setState:[(NSMenuItem *)anItem representedObject] == master || + [(NSMenuItem *)anItem representedObject] == slave]; + } else if ([anItem action] == @selector(toggleCheats:)) { [(NSMenuItem*)anItem setState:GB_cheats_enabled(&gb)]; } @@ -972,6 +1122,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [debugger_input_queue removeObjectAtIndex:0]; } [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; + if ((id)input == [NSNull null]) { + return NULL; + } return input? strdup([input UTF8String]): NULL; } @@ -1030,6 +1183,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) { while (!GB_is_inited(&gb)); bool was_running = running && !GB_debugger_is_stopped(&gb); + if (master) { + was_running |= master->running; + } if (was_running) { [self stop]; } @@ -1596,13 +1752,24 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) scale:2.0]; NSRect frame = self.printerFeedWindow.frame; frame.size = self.feedImageView.image.size; + [self.printerFeedWindow setContentMaxSize:frame.size]; frame.size.height += self.printerFeedWindow.frame.size.height - self.printerFeedWindow.contentView.frame.size.height; - [self.printerFeedWindow setMaxSize:frame.size]; [self.printerFeedWindow setFrame:frame display:NO animate: self.printerFeedWindow.isVisible]; [self.printerFeedWindow orderFront:NULL]; }); } + +- (void)printDocument:(id)sender +{ + if (self.feedImageView.image.size.height == 0) { + NSBeep(); return; + } + NSImageView *view = [[NSImageView alloc] initWithFrame:(NSRect){{0,0}, self.feedImageView.image.size}]; + view.image = self.feedImageView.image; + [[NSPrintOperation printOperationWithView:view] runOperationModalForWindow:self.printerFeedWindow delegate:nil didRunSelector:NULL contextInfo:NULL]; +} + - (IBAction)savePrinterFeed:(id)sender { bool shouldResume = running; @@ -1629,6 +1796,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (IBAction)disconnectAllAccessories:(id)sender { + [self disconnectLinkCable]; [self performAtomicBlock:^{ accessory = GBAccessoryNone; GB_disconnect_serial(&gb); @@ -1637,12 +1805,22 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (IBAction)connectPrinter:(id)sender { + [self disconnectLinkCable]; [self performAtomicBlock:^{ - accessory = GBAccessoryPrinter; + accessory = GBAccessoryPrinter; GB_connect_printer(&gb, printImage); }]; } +- (IBAction)connectWorkboy:(id)sender +{ + [self disconnectLinkCable]; + [self performAtomicBlock:^{ + accessory = GBAccessoryWorkboy; + GB_connect_workboy(&gb, setWorkboyTime, getWorkboyTime); + }]; +} + - (void) updateHighpassFilter { if (GB_is_inited(&gb)) { @@ -1753,4 +1931,83 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) { GB_set_cheats_enabled(&gb, !GB_cheats_enabled(&gb)); } + +- (void)disconnectLinkCable +{ + bool wasRunning = self->running; + Document *partner = master ?: slave; + if (partner) { + [self stop]; + partner->master = nil; + partner->slave = nil; + master = nil; + slave = nil; + if (wasRunning) { + [partner start]; + [self start]; + } + GB_set_turbo_mode(&gb, false, false); + GB_set_turbo_mode(&partner->gb, false, false); + partner->accessory = GBAccessoryNone; + accessory = GBAccessoryNone; + } +} + +- (void)connectLinkCable:(NSMenuItem *)sender +{ + [self disconnectAllAccessories:sender]; + Document *partner = [sender representedObject]; + [partner disconnectAllAccessories:sender]; + + bool wasRunning = self->running; + [self stop]; + [partner stop]; + GB_set_turbo_mode(&partner->gb, true, true); + slave = partner; + partner->master = self; + linkOffset = 0; + partner->accessory = GBAccessoryLinkCable; + accessory = GBAccessoryLinkCable; + GB_set_serial_transfer_bit_start_callback(&gb, linkCableBitStart); + GB_set_serial_transfer_bit_start_callback(&partner->gb, linkCableBitStart); + GB_set_serial_transfer_bit_end_callback(&gb, linkCableBitEnd); + GB_set_serial_transfer_bit_end_callback(&partner->gb, linkCableBitEnd); + if (wasRunning) { + [self start]; + } +} + +- (void)linkCableBitStart:(bool)bit +{ + linkCableBit = bit; +} + +-(bool)linkCableBitEnd +{ + bool ret = GB_serial_get_data_bit(&self.partner->gb); + GB_serial_set_data_bit(&self.partner->gb, linkCableBit); + return ret; +} + +- (void)infraredStateChanged:(bool)state +{ + if (self.partner) { + GB_set_infrared_input(&self.partner->gb, state); + } +} + +-(Document *)partner +{ + return slave ?: master; +} + +- (bool)isSlave +{ + return master; +} + +- (GB_gameboy_t *)gb +{ + return &gb; +} @end diff --git a/Cocoa/Document.xib b/Cocoa/Document.xib index 81ce018..a2cf5ee 100644 --- a/Cocoa/Document.xib +++ b/Cocoa/Document.xib @@ -19,7 +19,6 @@ - @@ -60,6 +59,9 @@ + + + @@ -244,9 +246,9 @@ - + - + @@ -507,7 +509,7 @@ - + @@ -786,9 +788,10 @@ + - + @@ -797,20 +800,25 @@ + + + + + + + + + + + + + + + + + - @@ -896,7 +904,7 @@