/*****************************************************************************\ Snes9x - Portable Super Nintendo Entertainment System (TM) emulator. This file is licensed under the Snes9x License. For further information, consult the LICENSE file in the root directory. \*****************************************************************************/ /*********************************************************************************** SNES9X for Mac OS (c) Copyright John Stiles Snes9x for Mac OS X (c) Copyright 2001 - 2011 zones (c) Copyright 2002 - 2005 107 (c) Copyright 2002 PB1400c (c) Copyright 2004 Alexander and Sander (c) Copyright 2004 - 2005 Steven Seeger (c) Copyright 2005 Ryan Vogt (c) Copyright 2019 - 2022 Michael Donald Buckley ***********************************************************************************/ #import #import "AppDelegate.h" #import "S9xPreferencesConstants.h" NSWindowFrameAutosaveName const kMainWindowIdentifier = @"s9xMainWindow"; NSWindowFrameAutosaveName const kCheatsWindowIdentifier = @"s9xCheatsWindow"; @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { self.s9xEngine = [S9xEngine new]; self.s9xEngine.inputDelegate = self; [self setupDefaults]; [self importRecentItems]; NSWindow *gameWindow = [[NSWindow alloc] initWithContentRect:s9xView.frame styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskClosable|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskResizable backing:NSBackingStoreBuffered defer:NO]; gameWindow.contentView.wantsLayer = YES; gameWindow.contentView.layer.backgroundColor = NSColor.blackColor.CGColor; gameWindow.title = @"Snes9x"; gameWindow.restorationClass = [self class]; gameWindow.frameAutosaveName = kMainWindowIdentifier; gameWindow.releasedWhenClosed = NO; gameWindow.backgroundColor = NSColor.clearColor; if ( ![gameWindow setFrameUsingName:kMainWindowIdentifier] ) { [gameWindow center]; } self.gameWindow = gameWindow; [NSNotificationCenter.defaultCenter addObserverForName:NSWindowWillCloseNotification object:gameWindow queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *notification) { [self.s9xEngine quit]; }]; } - (void)applicationWillTerminate:(NSNotification *)aNotification { [self.s9xEngine quit]; } - (void)setupDefaults { NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; NSDictionary *defaultSettings = @{ kKeyboardPrefs : @{ @(kUp).stringValue : @(kVK_UpArrow), @(kDown).stringValue : @(kVK_DownArrow), @(kLeft).stringValue : @(kVK_LeftArrow), @(kRight).stringValue : @(kVK_RightArrow), @(kY).stringValue : @(kVK_ANSI_X), @(kB).stringValue : @(kVK_ANSI_C), @(kX).stringValue : @(kVK_ANSI_D), @(kA).stringValue : @(kVK_ANSI_V), @(kL).stringValue : @(kVK_ANSI_A), @(kR).stringValue : @(kVK_ANSI_S), @(kStart).stringValue : @(kVK_Space), @(kSelect).stringValue : @(kVK_Return), @(kNumButtons + kUp).stringValue : @(kVK_ANSI_Keypad8), @(kNumButtons + kDown).stringValue : @(kVK_ANSI_Keypad2), @(kNumButtons + kLeft).stringValue : @(kVK_ANSI_Keypad4), @(kNumButtons + kRight).stringValue : @(kVK_ANSI_Keypad6), @(kNumButtons + kY).stringValue : @(kVK_PageDown), @(kNumButtons + kB).stringValue : @(kVK_PageUp), @(kNumButtons + kX).stringValue : @(kVK_End), @(kNumButtons + kA).stringValue : @(kVK_Home), @(kNumButtons + kL).stringValue : @(kVK_ANSI_Keypad0), @(kNumButtons + kR).stringValue : @(kVK_ANSI_KeypadDecimal), @(kNumButtons + kStart).stringValue : @(kVK_ANSI_KeypadEnter), @(kNumButtons + kSelect).stringValue : @(kVK_ANSI_KeypadPlus), @(kKeyFastForward).stringValue : @(kVK_ANSI_Backslash), @(kKeyFreeze).stringValue : @(kVK_ANSI_1), @(kKeyDefrost).stringValue : @(kVK_ANSI_0), @(kKeyScreenshot).stringValue : @(kVK_ANSI_Grave), @(kKeySPC).stringValue : @(kVK_ANSI_R), @(kKeyScopeTurbo).stringValue : @(kVK_ANSI_B), @(kKeyScopePause).stringValue : @(kVK_ANSI_N), @(kKeyScopeCursor).stringValue : @(kVK_ANSI_M), @(kKeyOffScreen).stringValue : @(kVK_Tab), @(kKeyFunction).stringValue : @(kVK_ANSI_Slash), @(kKeyAlt).stringValue : @(kVK_ANSI_Period), @(kKeyFFDown).stringValue : @(kVK_ANSI_Q), @(kKeyFFUp).stringValue : @(kVK_ANSI_W), @(kKeyEsc).stringValue : @(kVK_Escape), @(kKeyTC).stringValue : @(kVK_ANSI_Comma) }, kShowFPSPref : @(NO), kVideoModePref : @(VIDEOMODE_BLOCKY), kMacFrameSkipPref : @(macFrameSkip), kSuperFXClockSpeedPercentPref : @(100), kSoundInterpolationTypePref: @(2), kCPUOverclockPref : @(0), kApplyGameSpecificHacksPref : (@YES), kAllowInvalidVRAMAccessPref : @(NO), kSeparateEchoBufferFromRAMPref : @(NO), kDisableSpriteLimitPref : @(NO), }; [defaults registerDefaults:defaultSettings]; [defaults setBool:NO forKey:@"NSWindowAssertWhenDisplayCycleLimitReached"]; [defaults synchronize]; self.keys = [[defaults objectForKey:kKeyboardPrefs] mutableCopy]; for (NSString *control in [self.keys copy]) { NSInteger key = control.integerValue; NSInteger player = key / kNumButtons; S9xButtonCode buttonCode = (S9xButtonCode)(key - (kNumButtons * player)); [self setButtonCode:buttonCode forKeyCode:self.keys[control].integerValue player:player]; } NSDictionary *joypadPlayerPrefs = [defaults objectForKey:kJoypadPlayerPrefs]; for ( S9xJoypad *joypad in [self listJoypads]) { NSMutableDictionary *joypadPrefs = [[defaults objectForKey:kJoypadInputPrefs] mutableCopy]; if (joypadPrefs == nil) { joypadPrefs = [NSMutableDictionary new]; [defaults synchronize]; } NSString *key = [self prefsKeyForVendorID:joypad.vendorID productID:joypad.productID index:joypad.index]; for ( NSString *playerString in joypadPlayerPrefs ) { if ( [key isEqualToString:joypadPlayerPrefs[playerString]] ) { [self setPlayer:playerString.intValue forVendorID:joypad.vendorID productID:joypad.productID index:joypad.index]; } } NSMutableDictionary *devicePrefs = [joypadPrefs[key] mutableCopy]; if (devicePrefs == nil) { devicePrefs = [NSMutableDictionary new]; for (S9xJoypadInput *input in [self.s9xEngine getInputsForVendorID:joypad.vendorID productID:joypad.productID index:joypad.index]) { devicePrefs[@(input.buttonCode).stringValue] = [self prefValueForCookie:input.cookie value:input.value]; } joypadPrefs[key] = devicePrefs; [defaults setObject:joypadPrefs forKey:kJoypadInputPrefs]; [defaults synchronize]; } else { [self.s9xEngine clearJoypadForVendorID:joypad.vendorID productID:joypad.productID index:joypad.index]; for (NSString *buttonCodeString in devicePrefs) { S9xButtonCode buttonCode = (S9xButtonCode)buttonCodeString.intValue; NSString *str = devicePrefs[buttonCodeString]; uint32 cookie = 0; int32 value = -1; if ([self getValuesFromString:str cookie:&cookie value:&value]) { [self setButton:buttonCode forVendorID:joypad.vendorID productID:joypad.productID index:joypad.index cookie:cookie value:value]; } } } } self.deviceSetting = Gamepads; [self importKeySettings]; [self importGraphicsSettings]; [self applyEmulationSettings]; self.s9xEngine.cheatsEnabled = [defaults boolForKey:kEnableCheatsPref]; [defaults synchronize]; } - (void)setButtonCode:(S9xButtonCode)buttonCode forKeyCode:(int16)keyCode player:(int8)player { if (keyCode < 0) { return; } self.keys[@(buttonCode + (kNumButtons * player)).stringValue] = @(keyCode); S9xButtonCode oldButton = kNumButtons; int8 oldPlayer = -1; if ([self.s9xEngine setButton:buttonCode forKey:keyCode player:player oldButton:&oldButton oldPlayer:&oldPlayer oldKey:NULL]) { if (oldButton >= 0 && oldButton < kNumButtons && oldPlayer >= 0 && oldPlayer < MAC_MAX_PLAYERS && (oldPlayer != player || oldButton != buttonCode)) { [self.keys removeObjectForKey:@(oldButton + (kNumButtons * oldPlayer)).stringValue]; } [NSUserDefaults.standardUserDefaults setObject:[self.keys copy] forKey:kKeyboardPrefs]; } } - (void)clearButton:(S9xButtonCode)button forPlayer:(int8)player { [self.s9xEngine clearButton:button forPlayer:player]; NSMutableDictionary *keyDict = [[NSUserDefaults.standardUserDefaults objectForKey:kKeyboardPrefs] mutableCopy]; [keyDict removeObjectForKey:@(button + (kNumButtons * player)).stringValue]; [NSUserDefaults.standardUserDefaults setObject:[keyDict copy] forKey:kKeyboardPrefs]; [NSUserDefaults.standardUserDefaults synchronize]; } - (NSArray *)listJoypads { return [self.s9xEngine listJoypads]; } - (NSString *)prefsKeyForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index { return [NSString stringWithFormat:@"%@:%@:%@", @(vendorID).stringValue, @(productID).stringValue, @(index).stringValue]; } - (BOOL)getValuesFromString:(NSString *)str vendorID:(uint32 *)vendorID productID:(uint32 *)productID index:(uint32 *)index { if (vendorID == NULL || productID == NULL || index == NULL) { return NO; } NSArray *components = [str componentsSeparatedByString:@":"]; if (components.count != 3) { return NO; } *vendorID = components[0].intValue; *productID = components[1].intValue; *index = components[2].intValue; return YES; } - (NSString *)prefValueForCookie:(uint32)cookie value:(int32)value { return [NSString stringWithFormat:@"%@:%@", @(cookie).stringValue, @(value).stringValue]; } - (BOOL)getValuesFromString:(NSString *)str cookie:(uint32 *)cookie value:(int32 *)value { if (cookie == NULL || value == NULL) { return NO; } NSArray *components = [str componentsSeparatedByString:@":"]; if (components.count != 2) { return NO; } *cookie = components.firstObject.intValue; *value = components.lastObject.intValue; return YES; } - (void)setPlayer:(int8)player forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index { int8 oldPlayer = -1; [self.s9xEngine setPlayer:player forVendorID:vendorID productID:productID index:index oldPlayer:&oldPlayer]; NSMutableDictionary *playersDict = [[NSUserDefaults.standardUserDefaults objectForKey:kJoypadPlayerPrefs] mutableCopy]; if (playersDict == nil) { playersDict = [NSMutableDictionary new]; } if (oldPlayer >= 0 && player != oldPlayer) { [playersDict removeObjectForKey:@(oldPlayer).stringValue]; } playersDict[@(player).stringValue] = [self prefsKeyForVendorID:vendorID productID:productID index:index]; [NSUserDefaults.standardUserDefaults setObject:[playersDict copy] forKey:kJoypadPlayerPrefs]; } - (BOOL)setButton:(S9xButtonCode)button forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index cookie:(uint32)cookie value:(int32)value { S9xButtonCode oldButton = (S9xButtonCode)-1; BOOL result = [self.s9xEngine setButton:button forVendorID:vendorID productID:productID index:index cookie:cookie value:value oldButton:&oldButton]; NSMutableDictionary *prefsDict = [[NSUserDefaults.standardUserDefaults objectForKey:kJoypadInputPrefs] mutableCopy]; NSString *key = [self prefsKeyForVendorID:vendorID productID:productID index:index]; NSMutableDictionary *joypadDict = [prefsDict[key] mutableCopy]; if (result && button != oldButton) { [joypadDict removeObjectForKey:@(oldButton).stringValue]; } joypadDict[@(button).stringValue] = [self prefValueForCookie:cookie value:value]; prefsDict[key] = [joypadDict copy]; [NSUserDefaults.standardUserDefaults setObject:[prefsDict copy] forKey:kJoypadInputPrefs]; return result; } - (void)clearJoypadForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index buttonCode:(S9xButtonCode)buttonCode { [self.s9xEngine clearJoypadForVendorID:vendorID productID:productID index:index buttonCode:buttonCode]; NSString *key = [self prefsKeyForVendorID:vendorID productID:productID index:index]; NSMutableDictionary *joypadsDict = [[NSUserDefaults.standardUserDefaults objectForKey:kJoypadInputPrefs] mutableCopy]; NSMutableDictionary *deviceDict = [joypadsDict[key] mutableCopy]; [deviceDict removeObjectForKey:@(buttonCode).stringValue]; joypadsDict[key] = deviceDict; [NSUserDefaults.standardUserDefaults setObject:[joypadsDict copy] forKey:kJoypadInputPrefs]; [NSUserDefaults.standardUserDefaults synchronize]; } - (NSString *)labelForVendorID:(uint32)vendorID productID:(uint32)productID cookie:(uint32)cookie value:(int32)value { return [self.s9xEngine labelForVendorID:vendorID productID:productID cookie:cookie value:value]; } - (void)importRecentItems { const NSInteger maxRecents = 20; for (NSInteger i = maxRecents - 1; i >= 0; --i) { NSString *key = [NSString stringWithFormat:@"RecentItem_%02tu", i]; NSString *recentItem = [NSUserDefaults.standardUserDefaults objectForKey:key]; if (recentItem != nil) { [NSDocumentController.sharedDocumentController noteNewRecentDocumentURL:[NSURL fileURLWithPath:recentItem]]; [NSUserDefaults.standardUserDefaults removeObjectForKey:key]; } } [[NSUserDefaults standardUserDefaults] synchronize]; } - (void)importKeySettings { NSData *data = [self objectForPrefOSCode:'keyb']; NSUInteger length = data.length; char *bytes = (char*)data.bytes; for ( NSUInteger i = 0; i < length; ++i ) { // The enum values for controls changed between the Carbon and Cocoa versions. // The first 24 enum values are the same, but we have to adjust after that. if ( i < 24 ) { [self setButtonCode:(S9xButtonCode)(i - (i / 12)) forKeyCode:bytes[i] player:i / 12]; } else { [self setButtonCode:(S9xButtonCode)(i - 24 + kKeyFastForward) forKeyCode:bytes[i] player:0]; } } } - (void)importGraphicsSettings { NSData *data = [self objectForPrefOSCode:'dfps']; if (data != nil) { [NSUserDefaults.standardUserDefaults setBool:(data.length > 0 && ((char *)data.bytes)[0]) forKey:kShowFPSPref]; } [self setShowFPS:[NSUserDefaults.standardUserDefaults boolForKey:kShowFPSPref]]; data = [self objectForPrefOSCode:'Vmod']; if ( data != nil) { [NSUserDefaults.standardUserDefaults setInteger:((data.length >= 0 && ((char *)data.bytes)[0]) ? VIDEOMODE_SMOOTH : VIDEOMODE_BLOCKY) forKey:kVideoModePref]; } [self setVideoMode:(int)[NSUserDefaults.standardUserDefaults integerForKey:kVideoModePref]]; } - (id)objectForPrefOSCode:(uint32_t)osCode { NSString *key = [@"Preferences_" stringByAppendingString:[[NSString alloc] initWithBytes:(char *)&osCode length:sizeof(uint32_t) encoding:NSASCIIStringEncoding]]; id obj = [NSUserDefaults.standardUserDefaults objectForKey:key]; if (obj == nil) { osCode =CFSwapInt32(osCode); key = [@"Preferences_" stringByAppendingString:[[NSString alloc] initWithBytes:(char *)&osCode length:sizeof(uint32_t) encoding:NSASCIIStringEncoding]]; obj = [NSUserDefaults.standardUserDefaults objectForKey:key]; if (obj != nil) { [NSUserDefaults.standardUserDefaults removeObjectForKey:key]; } } else { [NSUserDefaults.standardUserDefaults removeObjectForKey:key]; } return obj; } - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { return [self openURL:[NSURL fileURLWithPath:filename]]; } - (IBAction)openDocument:(id)sender { NSOpenPanel* panel = [NSOpenPanel new]; NSModalResponse response = [panel runModal]; if ( response == NSModalResponseOK ) { [self openURL:panel.URL]; } } - (BOOL)openURL:(NSURL *)url { if ([self.s9xEngine loadROM:url]) { [self.s9xEngine recreateS9xView]; NSWindow *gameWindow = self.gameWindow; [gameWindow.contentView addSubview:s9xView]; [s9xView.topAnchor constraintEqualToAnchor:gameWindow.contentView.topAnchor].active = YES; [s9xView.bottomAnchor constraintEqualToAnchor:gameWindow.contentView.bottomAnchor].active = YES; [s9xView.centerXAnchor constraintEqualToAnchor:gameWindow.contentView.centerXAnchor].active = YES; [s9xView.leftAnchor constraintGreaterThanOrEqualToAnchor:gameWindow.contentView.leftAnchor].active = YES; [s9xView.rightAnchor constraintLessThanOrEqualToAnchor:gameWindow.contentView.rightAnchor].active = YES; if (self.cheatsWindowController != nil) { [((S9xCheatsViewController *)self.cheatsWindowController.contentViewController) deselectAll]; [((S9xCheatsViewController *)self.cheatsWindowController.contentViewController) reloadData]; } [gameWindow makeKeyAndOrderFront:self]; [NSDocumentController.sharedDocumentController noteNewRecentDocumentURL:url]; return YES; } return NO; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL action = menuItem.action; if (action == @selector(resume:) || action == @selector(softwareReset:) || action == @selector(hardwareReset:)) { return [self.s9xEngine isRunning] && [self.s9xEngine isPaused]; } else if (action == @selector(updateDeviceSetting:)) { menuItem.state = (self.deviceSetting == (S9xDeviceSetting)menuItem.tag) ? NSOnState : NSOffState; } else if (action == @selector(toggleCheats:)) { if (self.s9xEngine.cheatsEnabled) { menuItem.title = NSLocalizedString(@"Disable Cheats", nil); } else { menuItem.title = NSLocalizedString(@"Enable Cheats", nil); } } return !self.isRunningEmulation; } @dynamic isRunningEmulation; - (BOOL)isRunningEmulation { return [self.s9xEngine isRunning] && ![self.s9xEngine isPaused]; } - (IBAction)terminate:(id)sender { [self.s9xEngine stop]; [NSApp terminate:sender]; } - (IBAction)openPreferencesWindow:(id)sender { if ( self.preferencesWindowController == nil ) { self.preferencesWindowController = [[S9xPreferencesWindowController alloc] initWithWindowNibName:@"S9xPreferencesWindowController"]; } [self.preferencesWindowController showWindow:nil]; [self.preferencesWindowController.window makeKeyAndOrderFront:nil]; [self.preferencesWindowController.window makeKeyWindow]; } - (void)setVideoMode:(int)videoMode { [self.s9xEngine setVideoMode:videoMode]; [NSUserDefaults.standardUserDefaults setObject:@(videoMode) forKey:kVideoModePref]; [NSUserDefaults.standardUserDefaults synchronize]; } - (void)setMacFrameSkip:(int)_macFrameSkip { [self.s9xEngine setMacFrameSkip:_macFrameSkip]; [NSUserDefaults.standardUserDefaults setObject:@(_macFrameSkip) forKey:kMacFrameSkipPref]; [NSUserDefaults.standardUserDefaults synchronize]; } - (void)setShowFPS:(BOOL)showFPS { [self.s9xEngine setShowFPS:showFPS]; [NSUserDefaults.standardUserDefaults setObject:@(showFPS) forKey:kShowFPSPref]; [NSUserDefaults.standardUserDefaults synchronize]; } - (void)applyEmulationSettings { S9xEngine *engine = self.s9xEngine; NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; [engine setSuperFXClockSpeedPercent:(uint32_t)[defaults integerForKey:kSuperFXClockSpeedPercentPref]]; [engine setSoundInterpolationType:(int)[defaults integerForKey:kSoundInterpolationTypePref]]; [engine setCPUOverclockMode:(int)[defaults integerForKey:kCPUOverclockPref]]; [engine setApplySpecificGameHacks:[defaults boolForKey:kApplyGameSpecificHacksPref]]; [engine setAllowInvalidVRAMAccess:[defaults boolForKey:kAllowInvalidVRAMAccessPref]]; [engine setSeparateEchoBufferFromRAM:[defaults boolForKey:kSeparateEchoBufferFromRAMPref]]; [engine setDisableSpriteLimit:[defaults boolForKey:kDisableSpriteLimitPref]]; } - (IBAction)resume:(id)sender { [self.s9xEngine resume]; } - (IBAction)softwareReset:(id)sender { [self.s9xEngine softwareReset]; } - (IBAction)hardwareReset:(id)sender { [self.s9xEngine hardwareReset]; } - (IBAction)updateDeviceSetting:(id)sender { self.deviceSetting = (S9xDeviceSetting)[sender tag]; } - (void)setDeviceSetting:(S9xDeviceSetting)deviceSetting { _deviceSetting = deviceSetting; [self.s9xEngine setDeviceSetting:deviceSetting]; } - (BOOL)handleInput:(S9xJoypadInput *)input fromJoypad:(S9xJoypad *)joypad { if (NSApp.keyWindow != nil && NSApp.keyWindow == self.preferencesWindowController.window) { return [((S9xPreferencesWindowController *) self.preferencesWindowController) handleInput:input fromJoypad:joypad]; } return NO; } - (void)deviceSettingChanged:(S9xDeviceSetting)deviceSetting { _deviceSetting = deviceSetting; } - (IBAction)openCheatsWindow:(id)sender { if (self.cheatsWindowController == nil) { NSWindow *window = [NSWindow windowWithContentViewController:[[S9xCheatsViewController alloc] initWithNibName:@"S9xCheatsViewController" bundle:nil]]; self.cheatsWindowController = [[NSWindowController alloc] initWithWindow:window]; window = self.cheatsWindowController.window; window.title = NSLocalizedString(@"Cheats", nil); window.restorationClass = self.class; window.frameAutosaveName = kCheatsWindowIdentifier; window.releasedWhenClosed = NO; if ( ![window setFrameUsingName:kCheatsWindowIdentifier] ) { [window center]; } } [self.cheatsWindowController showWindow:nil]; [self.cheatsWindowController.window makeKeyAndOrderFront:nil]; [self.cheatsWindowController.window makeKeyWindow]; } - (IBAction)toggleCheats:(id)sender { self.s9xEngine.cheatsEnabled = !self.s9xEngine.cheatsEnabled; [NSUserDefaults.standardUserDefaults setBool:self.s9xEngine.cheatsEnabled forKey:kEnableCheatsPref]; } @end