Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .github/workflows/build-macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Build macOS arm64

# Native Apple Silicon (arm64) build. Runs on the GitHub-hosted macOS runners,
# which are arm64 from macos-14 onwards. Only fires when files that affect the
# macOS build change, so doc-only pushes don't burn runner time.
on:
push:
branches: [master]
paths:
- 'src/**'
- 'res/**'
- 'macos.mk'
- 'version.mk'
- 'tools/build_macos_deps.sh'
- 'tools/make_macos_app.sh'
- '.github/workflows/build-macos.yml'
pull_request:
paths:
- 'src/**'
- 'res/**'
- 'macos.mk'
- 'version.mk'
- 'tools/build_macos_deps.sh'
- 'tools/make_macos_app.sh'
- '.github/workflows/build-macos.yml'
workflow_dispatch:

jobs:
build-macos:
name: "Build macOS arm64"
runs-on: macos-15

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Homebrew dependencies
run: |
set -eux
brew install \
pkg-config \
sdl2 sdl2_image sdl2_mixer sdl2_net \
ffmpeg \
luajit \
openal-soft \
libspng \
minizip \
miniupnpc \
libnatpmp \
zlib \
curl \
dylibbundler

# The source-only deps (astronomy/centijson/enet6) are cloned from pinned
# upstreams and compiled to static libs. Cache them keyed on the build
# script so they only rebuild when that script changes.
- name: Cache source-only dependencies
id: deps-cache
uses: actions/cache@v4
with:
path: |
deps/astronomy
deps/centijson
deps/enet6
key: macos-deps-${{ hashFiles('tools/build_macos_deps.sh') }}

- name: Build source-only dependencies (astronomy, centijson, enet6)
if: steps.deps-cache.outputs.cache-hit != 'true'
run: |
set -eux
./tools/build_macos_deps.sh

- name: Build
run: |
set -eux
VER_MAJOR=$(grep -E '^VER_MAJOR=' version.mk | cut -d= -f2)
VER_MINOR=$(grep -E '^VER_MINOR=' version.mk | cut -d= -f2)
VER_RELEASE=$(grep -E '^VER_RELEASE=' version.mk | cut -d= -f2)
BUILD_NUMBER=$(git rev-list --count HEAD)
VER_SUFFIX=CI_$(git rev-parse --short=7 HEAD)
ARTIFACT_NAME=keeperfx-macos_arm64-${VER_MAJOR}_${VER_MINOR}_${VER_RELEASE}_${BUILD_NUMBER}_${VER_SUFFIX}
echo "ARTIFACT_NAME=$ARTIFACT_NAME" >> "$GITHUB_ENV"
make -f macos.mk BUILD_NUMBER="$BUILD_NUMBER" VER_SUFFIX="$VER_SUFFIX" -j"$(sysctl -n hw.ncpu)"

- name: Verify binary is a native arm64 Mach-O
run: |
set -eux
file bin/keeperfx
lipo -archs bin/keeperfx | grep -qw arm64

- name: Package self-contained KeeperFX.app
run: |
set -eux
./tools/make_macos_app.sh
# Fail if any Homebrew path leaked into the bundle (i.e. it isn't
# actually self-contained).
! grep -rl "/opt/homebrew" dist/KeeperFX.app/Contents/libs 2>/dev/null
otool -L dist/KeeperFX.app/Contents/MacOS/keeperfx | grep -q "@executable_path/../libs"
codesign --verify --verbose=2 dist/KeeperFX.app

- name: Upload engine binary
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
path: bin/keeperfx

- name: Upload KeeperFX.app
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}-app
path: dist/KeeperFX.app
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/.local
/bin
/dist
/game
/obj
/sdl
/pkg
Expand Down
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Currently the whole codebase of Dungeon Keeper is remade and all code has been r

## Features
- Windows 7/10/11 support
- Native Linux and macOS (Apple Silicon / arm64) builds
- Higher screen resolutions
- Increased FPS, decoupled gfx and game logic
- Improved and modernized controls
Expand All @@ -45,6 +46,64 @@ You will need the original Dungeon Keeper files, either from an old CD or from t
or [Steam](https://store.steampowered.com/app/1996630/Dungeon_Keeper_Gold/).


## macOS (Apple Silicon)

This fork adds a **native Apple Silicon (arm64) build** — a real Mach-O binary,
no Rosetta or emulation. It compiles, runs, and plays. See
[`docs/MACOS_ARM64_PORT.md`](docs/MACOS_ARM64_PORT.md) for the full write-up.

### Build from source

```sh
brew install pkg-config sdl2 sdl2_image sdl2_mixer sdl2_net ffmpeg luajit \
openal-soft libspng minizip miniupnpc libnatpmp zlib curl dylibbundler
./tools/build_macos_deps.sh # one-time: builds astronomy/centijson/enet6
make -f macos.mk -j"$(sysctl -n hw.ncpu)" # -> bin/keeperfx (arm64 Mach-O)
```

CI builds and verifies this on every push (`.github/workflows/build-macos.yml`),
and uploads a ready-to-run `KeeperFX.app` as a build artifact.

### Package a self-contained `KeeperFX.app`

```sh
tools/make_macos_app.sh # -> dist/KeeperFX.app
```

This bundles the engine's libraries (via `dylibbundler`, incl. the SDL3 that
`sdl2-compat` loads at runtime) and ad-hoc signs it, so the `.app` runs on any
Apple Silicon Mac with **no Homebrew installed**.

