QT AGI Studio for Mac

31st January 2019 | Programming

I've spent the past several months learning how to extract the resources from classic Sierra games built using the AGI game engine. I then started to contemplate building my own AGI Viewer program, which might then evolve into a more full featured IDE to be able to create or modify AGI games. Or...I could build upon what tools already exist instead of completely rebuilding the machine from scratch.

There have been numerous utilities around for decades dedicated to working with AGI games, however many of these programs were developed as far back as the 1990s when Windows was very dominant, so it was rare that apps were being built for alternative operating systems. Since most of my work is done on a Mac, I started looking for Mac alternatives.

I first came across Jeremy Penner's Mac port of the Linux version of AGI Studio. This project dates back to 2003, but I was able to download it and install it on my PowerBook G4 and it worked out pretty well to make some minor modifications to King's Quest 1, as seen in the screenshot below.

I started looking into QT AGI Studio which looked fairly complete. This version is for Linux, but I noticed it was built using the Qt frameworks. I've worked with Qt before, so I knew that it was possible to build apps for multiple platforms. As far as cross platform tools go, Qt is not the worst, but it still falls short of properly supporting and building Mac apps. Still, it would probably take less time to port AGI Studio to the Mac than trying to rewrite it in Objective-C and C (however, as I would discover, there are times I almost wished I was working in Xcode).

Setting up Qt and configuring the project to build for the Mac still proved to be an ordeal, as I have recorded below. Trying to find good instructions on building for the Mac with Qt Creator is like a massive treasure hunt. One will find a few hints here, a few hints there, but rarely are all of the steps available in any single location. I am hoping that these steps will also prove useful for anyone who is looking at trying to build an app for the Mac using Qt.


Download Qt Creator and Frameworks

Since the most recent builds of QT AGI Studio were from 2013, I opted to work with the versions of Qt from that time period. I had tried compiling the project with a more modern version of Qt, but it complained about the Qt3 support, and I did not want to have to wrestle with trying to upgrade from deprecated Qt3 frameworks to Qt5.

Qt has passed through several companies over the years, so it was not quite so easy to find the older versions of Qt. After some searching, I did find a good mirror site which provided many versions of both the frameworks and the Qt Creator IDE. The following are the versions I used:

The Qt frameworks will be installed in the /Library/Frameworks folder and other Qt applications and documentation will be installed in /Developer. For my development system (a 2007 MacBook Pro), I also used Xcode 3.2.5 on Mac OS X 10.6 Snow Leopard in addition to the Qt tools.

Modifying the .pro File

Upon the initial attempt to build the project (note: remember to first run qmake on the project), I received the following error:

cc1plus: error: unrecognized command line option "-Wno-unused-result"
make: *** [main.o] Error 1

I tried a variety of options and workarounds until I discovered that I did not need the DEFINES and QMAKE_CXXFLAGS to compile on Mac OS X. I commented out these two lines in the .pro file. The DEFINES section can remain and the app will still compile and run, but since it specifies Win32, I removed it for the Mac build.

# Add these lines
CONFIG += app_bundle
ICON = application.icns
RESOURCES += agistudio.qrc # Not necessary if a qrc file is not added

# Comment out these lines
# DEFINES += QT_DLL QT_THREAD_SUPPORT # win32 
# QMAKE_CXXFLAGS += -Wno-unused-result # -spec macx-g++

I only needed to add three new lines: CONFIG, ICON, and RESOURCES. CONFIG tells Qt Creator to build the app as a Mac app bundle, otherwise the app will be compiled as a single executable file. The ICON line is to specify the name of the icon file to set for the app bundle. RESOURCES is useful if you add a qrc file, such as if you are adding additional images to your project.

Updating the Info.plist

When Qt creates a Mac application, it only provides a bare bones Info.plist file. I ended up manually modifying the Info.plist file to update the necessary values and to add in any missing key-value pairs. This file is then copied into the app bundle to replace the Info.plist generated by Qt Creator. The following is the default Info.plist generated by Qt Creator:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
	<key>NSPrincipalClass</key>
	<string>NSApplication</string>
	<key>CFBundleIconFile</key>
	<string>application</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleGetInfoString</key>
	<string>Created by Qt/QMake</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleExecutable</key>
	<string>agistudio</string>
	<key>CFBundleIdentifier</key>
	<string>com.yourcompany.agistudio</string>
	<key>NOTE</key>
	<string>This file was generated by Qt/QMake.</string>
