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 {