Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ profile
# vscode
.vscode/
/.vs

# Claude Code session/agent state (local only)
.claude/
149 changes: 149 additions & 0 deletions HOWTO-Build-macOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Build RealRTCW natively on macOS (Apple Silicon)

A native arm64 build of RealRTCW for modern macOS. Tested on macOS 26.5
(Tahoe) on M1 / M2 / M3; should also work on later Apple Silicon and on
recent Intel Macs (the same Homebrew toolchain handles both arches — only
the `ARCH=` flag changes).

There is no Apple-Silicon build of RealRTCW on ModDB or the upstream
repository: the bundled `make-macosx-ub.sh` targets gcc-4.0 and the
10.5/10.6 SDKs and has not been updated for Apple Silicon. This file
documents the minimal set of changes needed to get a clean native build
on a current macOS install.

## 1. Prerequisites

Xcode is not required. The build only needs the Command Line Tools and a
handful of Homebrew libraries.

```bash
xcode-select --install # if you don't already have CLT installed

brew install pkgconf sdl3 ffmpeg freetype jpeg opus opusfile libogg libvorbis
```

(`pkgconf` provides the `pkg-config` binary the Makefile calls. `sdl3` is
required because RealRTCW upstream migrated from SDL2 to SDL3 — the
bundled `code/libs/macosx/libSDL2-2.0.0.dylib` predates that migration
and no longer satisfies symbols like `SDL_PutAudioStreamData` or
`SDL_UpdateGamepads`.)

## 2. Build

From the repo root:

```bash
make ARCH=arm64 USE_INTERNAL_LIBS=0
```

`USE_INTERNAL_LIBS=0` tells the Makefile to link against Homebrew's
zlib / freetype / jpeg / opus / ogg / vorbis instead of the in-tree
vendored copies. The vendored `zlib-1.2.11` and `freetype-2.9` were
written before C23 and no longer compile cleanly under clang 21 (the
stock CLT compiler in late-2025 macOS releases). This is also what
MacSourcePorts use for their iortcw build.

For an Intel Mac, replace `ARCH=arm64` with `ARCH=x86_64`.

The build deposits these artefacts under
`build/release-darwin-<arch>-nosteam/`:

* `RealRTCW.arm64` — main executable
* `renderer_sp_opengl1_arm64.dylib` — OpenGL 1 renderer (Apple's GL on
Apple Silicon is GL 2.1 backed by Metal, which is plenty for this
renderer)
* `main/qagame.sp.arm64.dylib`
* `main/cgame.sp.arm64.dylib`
* `main/ui.sp.arm64.dylib`

You will see a few link-time warnings of the form:

```
ld: warning: building for macOS-11.0, but linking with dylib
'/opt/homebrew/opt/ffmpeg/lib/libavcodec.62.dylib'
which was built for newer version 26.0
```

These are cosmetic — Homebrew ffmpeg is compiled against a newer minimum
SDK than RealRTCW's `-mmacosx-version-min=11.0`. Linker still resolves
all symbols.

## 3. Game data

RealRTCW needs both the original RTCW pak files **and** the RealRTCW mod
data (pak files, `realrtcwdefault.cfg`, cinematic videos). Both are
distributed via Steam:

| Steam AppID | Title | Required for |
|-------------|-------|--------------|
| 9010 | Return to Castle Wolfenstein | `pak0.pk3`, `sp_pak1.pk3`, etc. |
| 1379630 | RealRTCW | mod paks, `realrtcwdefault.cfg`, `video/` |

Both depots ship only Windows binaries on Steam; we force the Windows
platform on steamcmd. The .dll / .exe files inside those depots aren't
needed — we only consume their data files.

You can use `scripts/mac/install-rtcw-steamcmd.sh` to fetch RTCW and
symlink its paks into the homepath the native build looks at. Then run
the same `steamcmd` for AppID 1379630 to fetch RealRTCW's mod data and
symlink the rest:

```bash
brew install --cask steamcmd

scripts/mac/install-rtcw-steamcmd.sh <your_steam_login>
# steamcmd will prompt for password + Steam Guard the first time.

# Now grab the RealRTCW mod data (free Steam game; requires you to own RTCW).
mkdir -p ~/Games/RealRTCW-data
steamcmd +@sSteamCmdForcePlatformType windows \
+force_install_dir ~/Games/RealRTCW-data \
+login <your_steam_login> \
+app_update 1379630 validate \
+quit

# Symlink RealRTCW mod paks, configs, and cinematic videos:
SRC=~/Games/RealRTCW-data/Main
DST="$HOME/Library/Application Support/RealRTCW/main"
mkdir -p "$DST"
for f in "$SRC"/*.pk3 "$SRC"/*.cfg; do ln -sfh "$f" "$DST/$(basename "$f")"; done
ln -sfh "$SRC/video" "$DST/video"
```

After this, `~/Library/Application Support/RealRTCW/main/` should
contain `pak0.pk3` + `sp_pak1..4.pk3` (vanilla RTCW), all
`z_realrtcw_*.pk3` (mod content), `realrtcwdefault.cfg`, `autoexec.cfg`,
and `video/` (cinematics).

## 4. Run

```bash
./build/release-darwin-arm64-nosteam/RealRTCW.arm64
```

The engine opens a fullscreen SDL3/Cocoa window at native desktop
resolution, initialises Apple's GL-on-Metal driver, and drops you into
the RealRTCW main menu.

## What the macOS patches actually do

Three minimal changes against upstream:

1. **`Makefile`** — wire `ffmpeg` into the `darwin` block via
`pkg-config`. Upstream only adds `libavcodec/libavformat/libavutil/
libswscale/libswresample` flags in the Linux block, so on macOS
`cl_cin.c` failed with `'libavcodec/avcodec.h' file not found` even
with ffmpeg installed.

2. **`Makefile`** — replace the darwin SDL block. The stale `-framework
SDL2` (and bundled `libSDL2-2.0.0.dylib`) cannot satisfy upstream's
new SDL3 audio/gamepad API calls. Now links Homebrew SDL3 via
`pkg-config sdl3`.

3. **`code/game/g_main.c`** — fix a stray
`#include "../../steam/steam.h"` (every other file in `code/game/`
correctly uses `"../steam/steam.h"`). This is an upstream typo
that happens to compile under the maintainer's Windows toolchain.

That is the complete patch set. No engine code, no platform layer, no
renderer code was changed.
34 changes: 29 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,12 @@ ifeq ($(PLATFORM),darwin)
RENDERER_LIBS=
OPTIMIZEVM = -O3

# ffmpeg via Homebrew pkg-config (upstream Makefile only wires this up for Linux)
FFMPEG_CFLAGS := $(shell $(PKG_CONFIG) --silence-errors --cflags libavcodec libavformat libavutil libswscale libswresample)
FFMPEG_LIBS := $(shell $(PKG_CONFIG) --silence-errors --libs libavcodec libavformat libavutil libswscale libswresample)
BASE_CFLAGS += $(FFMPEG_CFLAGS)
LIBS += $(FFMPEG_LIBS)

# Default minimum Mac OS X version
ifeq ($(MACOSX_VERSION_MIN),)
MACOSX_VERSION_MIN=10.5
Expand Down Expand Up @@ -595,12 +601,24 @@ ifeq ($(PLATFORM),darwin)
BASE_CFLAGS += -fno-strict-aliasing -fno-common -pipe

ifeq ($(USE_OPENAL),1)
# Modern macOS no longer ships OpenAL.framework headers in the SDK.
# Use Homebrew's openal-soft. It's a keg-only formula (Apple's frozen
# OpenAL.framework dylib still lives in /System, so brew refuses to
# symlink the replacement into /opt/homebrew); naked pkg-config won't
# find it. Probe brew for the prefix and re-run pkg-config with the
# keg's pkgconfig dir on PKG_CONFIG_PATH when OPENAL_CFLAGS is empty.
ifeq ($(strip $(OPENAL_CFLAGS)),)
BREW_OPENAL_PREFIX := $(shell brew --prefix openal-soft 2>/dev/null)
ifneq ($(BREW_OPENAL_PREFIX),)
OPENAL_CFLAGS := $(shell PKG_CONFIG_PATH=$(BREW_OPENAL_PREFIX)/lib/pkgconfig $(PKG_CONFIG) --silence-errors --cflags openal)
OPENAL_LIBS := $(shell PKG_CONFIG_PATH=$(BREW_OPENAL_PREFIX)/lib/pkgconfig $(PKG_CONFIG) --silence-errors --libs openal)
endif
endif
ifneq ($(USE_LOCAL_HEADERS),1)
CLIENT_CFLAGS += -I/System/Library/Frameworks/OpenAL.framework/Headers
CLIENT_CFLAGS += $(OPENAL_CFLAGS)
endif
ifneq ($(USE_OPENAL_DLOPEN),1)
ifneq ($(USE_INTERNAL_LIBS),1)
CLIENT_CFLAGS += $(OPENAL_CFLAGS)
CLIENT_LIBS += $(THREAD_LIBS) $(OPENAL_LIBS)
CLIENT_EXTRA_FILES += $(LIBSDIR)/macosx/libopenal.dylib
else
Expand Down Expand Up @@ -637,9 +655,15 @@ ifeq ($(PLATFORM),darwin)
RENDERER_LIBS += $(LIBSDIR)/macosx/libSDL2-2.0.0.dylib
CLIENT_EXTRA_FILES += $(LIBSDIR)/macosx/libSDL2-2.0.0.dylib
else
BASE_CFLAGS += -I/Library/Frameworks/SDL2.framework/Headers
CLIENT_LIBS += -framework SDL2
RENDERER_LIBS += -framework SDL2
# macOS: link SDL3 from Homebrew (RealRTCW upstream migrated to SDL3 API;
# the bundled libSDL2-2.0.0.dylib in code/libs/macosx is stale and predates
# the migration, so the old -framework SDL2 fallback no longer satisfies
# symbols like SDL_PutAudioStreamData, SDL_UpdateGamepads, etc.).
SDL_CFLAGS ?= $(shell $(PKG_CONFIG) --silence-errors --cflags sdl3)
SDL_LIBS ?= $(shell $(PKG_CONFIG) --silence-errors --libs sdl3)
BASE_CFLAGS += $(SDL_CFLAGS)
CLIENT_LIBS += $(SDL_LIBS)
RENDERER_LIBS += $(SDL_LIBS)
endif

OPTIMIZE = $(OPTIMIZEVM) -ffast-math
Expand Down
2 changes: 1 addition & 1 deletion code/cgame/cg_playerstate.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void CG_CheckAmmo( void ) {
// first weap now WP_LUGER
for ( i = WP_KNIFE ; i < WP_NUM_WEAPONS ; i++ )
{
if ( !( weapons[0] & ( 1 << i ) ) ) {
if ( !COM_BitCheck( weapons, i ) ) {
continue;
}
if ( cg.snap->ps.ammo[BG_FindAmmoForWeapon( i )] < 0 ) {
Expand Down
2 changes: 1 addition & 1 deletion code/game/g_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ If you have questions concerning this license or the applicable additional terms

#include "g_local.h"
#include "g_survival.h"
#include "../../steam/steam.h"
#include "../steam/steam.h"

level_locals_t level;

Expand Down
20 changes: 16 additions & 4 deletions code/ui/ui_shared.c
Original file line number Diff line number Diff line change
Expand Up @@ -3445,27 +3445,34 @@ void Item_Text_Paint( itemDef_t *item ) {
const char *textPtr;
int height, width;
vec4_t color;
const char *savedText = NULL;
qboolean savedTextValid = qfalse;


//----(SA) added
if ( item->textSavegameInfo ) {
DC->getCVarString( "ui_savegameInfo", infostring, sizeof( infostring ) ); // grab the string the client set
// infostring is on this function's stack; stash the original
// item->text so the temporary override does not leak past return
// and produce a dangling pointer for the next frame.
savedText = item->text;
savedTextValid = qtrue;
item->text = &infostring[0];
}
//----(SA) end

if ( item->window.flags & WINDOW_WRAPPED ) {
Item_Text_Wrapped_Paint( item );
return;
goto cleanup;
}
if ( item->window.flags & WINDOW_AUTOWRAPPED ) {
Item_Text_AutoWrapped_Paint( item );
return;
goto cleanup;
}

if ( item->text == NULL ) {
if ( item->cvar == NULL ) {
return;
goto cleanup;
} else {
DC->getCVarString( item->cvar, text, sizeof( text ) );
textPtr = text;
Expand All @@ -3478,7 +3485,7 @@ void Item_Text_Paint( itemDef_t *item ) {
Item_SetTextExtents( item, &width, &height, textPtr );

if ( *textPtr == '\0' ) {
return;
goto cleanup;
}


Expand Down Expand Up @@ -3514,6 +3521,11 @@ void Item_Text_Paint( itemDef_t *item ) {
// }

DC->drawText( item->textRect.x, item->textRect.y, item->font, item->textscale, color, textPtr, 0, 0, item->textStyle );

cleanup:
if ( savedTextValid ) {
item->text = savedText;
}
}


Expand Down
82 changes: 82 additions & 0 deletions scripts/mac/install-rtcw-steamcmd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Install Return to Castle Wolfenstein on macOS via steamcmd, then link its
# pak files into RealRTCW's homepath so the native arm64 build can find them.
#
# RTCW on Steam ships only a Windows depot; we force the Windows platform on
# steamcmd to download it. We only need the pak0.pk3 / sp_pak*.pk3 data files,
# which are content-only and platform-agnostic.
set -euo pipefail

STEAM_LOGIN="${STEAM_LOGIN:-${1:-}}"
INSTALL_DIR="${INSTALL_DIR:-${2:-$HOME/Games/RTCW}}"
APPID="${APPID:-9010}" # 9010 = Return to Castle Wolfenstein

# Where the native macOS RealRTCW binary looks for `main/*.pk3`. See FS_Startup
# output: ~/Library/Application Support/RealRTCW/main is checked first.
HOMEPATH_MAIN="${HOMEPATH_MAIN:-$HOME/Library/Application Support/RealRTCW/main}"

usage() {
cat <<USAGE >&2
Usage: STEAM_LOGIN=<login> [INSTALL_DIR=<path>] $0
or: $0 <login> [<install_dir>]

Environment variables:
STEAM_LOGIN Your Steam account name (you must own RTCW, appid 9010).
INSTALL_DIR Where steamcmd installs RTCW. Default: \$HOME/Games/RTCW.
HOMEPATH_MAIN Where to symlink the .pk3 files for RealRTCW to find them.
Default: \$HOME/Library/Application Support/RealRTCW/main.

Notes:
* Steam Guard: steamcmd will prompt for the code on first login;
subsequent runs reuse the saved credentials.
* +@sSteamCmdForcePlatformType windows is mandatory and must come
before +login - RTCW has no native macOS depot on Steam.
USAGE
}

if [[ -z "$STEAM_LOGIN" ]]; then
usage
exit 1
fi

if ! command -v steamcmd >/dev/null 2>&1; then
echo "ERROR: steamcmd not found in PATH." >&2
echo " Install with: brew install --cask steamcmd" >&2
exit 1
fi

mkdir -p "$INSTALL_DIR"

echo "==> Installing appid $APPID into $INSTALL_DIR as Steam user '$STEAM_LOGIN'"

steamcmd \
+@sSteamCmdForcePlatformType windows \
+force_install_dir "$INSTALL_DIR" \
+login "$STEAM_LOGIN" \
+app_update "$APPID" validate \
+quit

# RTCW's Steam depot lays out paks under <install>/Main/ (capital M).
MAIN_SRC="$INSTALL_DIR/Main"
if [[ ! -d "$MAIN_SRC" ]]; then
echo "==> Install finished but $MAIN_SRC was not found. Layout may have changed." >&2
exit 1
fi

if [[ ! -f "$MAIN_SRC/pak0.pk3" ]]; then
echo "==> $MAIN_SRC exists but pak0.pk3 is missing. Likely a partial install." >&2
exit 1
fi

echo "==> Linking $MAIN_SRC/*.pk3 -> $HOMEPATH_MAIN/"
mkdir -p "$HOMEPATH_MAIN"
for pk3 in "$MAIN_SRC"/*.pk3; do
ln -sfh "$pk3" "$HOMEPATH_MAIN/$(basename "$pk3")"
done

echo
echo "==> Done. RealRTCW will find these on next launch:"
ls -l "$HOMEPATH_MAIN" | sed 's/^/ /'
echo
echo "==> Run:"
echo " ./build/release-darwin-arm64-nosteam/RealRTCW.arm64"
Loading