### Running: required game files & layout

Like every KeeperFX build, the macOS build needs a game directory populated with
the KeeperFX data **plus the original Dungeon Keeper files** listed in
[`docs/files_required_from_original_dk.txt`](docs/files_required_from_original_dk.txt).
The install gate is simply the presence of `data/bluepal.dat`.

`KeeperFX.app` is a *drop-in* engine: on startup it locates itself and changes
the working directory to the folder that contains the `.app`, so it finds the
game data sitting next to it. The layout is:

```
YourKeeperFX/ <- any folder (including Desktop/Documents)
├── KeeperFX.app <- drop the app in here, next to the data
├── data/ sound/ ldata/ fxdata/ creatrs/ campgns/ levels/ music/ ...
└── keeperfx.cfg
```

Double-click `KeeperFX.app`. On first launch:
- If Gatekeeper blocks it (ad-hoc signed, not notarized), right-click → Open once.
- If the folder is privacy-protected (Desktop, Documents, Downloads), macOS asks
to let KeeperFX access files there — click **Allow**. It then runs from any
location.

> Tip: if you have GOG's *Dungeon Keeper Gold* installed, the required original
> files ship **uncompressed** inside the app bundle at
> `Contents/Resources/game/{DATA,SOUND}/` (and the `keeper0*.ogg` soundtrack in
> its game root) — no CD-image extraction needed. Copy them in with lowercase names.


