/* ** i_main.mm ** **--------------------------------------------------------------------------- ** Copyright 2012-2018 Alexey Lysiuk ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** */ #include "i_common.h" #include "s_soundinternal.h" #include #include #include "c_console.h" #include "c_cvars.h" #include "cmdlib.h" #include "i_system.h" #include "m_argv.h" #include "st_console.h" #include "version.h" #include "printf.h" #include "s_music.h" #include "engineerrors.h" #define ZD_UNUSED(VARIABLE) ((void)(VARIABLE)) // --------------------------------------------------------------------------- CVAR (Bool, i_soundinbackground, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) EXTERN_CVAR(Int, vid_defwidth ) EXTERN_CVAR(Int, vid_defheight) EXTERN_CVAR(Bool, vid_vsync ) int GameMain(); // --------------------------------------------------------------------------- void Mac_I_FatalError(const char* const message) { I_SetMainWindowVisible(false); S_StopMusic(true); FConsoleWindow::GetInstance().ShowFatalError(message); } static bool ReadSystemVersionFromPlist(NSOperatingSystemVersion& version) { #if MAC_OS_X_VERSION_MAX_ALLOWED < 110000 // The version returned by macOS depends on the SDK which the software has been built against. // When built against the 10.15 SDK or earlier, Big Sur returns 10.16 for compatibility with previous numbering. // When built against the 11.0 SDK, it returns 11.0 for forward compatibility. // https://eclecticlight.co/2020/08/13/macos-version-numbering-isnt-so-simple/ // It's impossible to load real SystemVersion.plist when linking with old SDK, i.e. when building for Intel CPU // Any attempt to read this file is redirected to SystemVersionCompat.plist silently // Workaround with the external process is needed in order to report correct macOS version const char *const plistPath = "/System/Library/CoreServices/SystemVersion.plist"; struct stat dummy; if (stat(plistPath, &dummy) != 0) return false; char commandLine[1024] = {}; snprintf(commandLine, sizeof commandLine, "defaults read %s ProductVersion", plistPath); FILE *const versionFile = popen(commandLine, "r"); if (versionFile == nullptr) return false; NSOperatingSystemVersion plistVersion = {}; char versionString[256] = {}; if (fgets(versionString, sizeof versionString, versionFile)) { plistVersion.majorVersion = atoi(versionString); if (const char *minorVersionString = strstr(versionString, ".")) { minorVersionString++; plistVersion.minorVersion = atoi(minorVersionString); if (const char *patchVersionString = strstr(minorVersionString, ".")) { patchVersionString++; plistVersion.patchVersion = atoi(patchVersionString); } } } fclose(versionFile); if (plistVersion.majorVersion != 0) { version = plistVersion; return true; } #endif // MAC_OS_X_VERSION_MAX_ALLOWED < 110000 return false; } void I_DetectOS() { NSOperatingSystemVersion version = {}; if (!ReadSystemVersionFromPlist(version)) { NSProcessInfo *const processInfo = [NSProcessInfo processInfo]; if ([processInfo respondsToSelector:@selector(operatingSystemVersion)]) { version = [processInfo operatingSystemVersion]; } } const char* name = "Unknown version"; switch (version.majorVersion) { case 10: switch (version.minorVersion) { case 12: name = "macOS Sierra"; break; case 13: name = "macOS High Sierra"; break; case 14: name = "macOS Mojave"; break; case 15: name = "macOS Catalina"; break; case 16: name = "macOS Big Sur"; break; } break; case 11: name = "macOS Big Sur"; break; case 12: name = "macOS Monterey"; break; } char release[16] = "unknown"; size_t size = sizeof release - 1; sysctlbyname("kern.osversion", release, &size, nullptr, 0); char model[64] = "Unknown Mac model"; size = sizeof model - 1; sysctlbyname("hw.model", model, &size, nullptr, 0); const char* const architecture = #ifdef __x86_64__ "64-bit Intel"; #elif defined __aarch64__ "64-bit ARM"; #else "Unknown"; #endif Printf("%s running %s %d.%d.%d (%s) %s\n", model, name, int(version.majorVersion), int(version.minorVersion), int(version.patchVersion), release, architecture); } FArgs* Args; // command line arguments namespace { TArray s_argv; int DoMain(int argc, char** argv) { printf(GAMENAME" %s - %s - Cocoa version\nCompiled on %s\n\n", GetVersionString(), GetGitTime(), __DATE__); seteuid(getuid()); // Set LC_NUMERIC environment variable in case some library decides to // clear the setlocale call at least this will be correct. // Note that the LANG environment variable is overridden by LC_* setenv("LC_NUMERIC", "C", 1); setlocale(LC_ALL, "C"); // Set reasonable default values for video settings const NSSize screenSize = [[NSScreen mainScreen] frame].size; vid_defwidth = static_cast(screenSize.width); vid_defheight = static_cast(screenSize.height); vid_vsync = true; Args = new FArgs(argc, argv); NSString* exePath = [[NSBundle mainBundle] executablePath]; progdir = [[exePath stringByDeletingLastPathComponent] UTF8String]; progdir += "/"; auto ret = GameMain(); FConsoleWindow::DeleteInstance(); return ret; } } // unnamed namespace // --------------------------------------------------------------------------- @interface ApplicationController : NSResponder { } - (void)keyDown:(NSEvent*)theEvent; - (void)keyUp:(NSEvent*)theEvent; - (void)applicationDidBecomeActive:(NSNotification*)aNotification; - (void)applicationWillResignActive:(NSNotification*)aNotification; - (void)applicationDidFinishLaunching:(NSNotification*)aNotification; - (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename; - (void)processEvents:(NSTimer*)timer; - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender; - (void)sendExitEvent:(id)sender; @end ApplicationController* appCtrl; @implementation ApplicationController - (void)keyDown:(NSEvent*)theEvent { // Empty but present to avoid playing of 'beep' alert sound ZD_UNUSED(theEvent); } - (void)keyUp:(NSEvent*)theEvent { // Empty but present to avoid playing of 'beep' alert sound ZD_UNUSED(theEvent); } extern bool AppActive; - (void)applicationDidBecomeActive:(NSNotification*)aNotification { ZD_UNUSED(aNotification); S_SetSoundPaused(1); AppActive = true; } - (void)applicationWillResignActive:(NSNotification*)aNotification { ZD_UNUSED(aNotification); S_SetSoundPaused(i_soundinbackground); AppActive = false; } - (void)applicationDidFinishLaunching:(NSNotification*)aNotification { // When starting from command line with real executable path, e.g. ZDoom.app/Contents/MacOS/ZDoom // application remains deactivated for an unknown reason. // The following call resolves this issue [NSApp activateIgnoringOtherApps:YES]; // Setup timer for custom event loop NSTimer* timer = [NSTimer timerWithTimeInterval:0 target:self selector:@selector(processEvents:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; FConsoleWindow::CreateInstance(); const size_t argc = s_argv.Size(); TArray argv(argc + 1, true); for (size_t i = 0; i < argc; ++i) { argv[i] = s_argv[i].LockBuffer(); } argv[argc] = nullptr; exit(DoMain(argc, &argv[0])); } - (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename { ZD_UNUSED(theApplication); // Some parameters from command line are passed to this function // These parameters need to be skipped to avoid duplication // Note: SDL has different approach to fix this issue, see the same method in SDLMain.m const char* const charFileName = [filename UTF8String]; for (size_t i = 0, count = s_argv.Size(); i < count; ++i) { if (0 == strcmp(s_argv[i], charFileName)) { return FALSE; } } bool iwad = false; if (const char* const extPos = strrchr(charFileName, '.')) { iwad = 0 == stricmp(extPos, ".iwad") || 0 == stricmp(extPos, ".ipk3") || 0 == stricmp(extPos, ".ipk7"); } s_argv.Push(iwad ? "-iwad" : "-file"); s_argv.Push(charFileName); return TRUE; } - (void)processEvents:(NSTimer*)timer { ZD_UNUSED(timer); NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; while (true) { NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate dateWithTimeIntervalSinceNow:0] inMode:NSDefaultRunLoopMode dequeue:YES]; if (nil == event) { break; } I_ProcessEvent(event); [NSApp sendEvent:event]; } [NSApp updateWindows]; [pool release]; } - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { [self sendExitEvent:sender]; return NSTerminateLater; } - (void)sendExitEvent:(id)sender { throw CExitEvent(0); } @end // --------------------------------------------------------------------------- namespace { NSMenuItem* CreateApplicationMenu() { NSMenu* menu = [NSMenu new]; [menu addItemWithTitle:[@"About " stringByAppendingString:@GAMENAME] action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:[@"Hide " stringByAppendingString:@GAMENAME] action:@selector(hide:) keyEquivalent:@"h"]; [[menu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"] setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand]; [menu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:[@"Quit " stringByAppendingString:@GAMENAME] action:@selector(sendExitEvent:) keyEquivalent:@"q"]; NSMenuItem* menuItem = [NSMenuItem new]; [menuItem setSubmenu:menu]; if ([NSApp respondsToSelector:@selector(setAppleMenu:)]) { [NSApp performSelector:@selector(setAppleMenu:) withObject:menu]; } return menuItem; } NSMenuItem* CreateEditMenu() { NSMenu* menu = [[NSMenu alloc] initWithTitle:@"Edit"]; [menu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"]; [menu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"]; [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"]; [menu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; [menu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; [menu addItemWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""]; [menu addItemWithTitle:@"Select All" action:@selector(selectAll:) keyEquivalent:@"a"]; NSMenuItem* menuItem = [NSMenuItem new]; [menuItem setSubmenu:menu]; return menuItem; } NSMenuItem* CreateWindowMenu() { NSMenu* menu = [[NSMenu alloc] initWithTitle:@"Window"]; [NSApp setWindowsMenu:menu]; [menu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; [menu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""]; [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""]; NSMenuItem* menuItem = [NSMenuItem new]; [menuItem setSubmenu:menu]; return menuItem; } void CreateMenu() { NSMenu* menuBar = [NSMenu new]; [menuBar addItem:CreateApplicationMenu()]; [menuBar addItem:CreateEditMenu()]; [menuBar addItem:CreateWindowMenu()]; [NSApp setMainMenu:menuBar]; } void ReleaseApplicationController() { if (NULL != appCtrl) { [NSApp setDelegate:nil]; [NSApp deactivate]; [appCtrl release]; appCtrl = NULL; } } } // unnamed namespace int main(int argc, char** argv) { for (int i = 0; i < argc; ++i) { const char* const argument = argv[i]; #if _DEBUG if (0 == strcmp(argument, "-wait_for_debugger")) { NSAlert* alert = [[NSAlert alloc] init]; [alert setMessageText:@GAMENAME]; [alert setInformativeText:@"Waiting for debugger..."]; [alert addButtonWithTitle:@"Continue"]; [alert runModal]; } #endif // _DEBUG s_argv.Push(argument); } NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; [NSApplication sharedApplication]; // The following code isn't mandatory, // but it enables to run the application without a bundle if ([NSApp respondsToSelector:@selector(setActivationPolicy:)]) { [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; } CreateMenu(); atexit(ReleaseApplicationController); appCtrl = [ApplicationController new]; [NSApp setDelegate:appCtrl]; [NSApp run]; [pool release]; return EXIT_SUCCESS; }