Edenwaith Blog

AGS, Steam, Mac, and Dynamic Libraries

15th October 2024 | Games

This article is a follow up to the previous post, and will be far more technical, but it is not required reading for configuring an Adventure Game Studio (AGS) game to work with Steam. This will detail the processes I used to figure out how to get third party libraries to work together with an AGS game, Steam, and the Mac.

When I was first trying to figure out how to get Steam achievements to work in a Mac game, it required a lot of trial and error, with many failures along the way. There seemed to be no concrete details on the proper way to set up the game. Windows versions of AGS games just dump all of the extra files in the same folder as the executable, but a Mac app bundle is structured in a different manner. I tried a lot of experiments in where to place the dynamic libraries and the steam_appid.txt file, but to no avail. Looking at other games, I've seen files like libsteam_api.dylib located in various locations, or several variants of the agsteam library (libagsteam.dylib, libagsteam-unified.dylib, libagsteam-disjoint.dylib).

In my more recent attempts to get Steam achievements to work, I was developing and testing on a 2021 MacBook Pro with an M1 Pro processor. Despite following what seemed to be functional steps to configure the game properly, I wasn't seeing any achievements trigger. It wasn't until I tested on an older Intel-based Mac did I finally see some achievements.

Interesting...

This eventually led me to determine why some people had been able to get things to work, but I was not also seeing similar positive results. It required the game to be code signed on an older Intel-based Mac to be at least somewhat functional. This showed some promise, but it wasn't good enough. As of 2024, macOS still supports older programs built for Intel processors, but this will certainly not last forever. I've already been through the PPC to Intel migration, so it will likely not be much longer before Apple officially drops Intel support and removes Rosetta 2. Keeping this in mind, it was my goal to make sure that AGS games and associated libraries were ready for the next generation of Apple-supported processors. This required having Universal Binary versions of the AGS shell and popular third party libraries. The first task has already been accomplished, but ensuring that the libraries had been updated was going to be trickier.

I found inconsistencies in other AGS games in what files were needed and where they needed to be placed within the app bundle. In the end, I only needed three files to get Steam achievements to work, all which are placed in the bundle's Contents/Resources folder:

libsteam_api.dylib

The libsteam_api.dylib file is the dynamic library provided by Steam. This file can be found in other Mac games, regardless of the game engine, so the game can communicate with the Steam client. Download the Steamworks SDK from the Steamworks API Overview page. Uncompress the zip file, and libsteam_api.dylib is found in the Steamworks/sdk/redistributable_bin/osx folder. I used version 1.59 from February 2024.

steam_appid.txt

This is a very small text file (just a couple of bytes) which contains only the application ID of the game. According to the Heathen KB site, steam_appid.txt is not required for production versions of the game, but it is useful for cases such as dev testing. Still, I prefer to include this file "just in case".

libagsteam-unified.dylib

For AGS games to communicate with the Steam client, it requires integrating with the agsteam and ags2client plug-ins. Looking through other games, I've seen variants of the agsteam library, such as libagsteam.dylib, libagsteam-disjoint.dylib, and libagsteam-unified.dylib. According to agsteam's documentation, the unified build is the default and should work better if one wants to use AGS2Client to work with either Steam or GoG achievements. All of the games I found which used this library had an older Intel-only version. To get this to work properly on modern Macs, I would need to rebuild the library as a Universal Binary.

Building a Universal Binary libagsteam-unified.dylib

This dynamic library is the lynchpin to get Steam achievements to work with AGS games on the Mac. However, the last set of agsteam releases was back in 2018, several years before Apple Silicon was even announced, so the Mac library would have only been built for Intel processors.

The source project has a Makefile, so I decided to try and rebuild the library using make. Except...it didn't work. I tried to fix and modify the Makefile, and even purchased Managing Projects with GNU Make to improve my Make-fu, but to no avail. After banging my head against the wall for a little too long, I decided to write a bash script which followed the direction of the Makefile. The following version of the script creates a Universal Binary build of libagsteam-unified.dylib, which links against the Steamworks SDK (version 1.59).

#!/bin/bash

# File: build_agsteam.sh
# Description: Build script to build the libagsteam-unified.dylib as a Universal Binary.  Replacement for this
# project's Makefile.  
# Author: Chad Armstrong
# Date: 14-17 September 2024

# Define some paths...
PATH_SRC=.
PATH_AGS2CLIENT=$PATH_SRC/ags2client
# Path to the Steamworks SDK folder.  This build pointed to Steamworks build 159
PATH_STEAMWORKS=$PATH_SRC/../steamworks/sdk
PATH_STEAMWORKS_INC=$PATH_STEAMWORKS/public
PATH_STEAMWORKS_LIB=$PATH_STEAMWORKS/redistributable_bin
PATH_BUILD=$PATH_SRC/Solutions/build
SRCS="ags2client/IAGS2Client.cpp ags2client/IClientAchievements.cpp ags2client/IClientLeaderboards.cpp  ags2client/IClientStats.cpp ags2client/main.cpp AGS2Client.cpp AGSteamPlugin.cpp SteamAchievements.cpp SteamLeaderboards.cpp SteamStats.cpp"

# .o object files for ags2client end up in a separate directory
# https://linuxsimply.com/bash-scripting-tutorial/string/manipulation/string-replace/
OBJS="${SRCS//.cpp/.o}"
CXXFLAGS="-g -Wall -std=c++11 -O2 -fPIC -I$PATH_STEAMWORKS_INC"
CXX=g++ # g++ is needed to compile this project, clang throws errors

PATH_OSX_BUILD="$PATH_BUILD/osx" # platform_build_path
PATH_OSX_OBJ="$PATH_OSX_BUILD/obj" # platform_obj_path

# Get object file path names (e.g., ./Solutions/build/osx/obj/ags2client/IAGS2Client.o) for all object files
OSX_OBJ_FILE_PATHS="" # ${OBJS//PATH_OSX_OBJ/ /}" # obj_file_paths

# This may not be the most elegant way to do this, but it works to construct the list where each 
# of the object files is stored
for obj in ${OBJS}; do
	OSX_OBJ_FILE_PATHS+="$PATH_OSX_OBJ/$obj "
done

# OS X
OSX_CXX_FLAGS="-DMAC_VERSION"
OSX_STEAMWORKS_DIR=osx
OSX_LIB_FLAGS="-dynamiclib -o $PATH_OSX_BUILD/libagsteam-unified.dylib"

# Create a directory at ./Solutions/build/osx/obj/ags2client
mkdir -p "$PATH_OSX_OBJ/ags2client"

# Create an array of the source files, based from the SRCS string
SRCS_ARRAY=($SRCS)

# Generate the object (.o) files
# This works in bash, but not zsh
for filename in $SRCS; do
	# Swap the .cpp for a .o file extension
	OBJ_FILENAME="${filename//.cpp/.o}"
	# Example: g++ -g -Wall -std=c++11 -O2 -fPIC -I./../steamworks/sdk/public -DAGS2CLIENT_UNIFIED_CLIENT_NAME -DMAC_VERSION -c SteamStats.cpp -o ./Solutions/build/osx/obj/SteamStats.o
	$CXX -arch x86_64 -arch arm64 $CXXFLAGS -DAGS2CLIENT_UNIFIED_CLIENT_NAME $OSX_CXX_FLAGS -c $filename -o "$PATH_OSX_OBJ/$OBJ_FILENAME"
done

# Create the unified build, link up the object files created in the previous step
# Example: # g++  -v -arch x86_64 -arch arm64 -L./../steamworks/sdk/redistributable_bin/osx -lsteam_api -dynamiclib -o ./Solutions/build/osx/libagsteam.dylib ./Solutions/build/osx/obj/ags2client/IAGS2Client.o ./Solutions/build/osx/obj/ags2client/IClientAchievements.o ./Solutions/build/osx/obj/ags2client/IClientLeaderboards.o ./Solutions/build/osx/obj/ags2client/IClientStats.o ./Solutions/build/osx/obj/ags2client/main.o ./Solutions/build/osx/obj/AGS2Client.o ./Solutions/build/osx/obj/AGSteamPlugin.o ./Solutions/build/osx/obj/SteamAchievements.o ./Solutions/build/osx/obj/SteamLeaderboards.o ./Solutions/build/osx/obj/SteamStats.o 
$CXX -v -arch x86_64 -arch arm64 -L$PATH_STEAMWORKS_LIB/$OSX_STEAMWORKS_DIR -lsteam_api $OSX_LIB_FLAGS $OSX_OBJ_FILE_PATHS

Troubleshooting

Initially trying to implement Steam achievements for the Mac was a classic case of black box testing. I'd try something, but nothing would happen. I'd try something else. Still nothing. After enough random experimentation, I would make some tentative progress. Slowly I'd take additional steps to reach the ultimate goal, but not without needing additional tools and methods to inspect what did and did not work along the way.

A classic method of debugging is via print statements, or in this case, inspecting what is being output to either the console or log files. It can be useful to launch the game via the Terminal (./MyGame.app/Contents/MacOS/AGS) and look at the console log to see if there are any hints why Steam achievements may not be working, or if there is a crash. In my testing, I saw cases where a third party library (.dylib) would not load properly because it was reliant upon another library, was built for another architecture (x86 versus arm64), or was built for too new of a system and didn't work on an older OS.

In my early attempts, I would see a vague line like this:

# Plugin 'agsteam-unified' could not be loaded (expected 'libagsteam-unified.dylib'), trying built-in plugins...

After rebuilding AGS a few times and adding more debugging lines, I started to figure out more of what was happening. Several other developers I conferred with were both using Intel-based Macs to build their apps, and for whatever reason, those don't have issues with getting the Steam achievements to work. I started tracking down in files like agsplugin.cpp and library_posix.h why the libagsteam-unified.dylib wasn't being properly loaded. For this particular case, it looked like since that dylib was Intel/x86-only, that was causing the dlopen command to freak out and fail on the Apple Silicon Mac.

Command line tools like file or lipo can be used to check which architectures an executable or library supports. Examples of using the two utilities:

% file libagsteam-old.dylib
libagsteam-old.dylib: Mach-O 64-bit dynamically linked shared library x86_64

% lipo -archs libagsteam-old.dylib
x86_64

% file libagsteam-unified.dylib
libagsteam-unified.dylib: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [arm64:Mach-O 64-bit dynamically linked shared library arm64]
libagsteam-unified.dylib (for architecture x86_64):	Mach-O 64-bit dynamically linked shared library x86_64
libagsteam-unified.dylib (for architecture arm64):	Mach-O 64-bit dynamically linked shared library arm64

% lipo -archs libagsteam-unified.dylib
x86_64 arm64

In trying to figure out how AGS communicated with the various libraries, I used otool to inspect the executable and libraries to see how things were connected. Some apps did link the game's executable (Contents/MacOS/AGS) to libsteam_api.dylib, but in most cases it did not, which made things more confusing why there was an inconsistency in how these games were being constructed.

In this example, otool is used to inspect the libagsteam-unified.dylib library, which reveals that the Intel and Apple Silicon architectures are linked to Steam's libsteam_api.dylib library. Other tools like MacDependency and Apparency are also useful in inspecting dependencies, code signing, and entitlements of an applications.

% otool -L libagsteam-unified.dylib

libagsteam-unified.dylib (architecture x86_64):
	./Solutions/build/osx/libagsteam.dylib (compatibility version 0.0.0, current version 0.0.0)
	@loader_path/libsteam_api.dylib (compatibility version 1.0.0, current version 1.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1600.157.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.61.1)
libagsteam-unified.dylib (architecture arm64):
	./Solutions/build/osx/libagsteam.dylib (compatibility version 0.0.0, current version 0.0.0)
	@loader_path/libsteam_api.dylib (compatibility version 1.0.0, current version 1.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1600.157.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.61.1)

Issues, Errors, and Crashes

I encountered three primary issues when trying to get Steam achievements to work on the Mac with various testing attempts. A quick summary of the issues, followed by more in-depth explanations.

  1. The Old: The original third-party library libagsteam-unified.dylib was relatively old, dating back to ~2018, two years before Apple Silicon processors were available. I was able to get the achievements to run on Intel-based Macs, but they did not work on Apple Silicon Macs. There was a warning in the console about how there was an incompatible architecture (have 'x86_64', need 'arm64')
  2. Old and New: The first version of libagsteam.dylib I created was only built for Apple Silicon, so I combined it with the original Intel version using lipo to create a makeshift Universal Binary. This caused a crash on Intel because the old Intel libagsteam.dylib was linked to an older version of libsteam_api.dylib, and so it did not work properly with the updated version of Steam's library.
  3. The New: I then created a Universal binary build (built under macOS Ventura 13.6.7), but it had an issue when running on Intel and complained that the library was too new. I then had to rebuild under macOS Big Sur 11.7.

The Old

After I saw achievements work on Intel Macs, I needed to investigate why things were not working properly on Apple Silicon Macs. I launched the app from the Terminal (./MyGame.app/Contents/MacOS/AGS) so I could inspect any errors or warnings coming from AGS. When using the original libagsteam.dylib in a game, I received this error when trying to launch the game on Apple Silicon:

dlopen error: dlopen(/path/to/MyGame.app/Contents/Resources/libagsteam-unified.dylib, 0x0001): tried: '/path/to/MyGame.app/Contents/Resources/libagsteam-unified.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), '/System/Volumes/Preboot/Cryptexes/OS/path/to/MyGame.app/Contents/Resources/libagsteam-unified.dylib' (no such file), '/path/to/MyGame.app/Contents/Resources/libagsteam-unified.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64'))

Fortunately, this error was not overly cryptic and made sense for what needed to be done to get the library to work. Since the rest of the application was built as a Universal Binary (including the newer libsteam_api.dylib), I suspect that Rosetta 2 would not try and translate the single library, instead it threw an error, and silently failed. This spurred the necessity to rebuild the libagsteam-unified.dylib.

Old and New

When I first rebuilt the libagsteam library, I did not include the -arch x86_64 option during compilation, so the binary was built only for Apple Silicon. I then decided to use lipo to combine this new version with the original library, so it would create a fat binary with both architecture types.

To create a fat binary with lipo follow this pattern:

lipo -create -output universal_app x86_app arm_app

My example to combine the new and old libagsteam libraries:

lipo -create -output libagsteam-unified.dylib libagsteam-x86.dylib libagsteam-arm64.dylib

libagsteam-x86.dylib is the original libagsteam-unified.dylib I've found in other AGS games, which I simply renamed for this process to avoid confusion. libagsteam-arm64.dylib is the Apple Silicon version of the library I compiled. libagsteam-unified.dylib is the resultant file after combining the two libraries.

Note: If one really wants to do so, they can include other architecture types, such as PowerPC (PPC) builds, but there are very few Mac apps out there which support PowerPC, Intel, and Apple Silicon all in a single app.

With this build, I placed the libsteam_api.dylib, libagsteam-unified.dylib, and steam_appid.txt files into the game's Contents/Resources folder, launched the Steam client, and tested some achievements on an Apple Silicon Mac. With great joy, I finally saw achievements getting triggered! But to do a full regression, I also needed to test on an Intel Mac. The Intel version of libagsteam-unified.dylib worked before, so everything should work, right?

Right?!

One heartbreaking crash later when testing on an Intel iMac...

The error I encountered when launching the game from the Terminal:


# libname 'agsteam-unified' | libfile 'libagsteam-unified.dylib'
# Try library path: libagsteam-unified.dylib
# Plugin 'agsteam-unified' loaded from 'libagsteam-unified.dylib', resolving imports...
# dyld: lazy symbol binding failed: Symbol not found: _SteamAPI_Init
#   Referenced from: libagsteam-unified.dylib
#   Expected in: libsteam_api.dylib
# 
# dyld: Symbol not found: _SteamAPI_Init
#   Referenced from: libagsteam-unified.dylib
#   Expected in: libsteam_api.dylib
# 
# zsh: abort      ./MyGame.app/Contents/MacOS/AGS

And the crash log when trying to run the app after it was code signed on an M1 MBP:

# 
# Process:               AGS [1126]
# Path:                  /Users/USER/*/MyGame.app/Contents/MacOS/AGS
# Identifier:            com.companyname.mygame
# Version:               1.0 (1.0)
# Code Type:             X86-64 (Native)
# Parent Process:        ??? [1]
# Responsible:           AGS [1126]
# User ID:               501
# 
# Date/Time:             2024-09-14 18:50:56.150 -0600
# OS Version:            macOS 11.7.10 (20G1427)
# Report Version:        12
# Anonymous UUID:        E51BB38A-D649-7541-7AC8-154D8B33BA72
# 
# 
# Time Awake Since Boot: 650 seconds
# 
# System Integrity Protection: enabled
# 
# Crashed Thread:        0  Dispatch queue: com.apple.main-thread
# 
# Exception Type:        EXC_CRASH (SIGABRT)
# Exception Codes:       0x0000000000000000, 0x0000000000000000
# Exception Note:        EXC_CORPSE_NOTIFY
# 
# Termination Reason:    DYLD, [0x4] Symbol missing
# 
# Dyld Error Message:
#   Symbol not found: _SteamAPI_Init
#   Referenced from: libagsteam-unified.dylib
#   Expected in: libsteam_api.dylib
# 
# Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
# 0   dyld                          	0x000000010afa9e0a __abort_with_payload + 10
# 1   dyld                          	0x000000010afd2bb1 abort_with_payload_wrapper_internal + 80
# 2   dyld                          	0x000000010afd2be3 abort_with_payload + 9
# 3   dyld                          	0x000000010af52412 dyld::halt(char const*) + 672
# 4   dyld                          	0x000000010af5259c dyld::fastBindLazySymbol(ImageLoader**, unsigned long) + 167
# 5   libdyld.dylib                 	0x00007fff20745ce8 _dyld_fast_stub_entry(void*, long) + 65
# 6   libdyld.dylib                 	0x00007fff20745c26 dyld_stub_binder + 282
# 7   ???                           	0x00000001084950f0 0 + 4433989872
# 8   libagsteam-unified.dylib      	0x0000000108490a0c AGS_EngineStartup + 44
# 9   com.companyname.mygame            0x0000000101e5171f AGS::Engine::InitGameState(AGS::Common::LoadedGameEntities const&, GameDataVersion) + 15279
# 10  com.companyname.mygame            0x0000000101e84d04 load_game_file() + 3748
# 11  com.companyname.mygame            0x0000000101d9e942 initialize_engine(std::__1::map, std::__1::allocator > >, std::__1::less, std::__1::allocator, std::__1::allocator > > > > > const&) + 14690
# 12  com.companyname.mygame            0x0000000101da7cd5 ags_entry_point(int, char**) + 8949
# 13  libdyld.dylib                 	0x00007fff20746f3d start + 1
# 

I was hoping to avoid a little extra work by not having to create the entire Universal Binary version of libagsteam-unified.dylib, but that obviously wouldn't work. That would have been too easy, of course. I figured I would have to create a UB build, which was relatively simple by just adding the -arch x86_64 -arch arm64 options when compiling the library.

When trying to determine how these various libraries were linked together, I used a variety of tools like MacDependency or otool to check their dependencies. Using otool on my universal build of libagsteam-unified.dylib revealed details for both the x86 and arm64 architectures which were linked to the libsteam_api.dylib.

Mulling over this issue further, I surmised that the crash might be happening because I noticed that the libsteam_api.dylib I originally had in the game was just the x86 version, not the Universal Binary version. But I updated that library to the newer version, but when I created the libagsteam-unified.dylib, I integrated the old Intel version of the libagsteam-unified.dylib, and it may not be able to find something appropriately because it was not built and linked against the newer (version 1.59) Steam library which was available in the game, so something was not compatible.

This was making progress, but it obviously wasn't the complete working solution, so I would need to create a Universal Binary build that linked against the new Steam library.

The New

After creating a Universal Binary version of libagsteam-unified.dylib on my newer Mac, the achievements still worked on the M1 MacBook Pro, and the game launched on my Intel iMac, but achievements were still not launching on Intel. A positive step forward that it was no longer causing a crash, but the lack of achievements was annoying. The errors I saw when I launched the game again from the Terminal:

# 
# libname 'agsteam-unified' | libfile 'libagsteam-unified.dylib'
# Try library path: libagsteam-unified.dylib
# dlopen error: dlopen(libagsteam-unified.dylib, 1): Symbol not found: __ZNKSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEE3strEv
#   Referenced from: libagsteam-unified.dylib (which was built for Mac OS X 13.0)
#   Expected in: /usr/lib/libc++.1.dylib
# 
# Try library path: ./libagsteam-unified.dylib
# dlopen error: dlopen(./libagsteam-unified.dylib, 1): Symbol not found: __ZNKSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEE3strEv
#   Referenced from: ./libagsteam-unified.dylib (which was built for Mac OS X 13.0)
#   Expected in: /usr/lib/libc++.1.dylib
# 
# Try library path: /path/to/MyGame.app/Contents/Resources/libagsteam-unified.dylib
# dlopen error: dlopen(/path/to/MyGame.app/Contents/Resources/libagsteam-unified.dylib, 1): Symbol not found: __ZNKSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEE3strEv
#   Referenced from: /path/to/MyGame.app/Contents/Resources/libagsteam-unified.dylib (which was built for Mac OS X 13.0)
#   Expected in: /usr/lib/libc++.1.dylib
# 
# Plugin 'agsteam-unified' could not be loaded (expected 'libagsteam-unified.dylib'), trying built-in plugins...

The line which was built for Mac OS X 13.0 was the key to solving this issue. I initially built the library on macOS Ventura 13.6.7, but apparently that was too new for running on an older system (in this case, macOS Big Sur 11.7). The solution to this was to just rebuild the library under macOS Big Sur 11.7 on my Intel iMac. I tested this on as far back as macOS 10.14 Mojave, in addition to newer versions of macOS on Apple Silicon, and it seemed to work just fine then. Amazingly enough, I didn't have to alter my build script to get it to work on the slightly older Intel Mac.

This is the solution which finally worked for both Apple Silicon and Intel Macs. You can download the libraries here or download it from my branch of the agsteam project.

Tools

I used a number of tools to get everything working, especially with inspecting the dynamic libraries and determining how all of the disparate parts worked together.

Resources

All of the research for this post resulted in a lot references. Happy reading!

Implementing Steam Achievements for Mac AGS Games

28th September 2024 | Games

For the past several years I have assisted in porting several games created with Adventure Game Studio (AGS) over to the Mac. I have learned quite a bit in the process in how to port the games and how to build new versions of the AGS skeleton app, but one thing which has proved elusive is how to get Steam achievements to work. There have not been any clear cut instructions on how to set up an AGS game for the Mac to integrate with Steam functionality. Trying to get this to work seems like it requires some form of secret computer ritual involving a rubber chicken (with or without a pulley in the middle). There are those who have gotten it to work in the past, but the secrets seem to be relegated to tribal knowledge. Paul Korman (developer of the game The Phantom Fellows) has written up some extensive notes on the process he has used to get Steam achievements to work on the Mac. I followed these steps, but still encountered issues in getting the achievements to work in my own testing.

Fortunately, after a bunch of trial and error and delving down numerous rabbit holes, I have come up with a solution on getting Steam achievements to work on both older Intel Macs and newer ones running on Apple Silicon. This post intends to set the guidelines on how to set up an AGS game (which uses the ags2client plug-in) and integrate with Steam achievements. While this article mostly focuses on working with AGS and Steam, certain parts can also be applicable in working with other game engines or gaming platforms like GoG.

Setup and Configuration

Since I first wrote about porting AGS games to the Mac in 2020, Apple has made another seismic shift with their technology by abandoning Intel processors in favor of their home grown Apple Silicon. Last year I developed a Universal Binary version of the AGS app shell; one step closer to natively supporting the newer processors. However, this was only one major piece of the puzzle, since to truly support the newer platform, any additional components and libraries also need to be rebuilt. There are a number of older AGS games which work with Steam, but I have not seen any which have been entirely constructed as Universal Binaries to run natively on both Intel and Apple Silicon. I surmise that the older games were built for Intel, so when they launch on newer Macs, Rosetta 2 translates the Intel instructions to Apple Silicon. However, if the game shell is a Universal Binary, but third party libraries are still Intel-only, that can cause an issue with trying to run the older software.