## Development
To get started with KeeperFX development, refer to the [Development Guide](https://github.com/dkfans/keeperfx/wiki/Building-KeeperFX) for
detailed instructions on setting up a development environment and building KeeperFX from source.
Expand Down
140 changes: 140 additions & 0 deletions docs/MACOS_ARM64_PORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Native macOS (Apple Silicon / arm64) build

KeeperFX builds and runs **natively on Apple Silicon** — a Mach-O arm64 binary,
no Rosetta, no emulation. It has been verified end to end: it compiles, launches,
loads the original Dungeon Keeper data, and runs into live gameplay.

The macOS build reuses the native **Linux** build (`linux.mk`), which already does
almost all of the cross-platform work; `macos.mk` is the Linux build adapted for
arm64 + Homebrew.

## Why this is tractable

The hard part of porting KeeperFX (the historic hooking into the original 32-bit
`DK.exe` at fixed addresses) is **gone** — the engine is fully decompiled and
standalone. Concretely:

- **64-bit clean.** The Linux build compiles with `-march=x86-64`; there is no
32-bit lock-in. (Critical, since macOS has had zero 32-bit support since
Catalina.)
- **SDL2 is the platform layer** for video, input, audio and networking. The
renderer is software/surface based — no OpenGL or DirectX in the hot path.
- The platform-specific spots are already guarded: `src/bflib_cpu.c` (`cpuid`
asm behind `__i386__ || __x86_64__`), `src/bflib_crash.c` (POSIX path with a
non-Linux fallback), `src/linux.cpp` (pure POSIX, reused as-is), and every
`#include <windows.h>` sits behind `#ifdef _WIN32` / `__MINGW32__`.

## Source changes that *were* needed

Compiling on arm64 did require a few small edits to shared headers (they also
help a native Linux build and change no behaviour on Windows):

- **Un-packing in-memory-only structs.** Several structs were declared inside
`#pragma pack(1)` blocks (`GuiButton`/`GuiMenu` in `bflib_guibtns.h`;
`NamedCommand`/`NamedField`/`ConfigFileData` in `config.h`; `GamekeySettings`
in `front_input.h`; `DemoItem` in `frontend.h`). None of these are serialized
to disk — they are runtime helpers full of pointers and function pointers. On
arm64, statically-initialized packed structs place those pointers at unaligned
offsets that the linker rejects. They are now left at natural alignment.
- **`src/globals.h`** — the POSIX `<unistd.h>`/`<signal.h>` include path now also
fires on `__APPLE__` / `__unix__`, not just the legacy `unix` macro.

## Prerequisites

1. **Xcode Command Line Tools:** `xcode-select --install`
2. **Homebrew:** https://brew.sh
3. Homebrew libs: `sdl2 sdl2_image sdl2_mixer sdl2_net ffmpeg luajit openal-soft
libspng minizip miniupnpc libnatpmp zlib curl` (plus `innoextract`/`sevenzip`
if you extract original DK data from a GOG installer).

## Build

```sh
# from the repo root
./tools/build_macos_deps.sh # one-time (and after dep bumps)
make -f macos.mk -j"$(sysctl -n hw.ncpu)"
```

`build_macos_deps.sh` builds the three deps Homebrew doesn't publish for arm64
(`centijson`, `astronomy`, `enet6`) from source into `deps/`, where `macos.mk`
expects them.

Output binary: `bin/keeperfx` (Mach-O arm64).

## Running

Run the binary from a directory populated with a KeeperFX install **plus the
files listed in `docs/files_required_from_original_dk.txt`** copied from an
original Dungeon Keeper (CD / GOG / Steam / Origin). KeeperFX's install gate is
simply a check for `data/bluepal.dat`.

> Tip: on a machine with the GOG "Dungeon Keeper Gold" app installed, all 16
> required files ship **uncompressed** inside the app bundle under
> `Contents/Resources/game/{DATA,SOUND}/` — no `game.gog` CD-image extraction is
> needed. Copy them in with lowercase names.

Benign log noise on first run: missing `font12/16.fxfont` (optional Unicode
fonts; it falls back to the `.fon` bitmap fonts) and `colours.col` / `tables.dat`
/ `alpha.col` "errors" that are regenerated at runtime from the palette.

## Packaging a self-contained `KeeperFX.app`

`tools/make_macos_app.sh` turns `bin/keeperfx` into a redistributable
`dist/KeeperFX.app` that runs on any Apple Silicon Mac with **no Homebrew**:

```sh
make -f macos.mk && tools/make_macos_app.sh
```

What it does, and the non-obvious bits it handles:

- **Bundles the dylibs.** `dylibbundler` copies every non-system dylib into
`Contents/libs` and rewrites the engine's load paths to `@executable_path/../libs`.
- **SDL3, which `dylibbundler` can't see.** Homebrew's `sdl2` is actually
`sdl2-compat` — a thin `libSDL2` that **`dlopen`s SDL3 at runtime**, so SDL3 is
not a link-time dependency and the bundling pass misses it (the shim then dies
in its initializer). The shim's first search path is `@loader_path/libSDL3.dylib`
(note: *unversioned*), so the script copies `libSDL3.0.dylib` in as
`Contents/libs/libSDL3.dylib` and the shim finds it with no env vars or rpath.
- **A drop-in, self-locating engine.** Finder starts apps with `cwd=/`, but the
game data lives in the folder the user dropped the `.app` into. The engine is
the bundle's main executable and, on startup, `macos_chdir_to_bundle_parent()`
(in `src/linux.cpp`) resolves its own path via `_NSGetExecutablePath`; if it is
running from inside a `*.app/Contents/MacOS/`, it `chdir`s to the folder
containing the `.app`. Run outside a bundle (a developer launching
`bin/keeperfx` directly) and the working directory is left untouched. This
replaces the older bash-wrapper launcher.
- **Folder-access permission prompt.** Because the engine is a real signed main
executable (not a script) and `Info.plist` declares `NSDesktopFolderUsage`/
`NSDocumentsFolderUsage`/`NSDownloadsFolderUsageDescription`, macOS shows a
normal privacy prompt the first time the app reads data from a protected folder
(Desktop/Documents/Downloads). The user clicks **Allow** once and it works from
any location. (A bash-wrapper main executable is *silently denied* instead of
prompted, which is why the wrapper approach failed on the Desktop.)
- **Ad-hoc code signing.** arm64 requires every Mach-O to be signed to run. The
dylibs are signed first (inside-out), then the whole bundle — sealing the
engine and giving macOS a stable app identity to attach the folder-access grant
to. Users still clear Gatekeeper once (right-click → Open) since it isn't
notarized.

CI runs this and uploads the `.app` as an artifact
(`.github/workflows/build-macos.yml`).

## Optional enhancements

- **Crash register dump.** Add an `#elif defined(__APPLE__) && defined(__aarch64__)`
branch to `log_posix_context()` in `src/bflib_crash.c` reading
`((ucontext_t*)context)->uc_mcontext->__ss.__pc` / `__sp`. Nicer crash logs;
not required.
- **Notarization.** Replace the ad-hoc signature with a Developer ID signature +
`notarytool` so the `.app` opens without the Gatekeeper right-click dance.
- **Share the source list.** Factor the common `KFX_SOURCES` out of `linux.mk`
so `macos.mk` can include it instead of duplicating (keeps them from drifting
when upstream adds/removes a source file).

## Staying in sync with upstream

The port adds only new files (`macos.mk`, `tools/build_macos_deps.sh`, this doc)
plus a handful of guarded header edits, so pulling from upstream rarely
conflicts. If upstream adds or removes a file in `linux.mk`'s `KFX_SOURCES`,
mirror that one line into `macos.mk`.
Loading