diff --git a/README.md b/README.md index 3cb6cea4d..1566a2cb0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,46 @@ some rudimentary script function bindings between the bsnes emulator and AngelSc Download nightly binary builds here: https://dev.azure.com/ALttPO/alttpo/_build?definitionId=3&_a=summary&repositoryFilter=3 +Building on macOS +------------------ + +This repo builds natively on both Apple Silicon (arm64) and Intel (x86_64) Macs. The `GNUmakefile` +build system auto-detects your platform and architecture, and macOS needs no extra dependencies +beyond Xcode's command line tools (the UI is built directly against Cocoa; there's no GTK/Qt/Homebrew +requirement like the Linux build has). + +1. Install the Xcode command line tools if you haven't already: + ``` + $ xcode-select --install + ``` +2. Clone this repository: + ``` + $ git clone https://github.com/alttpo/bsnes-as.git + $ cd bsnes-as + ``` +3. Build it: + ``` + $ make -C bsnes build=performance -j$(sysctl -n hw.ncpu) + ``` + This produces `bsnes/out/bsnes.app`. `build=performance` matches what the official CI builds use + (`-O3`); the default `local=true` adds `-march=native`, which is fine as long as you run the + binary on the same Mac you built it on (drop it, e.g. `make -C bsnes build=performance local=false`, + if you intend to copy the binary to a different machine). +4. Run it directly from `bsnes/out/bsnes.app`, or install it system-wide: + ``` + $ make -C bsnes install + ``` + which copies it to `/Applications/bsnes.app` and sets up `~/Library/Application Support/bsnes/`. + +Note: Discord Rich Presence integration is only available on `x86_64` (there's no arm64 build of the +Discord Game SDK bundled in `lib/`). The build system detects this automatically via `arch` and +disables it (`-DDISCORD_DISABLE=1`) rather than failing, so this doesn't block an Apple Silicon build. + +A build you compile yourself is never downloaded from the internet, so it never picks up the +`com.apple.quarantine` extended attribute and won't trigger Gatekeeper's "app is damaged" dialog. If +you're working with a **pre-built** binary instead and hit that dialog, or ROMs won't open when +double-clicked, see [Troubleshooting](#troubleshooting) below. + Screenshots --- @@ -90,5 +130,11 @@ Refer to [this document](angelscript.md) for details on the AngelScript interfac Troubleshooting === -For users of MacOS Catalina (10.15), be sure to extract the download archive using https://www.keka.io/en/ file -archiver. DO NOT use the built-in "Archive Utility.app". +**"bsnes is damaged and can't be opened"**: extract the downloaded archive with `tar` from Terminal, +or a third-party archiver like [Keka](https://www.keka.io/en/), not the built-in "Archive +Utility.app". If you've already extracted it, just run `xattr -cr /path/to/bsnes.app` instead of +re-extracting. + +**Double-clicking a ROM doesn't open it, or opens the wrong copy of bsnes**: this usually means +macOS has multiple old copies of bsnes registered from previous downloads. Delete old copies you no +longer need and re-launch the one you want to keep once. diff --git a/bsnes/target-bsnes/presentation/presentation.cpp b/bsnes/target-bsnes/presentation/presentation.cpp index 5da54dddb..0b8ba65c8 100644 --- a/bsnes/target-bsnes/presentation/presentation.cpp +++ b/bsnes/target-bsnes/presentation/presentation.cpp @@ -279,6 +279,7 @@ auto Presentation::create() -> void { #if defined(PLATFORM_MACOS) Application::Cocoa::onAbout([&] { about.doActivate(); }); Application::Cocoa::onActivate([&] { setFocused(); }); + Application::Cocoa::onOpen([&](string location) { onDrop({location}); }); Application::Cocoa::onPreferences([&] { settingsWindow.show(2); }); Application::Cocoa::onQuit([&] { doClose(); }); #endif diff --git a/bsnes/target-bsnes/resource/bsnes.plist b/bsnes/target-bsnes/resource/bsnes.plist index e9565eebc..ef5b9562f 100644 --- a/bsnes/target-bsnes/resource/bsnes.plist +++ b/bsnes/target-bsnes/resource/bsnes.plist @@ -14,5 +14,135 @@ NSSupportsAutomaticGraphicsSwitching + CFBundlePackageType + APPL + CFBundleDocumentTypes + + + CFBundleTypeName + Super Famicom ROM Image + CFBundleTypeRole + Viewer + CFBundleTypeExtensions + + sfc + smc + + LSItemContentTypes + + org.byuu.bsnes-as.sfc-rom + + LSHandlerRank + Alternate + + + CFBundleTypeName + Game Boy ROM Image + CFBundleTypeRole + Viewer + CFBundleTypeExtensions + + gb + gbc + + LSItemContentTypes + + org.byuu.bsnes-as.gb-rom + + LSHandlerRank + Alternate + + + CFBundleTypeName + BS-X / Sufami Turbo ROM Image + CFBundleTypeRole + Viewer + CFBundleTypeExtensions + + bs + st + + LSItemContentTypes + + org.byuu.bsnes-as.bs-rom + org.byuu.bsnes-as.st-rom + + LSHandlerRank + Alternate + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + org.byuu.bsnes-as.sfc-rom + UTTypeDescription + Super Famicom ROM Image + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + sfc + smc + + + + + UTTypeIdentifier + org.byuu.bsnes-as.gb-rom + UTTypeDescription + Game Boy ROM Image + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + gb + gbc + + + + + UTTypeIdentifier + org.byuu.bsnes-as.bs-rom + UTTypeDescription + BS-X ROM Image + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + bs + + + + + UTTypeIdentifier + org.byuu.bsnes-as.st-rom + UTTypeDescription + Sufami Turbo ROM Image + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + st + + + + diff --git a/bsnes/target-bsnes/settings/settings.cpp b/bsnes/target-bsnes/settings/settings.cpp index f45134494..ecee0296a 100644 --- a/bsnes/target-bsnes/settings/settings.cpp +++ b/bsnes/target-bsnes/settings/settings.cpp @@ -249,6 +249,9 @@ auto SettingsWindow::show(int index) -> void { if(index == 7) compatibilitySettings.setVisible(true); if(index == 8) driverSettings.setVisible(true); panelContainer.resize(); + inputSettings.refreshMappings(); + hotkeySettings.refreshMappings(); + Application::processEvents(); setVisibleAndActivate(); setFocused(); panelList.setFocused(); diff --git a/hiro/cocoa/application.cpp b/hiro/cocoa/application.cpp index 2afaeb9e8..1ca41a494 100755 --- a/hiro/cocoa/application.cpp +++ b/hiro/cocoa/application.cpp @@ -15,6 +15,17 @@ return NO; } +-(void) handleOpenDocumentsEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent { + using hiro::Application; + NSAppleEventDescriptor* fileList = [event paramDescriptorForKeyword:keyDirectObject]; + for(NSInteger i = 1; i <= [fileList numberOfItems]; i++) { + NSAppleEventDescriptor* item = [fileList descriptorAtIndex:i]; + NSURL* url = [NSURL URLWithString:[item stringValue]]; + string path = url && [url isFileURL] ? [[url path] UTF8String] : [[item stringValue] UTF8String]; + if(path) Application::Cocoa::doOpen(path); + } +} + -(void) run:(NSTimer*)timer { using hiro::Application; if(Application::state().onMain) Application::doMain(); @@ -100,6 +111,19 @@ auto pApplication::initialize() -> void { [NSApplication sharedApplication]; cocoaDelegate = [[CocoaDelegate alloc] init]; [NSApp setDelegate:cocoaDelegate]; + + //register as early as possible (before any of the application's own, potentially slow, + //startup work runs) so a cold-launch "open document" Apple Event isn't missed: on a cold + //launch (double-click / Finder "Open With"), macOS can deliver this event before the app + //has finished constructing its UI, and it will not be redelivered if missed + [[NSAppleEventManager sharedAppleEventManager] setEventHandler:cocoaDelegate + andSelector:@selector(handleOpenDocumentsEvent:withReplyEvent:) + forEventClass:kCoreEventClass + andEventID:kAEOpenDocuments]; + + //give the just-registered handler a chance to receive any open-document event that is + //already queued for this process before proceeding into the application's slow startup path + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; } } diff --git a/hiro/cocoa/application.hpp b/hiro/cocoa/application.hpp index 4a65a8813..3c5f7604b 100755 --- a/hiro/cocoa/application.hpp +++ b/hiro/cocoa/application.hpp @@ -4,6 +4,7 @@ } -(NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*)sender; -(BOOL) applicationShouldHandleReopen:(NSApplication*)application hasVisibleWindows:(BOOL)flag; +-(void) handleOpenDocumentsEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent; -(void) run:(NSTimer*)timer; -(void) updateInDock:(NSTimer*)timer; @end diff --git a/hiro/core/application.cpp b/hiro/core/application.cpp index 4d5f2ebc4..c31892b32 100755 --- a/hiro/core/application.cpp +++ b/hiro/core/application.cpp @@ -109,6 +109,14 @@ auto Application::Cocoa::doActivate() -> void { if(state().cocoa.onActivate) return state().cocoa.onActivate(); } +auto Application::Cocoa::doOpen(const string& location) -> void { + //the open-document event can arrive (via a raw Apple Event handler registered at the + //earliest possible moment) before the application has finished constructing its UI and + //registered a real onOpen callback: buffer it so it isn't silently dropped on a cold launch + if(state().cocoa.onOpen) return state().cocoa.onOpen(location); + state().cocoa.pendingOpen.append(location); +} + auto Application::Cocoa::doPreferences() -> void { if(state().cocoa.onPreferences) return state().cocoa.onPreferences(); } @@ -125,6 +133,15 @@ auto Application::Cocoa::onActivate(const function& callback) -> void { state().cocoa.onActivate = callback; } +auto Application::Cocoa::onOpen(const function& callback) -> void { + state().cocoa.onOpen = callback; + if(callback) { + auto pending = move(state().cocoa.pendingOpen); + state().cocoa.pendingOpen = {}; + for(auto& location : pending) callback(location); + } +} + auto Application::Cocoa::onPreferences(const function& callback) -> void { state().cocoa.onPreferences = callback; } diff --git a/hiro/core/application.hpp b/hiro/core/application.hpp index daae3ccca..65c49065d 100644 --- a/hiro/core/application.hpp +++ b/hiro/core/application.hpp @@ -28,10 +28,12 @@ struct Application { struct Cocoa { static auto doAbout() -> void; static auto doActivate() -> void; + static auto doOpen(const string& location) -> void; static auto doPreferences() -> void; static auto doQuit() -> void; static auto onAbout(const function& callback = {}) -> void; static auto onActivate(const function& callback = {}) -> void; + static auto onOpen(const function& callback = {}) -> void; static auto onPreferences(const function& callback = {}) -> void; static auto onQuit(const function& callback = {}) -> void; }; @@ -56,6 +58,8 @@ struct Application { struct Cocoa { function onAbout; function onActivate; + function onOpen; + vector pendingOpen; function onPreferences; function onQuit; } cocoa; diff --git a/nall/decode/rle.hpp b/nall/decode/rle.hpp index 4a6c67ca9..9bfb07cce 100644 --- a/nall/decode/rle.hpp +++ b/nall/decode/rle.hpp @@ -12,7 +12,7 @@ inline auto RLE(array_view input) -> vector { uint base = 0; uint64_t size = 0; - for(uint byte : range(8)) size |= load() << byte * 8; + for(uint byte : range(8)) size |= (uint64_t)load() << byte * 8; output.resize(size); auto read = [&]() -> uint64_t {