The steps:

  1. Get a copy of the AGS skeleton app. This is built against AGS 3.6.1 Patch 3 and is a Universal Binary. As of this writing in 2024, 3.6.1 is the current version, but in the future if you want to build a newer version, follow my instructions on Building a Universal Binary Version of Adventure Game Studio for Mac.
  2. Set up the app, configure the Info.plist, and copy over the necessary game files. More complete instructions on this step are at Porting Adventure Game Studio Games to the Mac.
  3. Download the libagsteam-unified-universal-binary.zip file and unzip it. This archive contains two files, libagsteam-unified.dylib and libsteam_api.dylib. The first file is a rebuilt Universal Binary of the agsteam library which is linked to libsteam_api.dylib (version 1.59 of Steam's dynamic library).
  4. Copy libagsteam-unified.dylib and libsteam_api.dylib into the Contents/Resources folder of the app bundle. Make sure that the steam_appid.txt file is also in the Contents/Resources folder alongside the two dylib files.

Entitlements

In Apple's efforts to increase the security of the software running on its platforms, they have layered on numerous measures like Gatekeeper, code signing, notarization, and entitlements over the years.

When trying to enable Steam achievements with a Mac game, it is important to set the proper entitlements so the third party Steam library can communicate with Steam. This is not something limited to games developed with AGS, but with any game that integrates with Steam. This is often the tricky, missing piece that can make getting Steam achievements to trigger so challenging with Mac ports.

When code signing the app bundle and its components, you will need to add the --entitlements flag and the path to your entitlements file. In this example, there are only two necessary keys in the file: com.apple.security.cs.disable-library-validation and com.apple.security.cs.allow-dyld-environment-variables, and both have their values set to true. There are numerous other options which can be added, but these are the only two we need for the purpose of allowing the third party libraries to function properly.

An example entitlements.plist file:

Once your game has been properly built and code signed (more on this below), you can verify the entitlements from the command line:

codesign -d --entitlements - --xml MyGame.app

The returned result should show the path to the app bundle's executable, and if there is an entitlement, it will be displayed in a blob of XML.

Executable=/Users/johndoe/Library/Application Support/Steam/SteamApps/common/MyGame/MyGame.app/Contents/MacOS/AGS
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.security.cs.disable-library-validation</key><true/><key>com.apple.security.cs.allow-dyld-environment-variables</key><true/></dict></plist>

If you prefer a more visual method of checking the code signature and entitlements of an app bundle, use the excellent program Apparency.

Building

Now for the meat and potatoes to put everything together before testing: code signing and notarization! A lot of these steps were covered in my post Porting Adventure Game Studio Games to the Mac, but these instructions are also more up-to-date and concise.

In the following script, make the necessary substitutions for your game, then run the script in the same folder as your game. I tend to just have the script do the code signing, then I perform the remainder of the steps (archiving the app bundle and so on) from the Terminal, but if this script works for your purposes, go for it!


APP_NAME=MyGame.app # Set to the name of the app bundle
APP_DIR=MyGame.app # Same here
RESOURCES_DIR="${APP_DIR}/Contents/Resources"
MACOS_DIR="${APP_DIR}/Contents/MacOS"
ENTITLEMENTS_PLIST=~/path/to/entitlements.plist # Need to set to the path of the entitlements.plist
CERTIFICATE="Developer ID Application: John Doe (12AB3456CD)" # Set to details of the actual developer certificate
EXECUTABLE_NAME="AGS"
DEVELOPER_EMAIL="john.doe@example.com"
APP_SPECIFIC_PASSWORD="abcd-efgh-ijkl-mnop" # Need to generate an app-specific password for your game
TEAM_ID="12AB45543C" # only required if your developer account is connected to more than one team

# Clean out any troublesome forks, dot files, etc.
echo "Cleaning out detritus..."
xattr -cr "$APP_NAME"

# Loop through and code sign any dynamic libraries
echo "Code signing dylibs..."
libfile=`find "$APP_NAME" -name '*.dylib'`
while read -r libname; do
	echo "Sign:" $libname
	codesign --deep --force --verify -vvvv --timestamp --options runtime --entitlements "$ENTITLEMENTS_PLIST" --sign "$CERTIFICATE" "$libname"
done <<< "$libfile"

# Code sign the entire application
codesign -s "$CERTIFICATE" --timestamp --options=runtime -f --entitlements "$ENTITLEMENTS_PLIST" --deep --force "$APP_NAME"

# Verify the code signature
codesign --verify --deep --strict --verbose=4 "$APP_NAME"

# Archive the app bundle
ditto -c -k --sequesterRsrc --keepParent *app MG.zip

# Notarize
xcrun notarytool submit ./MG.zip --wait --apple-id "$DEVELOPER_EMAIL" --password "$APP_SPECIFIC_PASSWORD" --team-id "$TEAM_ID" 

# Staple
xcrun stapler staple -v "$APP_NAME"

# Validate
spctl --verbose=4 --assess --type execute "$APP_NAME"

A properly code signed AGS game with the necessary Steam-related libraries should look similar to this:

File structure of an AGS game for Mac

Side note: When you code sign an app and its components, it will remove any previously existing code signing. However, if you do want to remove the code signing, it seems to be possible with the command:

codesign --remove-signature MyGame.app

Testing

At this point, it is assumed that you have already set up your project in Steam, added the necessary Steam achievements, and perhaps even have a test build available (which are topics outside the scope of this post).

When testing the achievements, the critical step is to have the Steam client open!. However, the game does not need to be in the location where Steam downloads games (~/Library/Application Support/Steam/SteamApps/common/) during this testing. Launch your game. You should see an initial overlay which says "Access the Steam Community while playing" if you have the Steam client running. Perform an action in the game to trigger the achievement. If all goes well, you should hear the "blip" chime and an overlay to indicate which achievement was earned (although I don't always see an overlay appear). If that happened, then everything is working and you are good to go!

Except...

Things are rarely that easy, are they? In my own tests, I did a lot of testing and ended up with a lot of frustrating nothing. There were times where I ended up launching the game from the Mac Terminal via a command like ./MyGame.app/Contents/MacOS/AGS and then inspected the output which could give clues to why Steam achievements were not working. In several cases I did see various warnings where the necessary libraries could not get loaded properly, either due to the wrong computer architecture (Intel vs. Apple Silicon), or the linked libraries were not compatible with each other, or a library was built with too new of a system. Further details of these types of issues will be in a follow-up post.

If you are doing some testing of your game and need to reset your achievements, Steam has some nice functionality built into its client with a console. To open up the console in the Steam client, go to your web browser and type steam://open/console or from the Terminal, type: open steam://open/console

A couple of useful console commands are available to reset achievements. You will need the Application ID of the game, which can be found in the steam_appid.txt file. If that text file is not in the app bundle, you can go to the Steam Store and search for a game. The number in the page's URL should be the application ID (e.g. 2215540).

To reset all achievements:

reset_all_stats <application ID>

Example:

reset_all_stats 2215540

To reset a specific achievement:

achievement_clear <application ID> <achievement name>

Example:

achievement_clear 2215540 OnlyJustBegun

To find the name of achievements in a game, go to SteamDB, search for the game, then click on Achievements. This page shows the API and display names of the game's achievements. The API name serves as the achievement name.

Steam console

You might need to restart the Steam client to see the updated achievements list.

Thanks

A lot of work has gone into porting games over the years. A number of thanks go out to:

Resources

Cleaning Up Downloads with Folder Actions

4th May 2024 | Programming

I've been brushing up on my web development skills with the Web Development Bootcamp course through Udemy. The course involves downloading a lot of projects, bundled as zip files. Very quickly I tired of moving the zip file to my projects folder and then unzipping the file. It's only a couple of steps, but it becomes very repetitious and monotonous, an ideal task for automation.

Screenshot of Automator with a Folder Action and script

The initial setup steps:

The shell script:

Note: This script makes use of the zsh shell, but bash also seems to work, as well. This script was tested on macOS Ventura 13.6.6, but it should hopefully work on an array of other versions (older and newer) of macOS.

The Folder Action passes the downloaded file as an argument (and not via stdin, otherwise this script won't work), and then the script iterates over the supplied argument(s) $@.

If you open up the Get Info panel on a file, the More Info: section may contain information detailing where a file originated. To check where a file a file might have been downloaded from, the mdls command is used to check for the file's metadata attributes and the kMDItemWhereFroms option returns an array of possible values. One example of inspecting one of these zip files:

kMDItemWhereFroms = (
     "https://att-c.udemycdn.com/2023-04-13_10-05-58-56f38da77018db82042a9a9ae9fd7b77/original.zip?response-content-disposition=attachment%3B+filename%3D9.0%2BDisplay%2BFlex.zip&Expires=1714526174&Signature=3-tMr~b-Oczk0h27K4acbpMcYOgEBqzHmt2Q0XrxyhAzkEkVxplfY4Z77-xSEBJCfY8yPWH11TVgxm-cCum0LB8AP-KGA6Oy0mJArKZeF20tIDs1p3pRfd~b6p0zi5CktQp859yn9ui-jWjXUf9VPc1p67Ecqd1Dj5ow-n6Wp-UdV5C5Tz0JHdcoRKM1JeF7-JLkV79nx1YfF~K2pFfv3YOm13oMibu1q2UenE8WU5FIRbEbiIu0HEEMOXv9q4lIuPgH2ptgpB1fO6RYXXWpWhLnkQoE-cA0gWgXgG7S5rfWpPs4hTUqQbTbBTgTocaafqhOLDsD6u7~UMkgrGuuSw__&Key-Pair-Id=K3MG148K9RIRF4",
     "https://gale.udemy.com/"
)

The script just takes the output from the mdls call as a string and then checks if the gale.udemy.com URL is found, which indicates that the file originated from the Udemy website. All other files will be ignored.

The final steps involve unzipping the file into the destination folder and finally moving the original zip file (optional, but good as a cleanup process).

Challenges

This script is fairly short, but it required a number of iterations to get everything to work properly. I started by writing a shell script and running it against a downloaded file, but it doesn't work exactly the same as when it is treated as a script within Automator.

The first challenge was to figure out how to extract the Where From data via a script. Fortunately, mdls solved the initial problem. I initially thought I would have to parse out the data which returns as a CFArray object, but is is just a string in this context. One thing which didn't work initially was when I was creating the comparison string, I simply used "https://gale.udemy.com". This didn't work because the asterisks are required on both sides to act as wild cards. The fix is to compare against *"https://gale.udemy.com"* .

When I initialized the destination directory variable dst_dir, I initially surrounded the path ("~/Sites/the-complete-web-development-bootcamp/") with quotes as a safety measure in case there were ever spaces. However this caused a different issue in properly resolving the path, so I was getting No such file or directory errors when trying to work with that directory. After some research I discovered that using quotes caused the path with the ~ to not resolve properly.

In my initial script, I first moved the zip file to the destination directory, then changed to that new directory, and finally would unzip it. This worked properly when I ran the script directly, but when it was run within Automator, nothing seemed to work. Trying to debug such issues can be perplexing so I ended up running the unzip command and then piped any errors to a text file (e.g. unzip TheFile.zip &> errors.txt). After doing this, I was able to get a useful error:

unzip:  cannot find or open /Users/chad/Downloads/TheFile.zip, /Users/chad/Downloads/TheFile.zip.zip or /Users/chad/Downloads/TheFile.zip.ZIP.

When I was running the shell script directly from the Downloads folder, I would make a call like this: ./udemy.sh TheFile.zip . In this example, the name of the file being passed in as an argument would be just TheFile.zip, but when it is passed in through the Folder Action, it is the entire file path, which tripped up my initial approach.

My workaround for this issue was to just unzip the archive from the Downloads folder, but set the -d flag and use the dst_dir as the destination path. Once that is completed, I then moved the zip file to the destination folder.

Bonus Challenge

Another thing I tried, which was eventually abandoned, was to create a name for an enclosing folder where to unzip the contents of the archive. The enclosing folder would share the same name as the zip file, so I needed a way to take the file name and just strip off the file extension. This introduced me to some more UNIX commands, basename and its associated dirname. After doing more research, I tried two methods to isolate just the name of the file. One method worked for me, while the other did not.

# Get the file's name without the file suffix
filename="$f"
enclosing_folder=$(basename $filename) # This didn't work for me
enclosing_folder="${filename%.*}" # This does work to get just the file name

Conclusion

For what I had originally expected would be a somewhat simple task of automation to relieve a minor burden, resulted in becoming an interesting and frustrating experience as I fought against the nit-picky syntax of shell scripting and trying to integrate it with a Folder Action. The end result is a minimal amount of code, but not before going through several rounds of experimentation to carve the code down to its functional essence.

Custom Rounded Corners in Swift and SwiftUI

2nd March 2024 | Programming

Setting uniform rounded corners on a view in Swift is a relatively trivial task by setting the cornerRadius property on the view's layer. But what if not all of the corners need to be rounded? Or different radii are needed for each corner? This post goes over several different methods to approach these issues in Swift and SwiftUI.

Swift Examples

Example 1

This first example shows how to set the top two corners of the image to have a corner radius of 16 by setting the layer's maskedCorners property.

let cornerRadius:CGFloat = 16.0
stockImageView.layer.cornerRadius = cornerRadius
stockImageView.clipsToBounds = true
stockImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]

Example 2

This next example makes use of a a UIBezierPath and a masking layer to round off the two corners on the right. It's not as straightforward as the previous example, but its approach will prove useful for the next example.

let pathWithRadius = UIBezierPath(roundedRect: stockImageView.bounds, byRoundingCorners: [.topRight, .bottomRight], cornerRadii: CGSizeMake(16.0, 16.0))
let maskLayer = CAShapeLayer()
maskLayer.path = pathWithRadius.cgPath
stockImageView.layer.mask = maskLayer

Example 3

This is the most complex example, but the most flexible. As demonstrated in Example 2, a UIBezierPath and CAShapeLayer are used to create a mask to create an image with custom rounded corners. Whereas Example 2 used a rounded rectangle to predefine the shape of the path, this example creates its own shape using a combination of lines and arcs, which results in a rectangular-like shape with different sized rounded top corners. This example can be extended with a variety of shapes, such as a star.

let imageHeight = stockImageView.bounds.height
let imageWidth = stockImageView.bounds.width
let topLeftRadius = imageHeight / 2.0
let topRightRadius = 16.0
let customBezierMask = UIBezierPath()

// Starting point
customBezierMask.move(to: CGPoint(x: topLeftRadius, y: 0.0))
// Top line
customBezierMask.addLine(to: CGPoint(x: imageWidth-topRightRadius, y: 0.0))
// Top right corner with a radius of 16.0
customBezierMask.addArc(withCenter: CGPoint(x: imageWidth-topRightRadius, y: topRightRadius), radius: topRightRadius, startAngle: 3 * .pi/2, endAngle: 0, clockwise: true)
// Right line
customBezierMask.addLine(to: CGPoint(x: imageWidth, y: imageHeight))
// Bottom line
customBezierMask.addLine(to: CGPoint(x: 0.0, y: imageHeight))
// Left line
customBezierMask.addLine(to: CGPoint(x: 0.0, y: imageHeight - topLeftRadius))
// Top left corner with a radius that is half the height of the image
customBezierMask.addArc(withCenter: CGPoint(x: topLeftRadius, y: imageHeight - topLeftRadius), radius: topLeftRadius, startAngle: .pi, endAngle: 3 * .pi/2, clockwise: true)

let maskLayer = CAShapeLayer()
maskLayer.path = customBezierMask.cgPath
stockImageView.layer.mask = maskLayer

SwiftUI Examples

Example 1

This first SwiftUI example is modeled after Example 3 with the different rounded corners. Some of the principles are similar by using a custom Path, but there are also notable differences in the implementations between Swift and SwiftUI.

Example 2

An even easier approach for this particular technique with different rounded corners is to use the .clipShape() method with a .rect that has custom radii via the RectangleCornerRadii struct. This is a far simpler method to produce the same results.

let rectCornerRadii = RectangleCornerRadii(topLeading:119, bottomLeading: 0, bottomTrailing: 0, topTrailing: 16)
            
Image("BRF002-small")
	.resizable()
	.scaledToFill()
	.frame(width: 320, height: 238)
	.clipShape(.rect(cornerRadii: rectCornerRadii))

Using a custom path can be useful for creating unique shapes (such as a star), but for this demonstration which focused on providing rounded corners to an image, there are alternate solutions which don't require nearly as much code. In particular, this final SwiftUI example leverages the power of new functionality provided in this newer UI framework.

References

Edenwaith 2023 In Review

31st January 2024 | Edenwaith

You know the drill: Look back briefly at the past with disdain and/or longing, and then look forward with hope and anticipation.

Last Year's Plans

Plans for 2024

OneBitOrderedDither : A 1-Bit Ordered Dither Plug-in for Acorn

14th October 2023 | Programming

A black and white dithered picture of Maroon Bells

Continuing my research and implementation of dithering techniques, I have turned my attention to 1-bit ordered dithering methods, useful for creating graphics for the Playdate, a quirky, monochrome, handheld console (with a crank).

There are many dithering techniques available, but they are divided into two principle sections: ordered and error diffusion. This article will focus on the former.

While error diffusion methods (especially the Floyd-Steinberg algorithm) are popular ways to create dithering effects, ordered dithering has the advantage for speed and also provides for a distinctive appearance, such as the gradient sky in the EGA version of Secret of Monkey Island.

Screenshot from Monkey Island with some beautiful gradient dithering in the sky

In certain cases, ordered dithering is necessary for its speed or to be applied as a renderer, such as in The Return of the Obra Dinn.

This post will cover two programs which built up to creating a plug-in for Acorn to generate 1-bit images using an ordered dither algorithm.

Ordered Dithering Matrices - ordered_dithering.c

To implement ordered dithering, a matrix pattern is generated and is used as a tiled mask over the original image. The matrix can be a variety of sizes, such as 2x2 or 16x16. All of the examples I've seen have identical dimensions, so a 2x3 matrix is not used. The values assigned in the matrix are used to calculate the threshold value, whether a pixel should be set to white or black.

Matrix patterns

There does not seem to be one established way of how the matrix is populated. Even for a simple 2x2 matrix, I've noticed numerous examples in how the layout pattern is established, such as:

[ 0 3 2 1 ] vs [ 0 2 3 1 ] vs [ 3 1 0 2 ]

I tested these different patterns on the same image, and it didn't appear that there was any noticeable difference in the generated 1-bit image.

The algorithm in ordered_dithering.c populates a matrix with the order, but it does not necessarily determine the value of the threshold. The pixel color value generally has a range from 0.0 to 1.0 or from 0 to 255, and this determines how the threshold value is compared. This is dependent on the system and the programmer on how they want to calculate and compare the values. When one retrieves the color component from a pixel using Objective-C, each color component (red, green, blue) has a float value from 0.0 - 1.0, but this could be easily adjusted to be between 0 - 255 by multiplying the component value by the integer 255.

If one wants to compare integer values, then a possible 2x2 matrix could be:

[ 51 206 153 102 ]

This comes out very close to the [ 0.2 0.8 0.6 0.4 ] pattern, once these values are divided by 255, which would be the appropriate 2x2 matrix when comparing values between 0.0 - 1.0.

However, the values do not necessarily need to be evenly spaced. One could set custom threshold values to populate the matrix which will adjust the calculated value of each pixel. This might be necessary if evenly spaced values do not produce the desired dithered effect. Such an example with a 2x2 matrix might be:

[ 0.3 0.45 0.55 0.7 ]

The values are not evenly spaced as in the prior example, but it might prove necessary if the contrast of an image is too light or dark, so an adjusted set of matrix values is needed.

This small program will generate an ordered matrix, and some of these matrices are used in the following program ordered_dither.m. The original code, by Stephen Hawley, originates from the book Graphics Gems and has been updated for a more modern version of C.

While this program generates a suitable ordered matrix, other examples and patterns are available, such as a 3x3 matrix or a magic square where each row and column adds up to the same value.

The Algorithm - ordered_dither.m

ordered_dither.m is a small Objective-C program which is the basis for the dithering algorithm used in the plug-in discussed in the next section. A number of the example matrices displayed in this program come from the ordered_dithering.c program, the Dither: Ordered and Floyd-Steinberg, Monochrome & Colored web page, and several text books listed in the References section.

This program iterates over each pixel of the given image, converts the color to a grayscale version, and then compares the pixel value to the matrix. When a larger matrix is used, there is more variability to the ordered pattern, which reduces the noticeable tiling effect. The 16x16 matrix seems optimal, especially since it contains 256 values, which lines up with the 0 - 255 range that most color components encompass.

OneBitOrderedDither - Acorn Plug-in

A black and white dithered picture of Moraine Lake

All of the research and experiments were built up to create another plug-in for Acorn (the first plug-in being AGIfier, which created low resolution images with an EGA color palette). This new plug-in, OneBitOrderedDither, generates a black and white image using a 16x16 ordered dither matrix. Much of the logic for this plug-in is based from the ordered_dither.m program.

I first built AGIfier several years ago using Xcode 11.3.1 and macOS 10.14. I started creating OneBitOrderedDither using Xcode 14.2 on macOS 13.5.2, but I noticed an issue when I created a new Bundle — the new project was missing the Info.plist file. I went back to my older machine and started up the new project under Xcode 11.3.1, which generated the expected Info.plist, then brought the project over to my newer computer.

Once I was finished coding the plug-in, I moved the bundle over to the ~/Library/Application Support/Acorn/Plug-Ins folder, but the plug-in was not displaying in Acorn's menus. I checked the Console log and came across some hints that the bundle was not building with an appropriate Apple Silicon architecture, in addition to an Intel version. I had to check the project settings and ensured that it was not supposed to build for only the active architecture (ONLY_ACTIVE_ARCH = NO;), and this seemed to resolve the problem.

Download and Install:

To Build and Install:

If you prefer to build from the source code and install the plug-in, follow these steps:

To run:

References

Steel-Cut Oatmeal with Apples Recipe

14th October 2023 | Recipe

Photo shoot of steel-cut oatmeal with apples.  Recipe by Chad Armstrong.

My staple breakfast. The apples provide a nice texture and sweetness so no extra sugar is necessary.

Ingredients

Directions

  1. Boil ⅔ cup of water in a small sauce pan. Add a dash of sea salt.
  2. While the water is heating, prepare the oats. Pour ⅛ cup of steel-cut oats into a ¼ measuring cup. Put a dash of ground cloves and nutmeg on the oats. Shake the cup to even the oats.
  3. Pour another 1/8 cup of oats into the cup. Then add a dash of ground cinnamon. Shake the cup to even the oats.
  4. Once the water is boiling, pour the oats into the saucepan. Stir, then cover with a lid.
  5. Lower the heat to low so the oats simmer.
  6. While the oats are cooking, dice 1/3 of a medium-sized Honeycrisp apple.
  7. Stir the oats occasionally. The oatmeal is done cooking when small bubbles start appearing, after ~10-15 minutes.
  8. Pour the oatmeal into a bowl.
  9. Add the sliced apples on the oatmeal.
  10. Add a dash of cinnamon to the apples.
  11. Drizzle 1 ounce of water onto the oatmeal and apples, then stir together.

Italian Herb Bread Recipe

6th October 2023 | Recipe

Photo shoot of tasty Italian herb bread.  Recipe by Chad Armstrong.

I've baked dozens of types of breads, and this one still remains my favorite. Even before you bake the bread, it will make your kitchen smell amazing. Share and enjoy.

Ingredients

Directions

  1. Mix yeast, warm water and sugar together in a large bowl.
  2. Set aside for five minutes, or until the mixture becomes foamy.
  3. Stir in olive oil, salt, herbs, garlic powder, onion powder, cheese and 3 cups flour into the yeast mixture. (Note: if you are using fresh herbs, instead of dried ones, double the amount from ½ to 1 tablespoon.)
  4. Gradually mix in the next 2-3 cups of flour. Dough will be stiff. If the dough is still wet and sticky, gradually add one tablespoon of flour to the mixture.
  5. Knead for 5 to 10 minutes, or until it is smooth and elastic.
  6. Place in an oiled bowl, turning to cover the sides with oil.
  7. Cover with a damp linen towel or greased plastic wrap.
  8. Let the dough rise for 1 hour or until dough has doubled.
  9. Punch down to release all the air.
  10. Shape into two loaves. I like to roll the dough out into a 12" x 6" rectangle and then roll them up to form the loaves.
  11. Place loaves on a pizza stone with parchment paper, a greased cookie sheet with cornmeal, a baguette pan, or into two 9x5 inch, greased pans.
  12. Allow to rise for ½ hour again, until doubled in a warm place.
  13. Score the loaves with a sharp knife or lame.
  14. Spray the oven and the loaves with water if you want a little crispier crust.
  15. Bake at 375°F (190°C) for 35 minutes.
  16. Remove loaves from the oven and let them cool on wire racks for at least 15 minutes (ideally an hour) before slicing.

Integrating AppleScript With a Mac Application

9th August 2023 | Programming

For the past several years I have been slowly undertaking the monolithic task of rewriting Permanent Eraser. However, such an endeavor has become quite the Sisyphean task as my efforts have come in random spurts between my various other projects. As an interim option, I looked at adding a new feature to the existing app, such as including AppleScript support (something I've been wanting to include for many, many years).

AppleScript Scriptable Resources

Here's a variety of links I went through on how to make a Mac app scriptable. The first three are the ones which I found the most useful and cut through a lot of the confusion of how to get things set up.

Acorn's "taunt"

When I was doing research about scripting Mac apps, I took a look at other older programs and their AppleScript dictionaries. One of the examples I came across was the stalwart image editor Acorn which has an amusing taunt command, which results in this fun little dialog to appear:

Screenshot of Acorn taunting me like a Frenchman from a Monty Python movie

Here's the AppleScript:

tell application "Acorn"
	taunt
end tell

Permanent Eraser and AppleScript

After some initial experimentation, I was able to finally get AppleScript to communicate with Permanent Eraser. That's the good news, a basic proof of concept showed that it can work. The bad news is that much like when I tried to implement NSServices in Permanent Eraser the poor design choices made back in 2004 prevent it from working well. To get either NSServices or AppleScript to work with Permanent Eraser will require rearchitecting the app, but doing so will move it much closer to what I've hoped to fulfill with Permanent Eraser. While making Permanent Eraser 2 scriptable is not feasible at the moment, it has been quite an interesting and rewarding trip down another rabbit hole.

The Japanese Version of Quest For Glory I (EGA)

7th May 2023 | Games

Over the past several years, I have been an ardent collector of boxed copies of Sierra computer games, particularly those from the Quest for Glory series. In the past year I finally managed to add an elusive gem to my QFG collection: クエスト フォー グローリィ, the Japanese version of Quest for Glory I (EGA).

I now have a physical copy, but the disks are 5.25" and are intended for a PC-98 computer. Even if this was intended for a standard IBM clone, I would not have a way to read the disks. I think the last time I used a computer with 5.25" drives was back in the late 90s, and even then, those were older computers running Windows 3.1. Fortunately, the internet is obliging in providing alternative methods in preserving old software, even if the original medium has become antiquated. The next trick was to figure out how to play this game on (slightly) more modern computers.

Emulating a PC-98 Computer

While I had not seen anyone play this version of Quest for Glory before, I had seen people stream the Japanese version of Police Quest II, so this indicated that it was possible to play PC-98 games on modern hardware, but this was an entirely new realm for me. My initial research pulled up a couple of articles and blog posts.

On my MacBook Pro, I first tried setting up np2sdl2 (version 0.86), one of the Neko Project II emulators, but it kept crashing when I launched it. I was a little too hopeful that things would "just work". I then started looking at the included file はじめにお読みください.txt, which is in Japanese, so it was difficult for me to parse out the instructions, but a couple of words were in English, such as BIOS.ROM, FONT.ROM, and https://www.libsdl.org/download-2.0.php. Going to the SDL link briefly showed a message that the webpage had been moved to GitHub, which at the time of this writing redirects to https://github.com/libsdl-org/SDL/releases/tag/release-2.26.2. This was one initial clue to why the app was crashing. I inspected the crash log, and it indicated that I needed to install the SDL2 library first. I then downloaded SDL2-2.26.2.dmg and installed those libraries. Now np2sdl2 no longer crashed. It started up, made a beep, checked its RAM...then showed a blank screen. I then placed the BIOS.ROM and FONT.ROM files in the same folder as the np2sdl2.app, but starting the app still resulted in a black screen. Any details to configure and troubleshoot are sparse, especially for the Mac version, which doesn't seem to be very well supported, but this is marginally better than Neko Project 21/W which doesn't even have a Mac version.

If I launch np2sdl2 from the command line (np2sdl2.app/Contents/MacOS/np2sdl2), it prints out twice "Device not found", but it does get to an actual screen, first asking how many devices, and a menu along the bottom. After setting the HDD to point to the disk image of the game, Quest for Glory I finally booted in Neko Project II.

After struggling with trying to get np2sdls2 to work, I tried another approach. The most straightforward solution I found was an old PowerPC application called "NP2 Carbon", even though the About Screen says "Neko Project II ver. 0.81a (Carbon)".

I tried this Carbon-based application on my PowerBook G4, and it was more promising than my first experiments with np2sdl2. Once I added FONT.ROM and BIOS.ROM into the same folder as the NP2 Carbon app, it booted up. This program uses a proper Mac menu bar, not some weird Windows 9X-looking interface inside of the window like np2sdl2 does. The audio was somewhat tinny on my PowerBook G4 (more on this in the Sounds section), but serviceable. The sound was somewhat off-putting, and I wasn't sure if I wanted to play through the entire game on the old Mac, but it's a nice option that PC-98 games can still be played on older Macs.

One shortcoming I noticed with both of these Neko Project II emulators is that neither one could go into Full Screen mode. On the PowerBook, the default window size wasn't too bad (since the screen resolution of the 2003 PowerBook was still years away from what Retina displays would provide), but the windowed screen on a MacBook Pro was not very enjoyable to squint at.

Another option I came across was to use DOSBox-X (not to be confused with the regular version of DOSBox), which does have PC-98 emulation. However, I encountered some snags here, as well, as the application could not find the dosbox.conf file. Unlike DOSBox, which has a preferences file in the standard ~/Library/Preferences folder on a Mac, DOSBox-X doesn't seem to have a corresponding preferences file. After reading up about this issue, it looks like the dosbox.conf file is instead saved in the home folder (not where it should be). A blank file was created, but that didn't help. I ended up copying over my "DOSBox 0.74 Preferences" file to dosbox.conf to my home folder. Perhaps copying the file https://github.com/joncampbell123/dosbox-x/blob/master/dosbox-x.reference.conf might be another option.

I then opened up the dosbox.conf file and changed the machine option to pc98. Restarting DOSBox-X then started up the PC-98 emulation. I then used the IMGMOUNT command to the mount the hard drive image (hdi) of QFG1.

IMGMOUNT C /Applications/DOSBox/dosbox/SIERRA/QGANTH/QG1J/QFG.hdi
c:
cd SIERRA
GLORY1.BAT

Note: To change to the new C: drive, since DOSBox-X is using a PC-98 style keyboard (I assume), not all of the keys are in the same place as on a US keyboard. The colon is the ' (apostrophe) key on my keyboard. This took a bunch of experimentation until I found the proper key. I address in the Issues section how I fixed a couple of keyboard issues, including when the CTRL key was not working for me.

Once I was able to switch over to the new mounted drive, I could see the standard game files. I started up the game and away I went.

It's been interesting trying to interact with this version since it is in Japanese (which I have pretty much no knowledge of). But using Google Translate, I was able to figure out a couple of commands in the game by phonetically typing in the words. Example phrases I figured out: look, run, sneak, rap door, climb, yes, hello, bye.

I get the sense that for non-English speakers, this was probably a similar experience by trying to learn English and when typing in commands, attempting to figure out the correct thing to type. (Consider that the Police Quest 1 phrase "administer field sobriety test" is challenging even for a native English speaker!) Fortunately, it is possible to switch the language input, or show a combination of both English and Japanese (CTRL+L).

What's New

I've lost count of how many times I've played this game since 1989, so it is always interesting to find something new or different. Besides the obvious language differences, I did encounter a couple of other things I hadn't discovered before.

Screenshots

There are a handful of cases where the text and graphics were updated for the Japanese market, primarily the signs in town. Changing the language settings back to English will switch the signs back to English.

As a side note, it was interesting to see that full screenshots taken using DOSBox-X on my 2021 MacBook Pro rendered at a large 3024x1964 pixels, whereas screenshots in standard DOSBox will return the native resolution of the game (generally 320x200 for Sierra games of the late 80s). Since I took a good number of screenshots for this post, I needed some ways to batch resize and shrink down the image sizes. Many of these screenshots were between 1 to 2 MB in size, which adds up quickly when displaying ~40 screenshots on a single webpage. Loading 50+ MB for a single blog post is superfluous, especially if one is trying to load over a cellular connection, where bandwidth is still valuable, especially in areas of poor or slow connections.

I started off by installing the command line utility pngcrush via Homebrew with the command: brew install pngcrush, then ran it against a test screenshot to see what type of gains could be made.

% pngcrush test.png test-crushed.png
  Recompressing IDAT chunks in test.png to test-crushed.png
   Total length of data found in critical chunks            =   1894274
   Best pngcrush method        =   6 (ws 15 fm 6 zl 9 zs 0) =    853831
CPU time decode 0.321226, encode 4.332043, other 0.009336, total 4.663681 sec
% ls -la test*png
-rw-r--r--  1 chada  staff   857474 Apr 30 21:18 test-crushed.png
-rw-r--r--@ 1 chada  staff  1898113 Feb  2 18:54 test.png

That managed to reduce the original 1.9MB file down to more than half at 857KB. A great start, but there's plenty of room for improvement. Many of the screenshots I took on my PowerBook were under 100KB, so I wanted to get the reduced screenshots closer to that size. I then used sips (scriptable image processing system) to resize the image closer to the original game resolution. Even though the EGA version of QFG1 had a width of 320 pixels, I went for 640, which works out well for these types of posts.

sips --resampleWidth 640 test.png --out test-resized.png

That shrunk the 1.9MB file down to 388KB. Making progress. Let's compress the new image with pngcrush.

% pngcrush test-resized.png test-resized-crushed.png
  Recompressing IDAT chunks in test-resized.png to test-resized-crushed.png
   Total length of data found in critical chunks            =    383535
   Best pngcrush method        =   7 (ws 15 fm 0 zl 9 zs 0) =    280199
CPU time decode 0.038348, encode 0.200314, other 0.002597, total 0.241483 sec

% ls -la test*.png
-rw-r--r--  1 chada  staff   857474 Apr 30 21:18 test-crushed.png
-rw-r--r--  1 chada  staff   284051 May  3 20:55 test-resized-crushed.png
-rw-r--r--  1 chada  staff   387543 May  3 20:54 test-resized.png
-rw-r--r--@ 1 chada  staff  1898113 Feb  2 18:54 test.png

After resizing and then using pngcrush, I managed to reduce the 1.9MB image down to 284KB. A good improvement, but this could be even better. Weniger, aber besser. What if we try saving to another image format?

sips -s formatOptions 80 -s format jpeg --resampleWidth 640 test.png --out test.jpg

% ls -la test*
-rw-r--r--  1 chada  staff  staff   857474 Apr 30 21:18 test-crushed.png
-rw-r--r--  1 chada  staff  staff   284051 May  3 20:55 test-resized-crushed.png
-rw-r--r--  1 chada  staff  staff   387543 May  3 20:54 test-resized.png
-rw-r--r--  1 chada  staff  staff   146602 Apr 30 21:24 test.jpg
-rw-r--r--@ 1 chada  staff  staff  1898113 Feb  2 18:54 test.png

I resized and changed the image type to a JPEG (with 80% quality), which reduced the image down to 147KB, whereas the PNG version was nearly twice the size. Considering that there is no need for transparency in these screenshots, JPEG will work well, especially with the smaller file sizes. That was a reduction of down to 8% of the original file size! Not bad, at all.

Since I had an entire folder full of the original screenshots, I was able to resize all of the images and save them out to a new folder named "resized" with a single command:

sips -s formatOptions 80 -s format jpeg --resampleWidth 640 *.png --out resized

Sound

For the most part, this game plays similar to the DOS version. The music does sound a little different, though, perhaps due to differences in the hardware I was using, in addition to how the PC-98 computer is emulated. This is my first exposure with a PC-98 game, so I'm not familiar if this is a standard variant between computer types or just how the emulators function.

However, there is a noticeable difference in the PC-98 version versus the traditional PC version. To give an example of how the game sounds, I made a recording of several scenes including Erana's Peace, the fight with the ogre, and the Kobold cave. Listen to hear the subtle differences in the Japanese version.

Trying to get a proper audio recording was an interesting process in itself, which resulted in using Snapz Pro X 2 on my PowerBook G4, instead of trying to jump through numerous hoops to get things to record well on a newer Mac. I briefly detailed some of the steps I took to make the recording in an older post about using the PowerBook.

When I got the game running on a PowerBook G4 under NP2 Carbon, the audio sounded pretty tinny, but that was likely due to the less-than-stellar speakers of a 20 year old laptop. Once I used headphones or external speakers, the PowerBook version sounded closer to the DOSBox-X version on my MacBook Pro. Considering that this sounded a lot better with dedicated speakers, perhaps I should have played from the PowerBook, but this did prove to be an interesting experiment to learn how to play a PC-98 game on various eras of hardware and software.

Issues

This version of QFG1 is missing some of the keyboard shortcuts that the English version uses, such as CTRL+A for "Ask about", but perhaps the concept to ask a question has different connotations in Japanese that wouldn't make sense for the dedicated shortcut. I was only able to figure out a handful of commands before learning that I could switch the game over to English (Sierra menu > Language, or CTRL+L). However, several shortcuts did remain, such as bringing up the stats or telling the time of day.

Unfortunately, the CTRL key was not registering under DOSBox-X 0.83.9. It worked fine in the NP2 Carbon emulator, so I initially assumed I needed to configure DOSBox-X in a different manner, perhaps to use a different keyboard layout. So I updated keyboardlayout in the dosbox.conf file to us. One can also set: pc-98 force ibm keyboard layout = true or the keyboard controller type to at. This also helped with typing from the command line, since the : was not the same key (use the ' key for that on a US layout).

Altering the configuration helped, but it still did not resolve the issue where CTRL was not recognized in Quest for Glory 1. After more research, I came across a bug report in DOSBox-X where the CTRL button not working (PC98mode). This sounded like the same issue I was having, and by upgrading to a newer version of DOSBox-X (version 2022.16.26, which is also Apple Silicon native. I don't believe the official DOSBox project has been updated to run on Apple Silicon processors yet.), this fixed the keyboard shortcut issue. Now I could quickly pull up the stats menu or perform some cheat codes!

Cheats

Even in the days before the internet, I learned about the infamous cheat code "razzle dazzle root beer" in Hero's Quest/QFG1, which helps avoid a lot of excess stats grinding (and I've done PLENTY of that over the past several decades). As I mentioned in the previous section, with the initial version of DOSBox-X I used, the CTRL key did not work, which resulted in the keyboard shortcuts being useless, and it also meant that it was not possible to select cheat codes if CTRL and ALT meta keys were not recognized. The following are links for the QFG1 cheats and debug codes to modify stats, inventory, and other debugging information.

Cheat Codes:

But even if that was not possible, there are other tools which can hack the character's save file which is used to transfer between Quest for Glory games. The most prominent tools I found were QFG Importer and QFG Character Editor. This latter tool is older and only works with files exported from the first two games, but there is a web interface (in addition to an archived Windows program). QFG Importer is newer and is a Windows program which can work with the exported .sav files from the first four games.

QFG Importer:

QFG Character Editor:

Extra — QFG1 Randomizer

Not related to the Japanese version of Quest for Glory 1 specifically, but there is now a QFG1 Randomizer program which randomly switches the location of inventory items throughout the game, which gives the game an interesting twist. It's differences like this which inspired me to play the Japanese version of QFG1 — to add something new to a game I've been playing repeatedly since it was first released.

Older posts »