</dict>
</plist>

The default Info.plist is sparse on the necessary details, so several new key-value pairs needed to be added, a couple modified, and one removed.

After making the necessary updates, the Info.plist will look more like the following:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
	<key>NSPrincipalClass</key>
	<string>NSApplication</string>
	<key>CFBundleIconFile</key>
	<string>application</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleGetInfoString</key>
	<string>AGI Studio 1.3.0</string>
	<key>CFBundleShortVersionString</key>
	<string>1.3.0</string>
	<key>CFBundleVersion</key>
	<string>1.3.0.1</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleExecutable</key>
	<string>agistudio</string>
	<key>CFBundleName</key>
	<string>AGI Studio</string>
	<key>CFBundleIdentifier</key>
	<string>com.edenwaith.agistudio</string>
	<key>NSHumanReadableCopyright</key>
	<string>Copyright © 2019. All rights reserved</string>
</dict>
</plist>

Handling the -psn Argument

On older versions of Mac OS X, launching an app will pass a process serial number as an argument to the app's executable in the form of -psn_0_1446241. According to a Stack Overflow post, this argument is no longer passed in newer versions of macOS. I have modified a bit of the code in main.cpp to watch for the -psn argument, and if it is seen, ignore it so the app can continue launching.

// startsWith() is a custom function to check if the argument is prefixed with -psn
if (startsWith("-psn", argv[i])) {
	break;
}

Adding a Custom Icon

Since this is a Mac application, I wanted to provide a high resolution icon, instead of using the generic AGI-character icon that is set in the app.

  1. Using Icon Composer, I generated the app icon, modeled after the traditional Sierra On-line logo.
  2. The .icns file is placed in the src folder along side the rest of the images and source code of the project.
  3. In the .pro file, the line ICON = application.icns is added so Qt Creator knows to add the icon to the app bundle.
  4. In the app's Info.plist, set the value for the CFBundleIconFile key to the name of the icon file (in this example, application.icns). If the icon does not appear once the app has been built, rename the app bundle and that will "refresh" the icon for the app, then you can rename the app back to its original name.

Mac Conditional Checks

Since this Mac app has been set up to use an icns file for the application icon, a bit of platform-specific code is needed so it does not try and set another icon for the app. In the menu.cpp file, I performed a check to see if it was not a Mac that was running the code. In this example, I checked that constant Q_OS_MAC was not defined before setting the icon. On a Mac, this would skip the setIcon method since the application icon has already been set. If this method was called, it would change the app icon.

// For non-Mac systems, set the application icon to app_icon
#if defined(Q_OS_MAC) == false
  setIcon((const char**)app_icon);
#endif

If you need to check if the app is running on another system such as Linux or Windows, perform a check like #ifdef __linux__ or #ifdef _WIN32.

Modifying XPM Files

The QT AGI Studio project makes use of XPM (X PixMap) files for images, which is an interesting design choice instead of using another common image format such as PNG, GIF, or JPG. The thing which is interesting about XPM files is that they are actually text files which can be read in and treated as C code. I was able to use BBEdit to take any of the XPM files and open them up, which resembled a mix of C, HTML, and ASCII art. This proved to be useful to fix a problem I later encountered.

XPM is not a common image format so few image editors support it. Fortunately, an old version of Pixelmator (v. 1.6.7) on my computer was able to export to XPM. However, after I modified the image and then tried to build the app, I received the following errors:

../src/logo.xpm:215: warning: deprecated conversion from string constant to 'char*'
../src/logo.xpm:215: warning: deprecated conversion from string constant to 'char*'
../src/menu.cpp: In constructor 'About::About(QWidget*, const char*)':
../src/menu.cpp:926: error: 'logo' was not declared in this scope
../src/logo.xpm: At global scope:
../src/logo.xpm:2: warning: 'xpm_' defined but not used

This initially greatly confused me why Qt Creator was complaining just because I modified an image. It wasn't until I investigated the XPM format further that I discovered the source of the conflict. If I opened up the original logo.xpm file in a text editor, the beginning of the file looked like this:

/* XPM */
static const char *logo[] = {
/* columns rows colors chars-per-pixel */
"320 148 68 1",
"  c #000000",
". c #505050",

After I modified the image and then exported it from Pixelmator, it looked like this:

/* XPM */
static char *xpm_[] = {
/* columns rows colors chars-per-pixel */
"320 148 61 1",
"  c #000000",
". c #505050",

It was quick work to spot the difference between the two files, which make it clear what some of the errors and warnings meant. When Pixelmator exported the file, it saved the variable with the generic name of xpm_, but the app was expecting the variable to share the same name as the file. Once I changed xpm_ to logo, the app was able to build again without error.

I also tried to load in other image types (e.g. png) by loading them in a qrc resource file, but for some unknown and odd reason, the AGI Studio project never could see the non-XPM files. At this time, I do not really need to add any other images, but if I do, I can make due with the XPM files. Since this project makes use of Qt 3 frameworks, perhaps there is some odd limitation or other complication which is preventing more common image formats from being used in this project.

macdeployqt

Even though Qt Creator can create a Macintosh application bundle, it is often incomplete and requires some additional manual tweaking to finish the set-up. Since most Qt projects make use of the Qt frameworks, which are not native to the Mac OS, the frameworks need to be provided. One can distribute the frameworks via an installer, but the more Mac-like method is to include the frameworks within the app bundle. This is one of the key pieces of functionality where the macdeployqt utility comes in. If you have installed the Qt frameworks and Qt Creator, macdeployqt should already be set up on your development system. If you are not certain, type which macdeployqt in the Terminal. On my system it is installed at /usr/bin/macdeployqt. It is designed to automate the process of creating a deployable application bundle that contains the Qt libraries as private frameworks.

The basic call to include the frameworks is straightforward by calling macdeployqt on the app bundle.

macdeployqt AGI\ Studio.app

macdeployqt has additional functionality such as packaging the app into a distributable disk image (dmg), or in newer versions of macdeployqt, code sign the app.

Once macdeployqt has been run against the app bundle, the necessary Qt frameworks should be installed within the app's Frameworks folder. Unfortunately, this is still not without its faults. Try running the app, and you will likely see a similar error in the Console:

1/21/19 10:13:35 PM [0x0-0x275275].com.yourcompany.agistudio[15522] On Mac OS X, you might be loading two sets of Qt binaries into the same process. Check that all plugins are compiled against the right Qt binaries. Export DYLD_PRINT_LIBRARIES=1 and check that only one set of binaries are being loaded.

You might even encounter a bevy of other warnings in the background while trying to use the app since it is confused about which version of the Qt frameworks should be used.

objc[10717]: Class QNSImageView is implemented in both /Library/Frameworks/QtGui.framework/Versions/4/QtGui and /Users/admin/Programs/agistudio-1.3.0/agistudio-build-Desktop-Debug/agistudio.app/Contents/MacOS/./../Frameworks/QtGui.framework/Versions/4/QtGui. One of the two will be used. Which one is undefined. QObject::moveToThread: Current thread (0x101911fb0) is not the object's thread (0x113e48c50). Cannot move to target thread (0x101911fb0)

If you try and launch the app from the Terminal by directly calling the app's executable, there might be another set of errors:

$ ./agistudio
dyld: Library not loaded: Qt3Support.framework/Versions/4/Qt3Support
  Referenced from: ./agistudio
  Reason: image not found
Abort trap: 6

The problem here is that the executable (agistudio) does not know about the Qt frameworks, so Mac OS X is confused about which version of the frameworks to use if there are multiple copies on a system. This issue will be addressed in the next section.

otool and install_name_tool

Even if the Qt frameworks are not fully installed yet, the app might still launch, but will likely crash once a Qt library call is made, such as trying to open up the About QT menu. Trying to do so without having the frameworks properly installed will result in the app crashing.

Process:         agistudio [10717]
Path:            /Users/admin/Programs/agistudio-1.3.0/agistudio-build-Desktop-Debug/AGI Studio.app/Contents/MacOS/./agistudio
Identifier:      com.edenwaith.agistudio
Version:         1.3.0 (1.3.0)
Code Type:       X86-64 (Native)
Parent Process:  tcsh [10506]

Date/Time:       2019-01-14 19:04:57.106 -0700
OS Version:      Mac OS X 10.6.8 (10K549)
Report Version:  6

Interval Since Last Report:          1440136 sec
Crashes Since Last Report:           88
Per-App Interval Since Last Report:  2252 sec
Per-App Crashes Since Last Report:   7
Anonymous UUID:                      4FB4767E-3345-46C6-AA9E-288C90CF1074

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Crashed Thread:  0  Dispatch queue: com.apple.main-thread

Application Specific Information:
abort() called

Thread 0 Crashed:  Dispatch queue: com.apple.main-thread
0   libSystem.B.dylib             	0x00007fff81e6d0b6 __kill + 10
1   libSystem.B.dylib             	0x00007fff81f0d9f6 abort + 83
2   QtCore                        	0x0000000117254cf5 qt_message_output(QtMsgType, char const*) + 117
3   QtCore                        	0x0000000117254ed7 qt_message_output(QtMsgType, char const*) + 599
4   QtCore                        	0x000000011725509a qFatal(char const*, ...) + 170
5   QtGui                         	0x000000011670ee95 QWidgetPrivate::QWidgetPrivate(int) + 853
6   QtGui                         	0x000000011671e55b QWidget::QWidget(QWidget*, QFlags) + 59
7   QtGui                         	0x000000011667cd39 QDesktopWidget::QDesktopWidget() + 41
8   QtGui                         	0x00000001166c6f2b QApplication::desktop() + 59
9   QtGui                         	0x00000001166781db QMacCocoaAutoReleasePool::~QMacCocoaAutoReleasePool() + 4203
.
.
.
50  QtCore                        	0x00000001013e54c4 QEventLoop::exec(QFlags) + 324
51  QtCore                        	0x00000001013e7bac QCoreApplication::exec() + 188
52  com.edenwaith.agistudio       	0x00000001000388d5 main + 794 (main.cpp:99)
53  com.edenwaith.agistudio       	0x0000000100003a3c start + 52

To notify the executable file where to find the Qt frameworks, we need to use the install_name_tool utility. From the Terminal, change into the directory AGI Studio.app/Contents/MacOS and then run otool -L agistudio.


$ otool -L agistudio 
agistudio:
	Qt3Support.framework/Versions/4/Qt3Support (compatibility version 4.8.0, current version 4.8.4)
	QtGui.framework/Versions/4/QtGui (compatibility version 4.8.0, current version 4.8.4)
	QtCore.framework/Versions/4/QtCore (compatibility version 4.8.0, current version 4.8.4)
	/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
	/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 103.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.11)

Note how the first three lines reference Qt frameworks, yet they are not given a proper relative path to the agistudio executable file. To fix this issue, run the following three lines to resolve the path for each of the three frameworks.

install_name_tool -change Qt3Support.framework/Versions/4/Qt3Support @executable_path/../Frameworks/Qt3Support.framework/Versions/4/Qt3Support ./agistudio
install_name_tool -change QtGui.framework/Versions/4/QtGui @executable_path/../Frameworks/QtGui.framework/Versions/4/QtGui ./agistudio
install_name_tool -change QtCore.framework/Versions/4/QtCore @executable_path/../Frameworks/QtCore.framework/Versions/4/QtCore ./agistudio

Run otool -L agistudio again and you will see that each of the Qt frameworks is now prefixed with @executable_path/../ which references the path of agistudio.


$ otool -L agistudio
agistudio:
	@executable_path/../Frameworks/Qt3Support.framework/Versions/4/Qt3Support (compatibility version 4.8.0, current version 4.8.4)
	@executable_path/../Frameworks/QtGui.framework/Versions/4/QtGui (compatibility version 4.8.0, current version 4.8.4)
	@executable_path/../Frameworks/QtCore.framework/Versions/4/QtCore (compatibility version 4.8.0, current version 4.8.4)
	/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
	/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 103.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.11)

Now the app can be launched and used without crashing. With the Qt frameworks properly installed, the AGI Studio app can be distributed without the need of an installer.

To Do

This is the initial effort to port AGI Studio to the Mac, but there is still plenty of work to continue improving this project. As I work further with AGI Studio, I am hoping that I will resolve some of these issues over time.

Conclusion

Like any cross-platform toolset, it often settles for the least common denominator, so it will never be a perfect solution, but all things considered, Qt is passable. This process was not without its challenges, but it was ultimately a rewarding and educational endeavor. Getting QT AGI Studio to build on a Mac was just the first step for my next project, and I imagine as I begin to work with AGI Studio for Mac I will continue to refine the program to smooth out the wrinkles and glitches.

The standalone app can be downloaded here and the source code is available on my GitHub page.

References: