diff --git a/.gitignore b/.gitignore index ee529704a..a4b9fec53 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ profile # vscode .vscode/ /.vs + +# Claude Code session/agent state (local only) +.claude/ diff --git a/HOWTO-Build-macOS.md b/HOWTO-Build-macOS.md new file mode 100644 index 000000000..da2c0a0ab --- /dev/null +++ b/HOWTO-Build-macOS.md @@ -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--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 +# 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 \ + +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. diff --git a/Makefile b/Makefile index 1dd8cee48..17b018105 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 @@ -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 diff --git a/code/cgame/cg_playerstate.c b/code/cgame/cg_playerstate.c index 1c5d9a293..858af7d34 100644 --- a/code/cgame/cg_playerstate.c +++ b/code/cgame/cg_playerstate.c @@ -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 ) { diff --git a/code/game/g_main.c b/code/game/g_main.c index d8e4481c6..bf10d4f38 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -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; diff --git a/code/ui/ui_shared.c b/code/ui/ui_shared.c index 354cf5260..c4b965f62 100644 --- a/code/ui/ui_shared.c +++ b/code/ui/ui_shared.c @@ -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; @@ -3478,7 +3485,7 @@ void Item_Text_Paint( itemDef_t *item ) { Item_SetTextExtents( item, &width, &height, textPtr ); if ( *textPtr == '\0' ) { - return; + goto cleanup; } @@ -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; + } } diff --git a/scripts/mac/install-rtcw-steamcmd.sh b/scripts/mac/install-rtcw-steamcmd.sh new file mode 100755 index 000000000..d4f842c67 --- /dev/null +++ b/scripts/mac/install-rtcw-steamcmd.sh @@ -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 <&2 +Usage: STEAM_LOGIN= [INSTALL_DIR=] $0 + or: $0 [] + +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 /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" diff --git a/scripts/mac/playtest.sh b/scripts/mac/playtest.sh new file mode 100755 index 000000000..9377a5f6b --- /dev/null +++ b/scripts/mac/playtest.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# RealRTCW playtest launcher with ASAN/UBSAN capture. +# +# Запускает текущий arm64 билд, ловит любые ASAN/UBSAN reports в /tmp, +# и в конце выводит чеклист, что проверять для 4 фиксов на этой ветке. +# +# Если билд собран с -fsanitize (наличие Makefile.local с -fsanitize=...), +# инструментация активна — играй спокойно, при любом UAF/UB будет вопль +# в stderr ИЛИ в /tmp/rtcw-asan..log. +# +# Если хочешь обычный (быстрый, без оверхеда) билд: +# rm Makefile.local +# make ARCH=arm64 USE_INTERNAL_LIBS=0 USE_OPENAL=0 -j8 +# +# Если хочешь снова ASAN — Makefile.local восстанавливается одной строкой: +# echo "BASE_CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1\nLDFLAGS += -fsanitize=address,undefined" > Makefile.local +# make clean && make ARCH=arm64 USE_INTERNAL_LIBS=0 USE_OPENAL=0 -j8 + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BIN="$REPO_ROOT/build/release-darwin-arm64-nosteam/RealRTCW.arm64" +ASAN_LOG_PREFIX="/tmp/rtcw-asan" +UBSAN_LOG_PREFIX="/tmp/rtcw-ubsan" + +if [ ! -x "$BIN" ]; then + echo "ERROR: бинарник не найден: $BIN" + echo " собери его: cd $REPO_ROOT && make ARCH=arm64 USE_INTERNAL_LIBS=0 USE_OPENAL=0 -j8" + exit 1 +fi + +# Чистим прошлые логи sanitizer'а — чтобы видеть свежие. +rm -f "${ASAN_LOG_PREFIX}".* "${UBSAN_LOG_PREFIX}".* 2>/dev/null + +# Определяем, есть ли ASAN в бинаре, чтобы предупредить +if otool -L "$BIN" 2>/dev/null | grep -q asan; then + echo "═══ RealRTCW ASAN/UBSAN build ═══" + echo "Sanitizer активен — будет медленнее обычного, но любой UAF/UB будет пойман." +else + echo "═══ RealRTCW release build (без sanitizer'а) ═══" +fi +echo "Лог-префиксы: ${ASAN_LOG_PREFIX}..log ${UBSAN_LOG_PREFIX}..log" +echo "Бинарник: $BIN" +echo "" + +export ASAN_OPTIONS="abort_on_error=0:detect_leaks=0:halt_on_error=0:log_path=${ASAN_LOG_PREFIX}" +export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=0:log_path=${UBSAN_LOG_PREFIX}" + +# cd в каталог с бинарником, чтобы он нашёл renderer .dylib рядом. +cd "$(dirname "$BIN")" +# stdin → /dev/null: иначе Q3-движок включает встроенную TTY-консоль и +# крадёт клавиатуру у SDL-окна (символы дублируются в терминал). +"$BIN" "$@" < /dev/null +GAME_EXIT=$? + +echo "" +echo "═══ Игра вышла с кодом $GAME_EXIT ═══" + +ASAN_HITS=$(ls "${ASAN_LOG_PREFIX}".* 2>/dev/null | wc -l | tr -d ' ') +UBSAN_HITS=$(ls "${UBSAN_LOG_PREFIX}".* 2>/dev/null | wc -l | tr -d ' ') + +if [ "$ASAN_HITS" -gt 0 ] || [ "$UBSAN_HITS" -gt 0 ]; then + echo "" + echo "⚠ Sanitizer что-то нашёл:" + for f in ${ASAN_LOG_PREFIX}.* ${UBSAN_LOG_PREFIX}.*; do + [ -f "$f" ] || continue + echo "─── $f ───" + head -40 "$f" + echo "(см. полностью: cat $f)" + echo "" + done +else + echo "✓ Никаких ASAN/UBSAN репортов. Sanitizer молчал — это хорошо." +fi + +cat <<'EOF' + +═══ ЧЕКЛИСТ — что проверить для 4 фиксов ═══ + +A1 — low-ammo warning для поздних оружий (cg_playerstate.c) + • Новая игра / любой сохраняшка + • В консоли: \cheats 1; \giveall + • Возьми MP44 / MG42M / BAR / WP_VENOM / WP_PANZERFAUST + (любое оружие с enum >= 32 — см. bg_public.h:528-599) + • Постреляй до низкого боезапаса + • ОЖИДАЕМО: должен прозвучать warning beep + HUD ammo flash + • БЫЛО ДО ФИКСА: тишина — этот код пути для оружия >=32 был сломан + +A2 — savegame UAF (ui_shared.c) + • Запусти кампанию, дойди до места, сделай Quick Save (F5) + • ESC → Load Game + • Наведи курсор на сохраняшку + • ОЖИДАЕМО: справа корректная инфа — date, level, playtime + • БЫЛО ДО ФИКСА: каждый кадр после первого item->text dangling — мог + показываться garbage / crash на ASAN-сборке + +A3 — Com_StringContains size_t wrap (common.c) + • В игре, в консоли: \find этооченьдлиннаяподстрокачтобывсёполомать + • ОЖИДАЕМО: ничего не находит, не крашит, не подвисает + • БЫЛО ДО ФИКСА: на arm64 (64→32-bit truncation) — UB, потенциально + garbage iter / OOB read + +A4 — snd_mix NULL chunk wrap (snd_mix.c) + • Самый сложный для ручной проверки. + • В консоли: \s_useOpenAL 0; \snd_restart + (откатываемся на legacy SDL mixer — путь, где жил баг) + • Поиграй 60+ секунд, послушай звуки + • ОЖИДАЕМО: звук работает корректно, не крашит на длинных сэмплах + • БЫЛО ДО ФИКСА: NULL deref на границе chunk'а в SetVoiceAmplitudeFrom16 / + S_PaintChannelFrom16_scalar / S_PaintChannelFromMuLaw + +EOF