git: 2ac9e4a8713d - main - games/CWR-CE: New port

From: Olivier Cochard <olivier_at_FreeBSD.org>
Date: Thu, 02 Jul 2026 00:54:05 UTC
The branch main has been updated by olivier:

URL: https://cgit.FreeBSD.org/ports/commit/?id=2ac9e4a8713de9ed3443db974b6e410c21f90cbf

commit 2ac9e4a8713de9ed3443db974b6e410c21f90cbf
Author:     Olivier Cochard <olivier@FreeBSD.org>
AuthorDate: 2026-07-02 00:50:48 +0000
Commit:     Olivier Cochard <olivier@FreeBSD.org>
CommitDate: 2026-07-02 00:53:20 +0000

    games/CWR-CE: New port
    
    Game engine of Arma: Cold War Assault - Remastered
---
 games/CWR-CE/Makefile                  |  79 +++++++
 games/CWR-CE/distinfo                  |   7 +
 games/CWR-CE/files/cwr-ce.sh           |  41 ++++
 games/CWR-CE/files/install-cwr-data.sh | 392 +++++++++++++++++++++++++++++++++
 games/CWR-CE/files/pkg-message.in      |  31 +++
 games/CWR-CE/pkg-descr                 |  15 ++
 games/Makefile                         |   1 +
 7 files changed, 566 insertions(+)

diff --git a/games/CWR-CE/Makefile b/games/CWR-CE/Makefile
new file mode 100644
index 000000000000..614406cd3e5c
--- /dev/null
+++ b/games/CWR-CE/Makefile
@@ -0,0 +1,79 @@
+PORTNAME=	CWR-CE
+DISTVERSIONPREFIX=	v
+DISTVERSION=	3.01
+CATEGORIES=	games
+
+PATCH_SITES=	https://github.com/ocochard/CWR-CE/compare/
+PATCHFILES=	9abbdf2769f3008d76c47ac0af219388ff0323dc..b6c9690f03cf0abb6af577db7294b00c30f4172c.patch:-p1 \
+		b6c9690f03cf0abb6af577db7294b00c30f4172c..85dd554fc9e21312ee52d7f9eced6b171a6a892b.patch:-p1
+
+MAINTAINER=	olivier@FreeBSD.org
+COMMENT=	Arma: Cold War Assault - Remastered Community Edition
+WWW=		https://github.com/ofpisnotdead-com/CWR-CE
+
+LICENSE=	GPLv3+
+LICENSE_FILE=	${WRKSRC}/LICENSE
+
+BUILD_DEPENDS=	stb>0:devel/stb \
+		${LOCALBASE}/include/CLI/CLI.hpp:devel/cli11 \
+		${LOCALBASE}/include/vulkan/vulkan.h:graphics/vulkan-headers \
+		${LOCALBASE}/lib/cmake/Catch2/Catch2Config.cmake:devel/catch2 \
+		${LOCALBASE}/lib/cmake/glslang/glslang-config.cmake:graphics/glslang
+LIB_DEPENDS=	libcjson.so:devel/libcjson \
+		libcurl.so:ftp/curl \
+		libenkiTS.so:devel/enkits \
+		libfmt.so:devel/libfmt \
+		libfreetype.so:print/freetype2 \
+		libimgui.so:x11-toolkits/imgui \
+		libmimalloc.so:devel/mimalloc \
+		libogg.so:audio/libogg \
+		libopenal.so:audio/openal-soft \
+		libopus.so:audio/opus \
+		libSDL3.so:devel/sdl3 \
+		libvulkan.so:graphics/vulkan-loader \
+		libspdlog.so:devel/spdlog \
+		libvorbis.so:audio/libvorbis \
+		libzstd.so:archivers/zstd
+
+USES=		cmake compiler:c++20-lang dos2unix gl localbase pkgconfig
+
+DOS2UNIX_REGEX=	.*\.(c|h|cpp|hpp)
+USE_GITHUB=	yes
+GH_ACCOUNT=	ofpisnotdead-com
+GH_PROJECT=	CWR-CE
+GH_TAGNAME=	9abbdf2769f3008d76c47ac0af219388ff0323dc
+USE_GL=		gl
+
+SUB_FILES=	pkg-message
+
+# Disable vcpkg toolchain wiring — find_package(... CONFIG REQUIRED) picks up
+# system libraries from ${LOCALBASE}/lib/cmake instead.
+CMAKE_ARGS=	-DVCPKG_MANIFEST_MODE=OFF \
+		-DCMAKE_DISABLE_FIND_PACKAGE_VCPKG=ON
+
+CMAKE_OFF=	POSEIDON_DISABLE_PCH POSEIDON_BUILD_FUZZERS
+
+PLIST_FILES=	bin/PoseidonGame \
+		bin/PoseidonGameDemo \
+		bin/PoseidonServer \
+		bin/PoseidonStudio \
+		bin/PoseidonTetris \
+		bin/PoseidonTools \
+		bin/cwr-ce \
+		bin/install-cwr-data
+
+do-install:
+	${INSTALL_PROGRAM} \
+		${BUILD_WRKSRC}/apps/cwr/Game/PoseidonGame \
+		${BUILD_WRKSRC}/apps/cwr/GameDemo/PoseidonGameDemo \
+		${BUILD_WRKSRC}/apps/cwr/Server/PoseidonServer \
+		${BUILD_WRKSRC}/apps/tools/Studio/PoseidonStudio \
+		${BUILD_WRKSRC}/apps/tetris/Tetris/PoseidonTetris \
+		${BUILD_WRKSRC}/apps/tools/Tools/PoseidonTools \
+		${STAGEDIR}${PREFIX}/bin
+	${INSTALL_SCRIPT} ${FILESDIR}/install-cwr-data.sh \
+		${STAGEDIR}${PREFIX}/bin/install-cwr-data
+	${INSTALL_SCRIPT} ${FILESDIR}/cwr-ce.sh \
+		${STAGEDIR}${PREFIX}/bin/cwr-ce
+
+.include <bsd.port.mk>
diff --git a/games/CWR-CE/distinfo b/games/CWR-CE/distinfo
new file mode 100644
index 000000000000..670262e54b4a
--- /dev/null
+++ b/games/CWR-CE/distinfo
@@ -0,0 +1,7 @@
+TIMESTAMP = 1782950316
+SHA256 (ofpisnotdead-com-CWR-CE-v3.01-9abbdf2769f3008d76c47ac0af219388ff0323dc_GH0.tar.gz) = 87df6297ed74993d56d3ceec9d90fb9287cebc58f47030a285a2eafb61c3ce22
+SIZE (ofpisnotdead-com-CWR-CE-v3.01-9abbdf2769f3008d76c47ac0af219388ff0323dc_GH0.tar.gz) = 4944610
+SHA256 (9abbdf2769f3008d76c47ac0af219388ff0323dc..b6c9690f03cf0abb6af577db7294b00c30f4172c.patch) = 8add6877aae7534967c2f609026cd4c4aa861a41f27d6cd68ec89c052e56d31d
+SIZE (9abbdf2769f3008d76c47ac0af219388ff0323dc..b6c9690f03cf0abb6af577db7294b00c30f4172c.patch) = 23346
+SHA256 (b6c9690f03cf0abb6af577db7294b00c30f4172c..85dd554fc9e21312ee52d7f9eced6b171a6a892b.patch) = d87ab832d44d46ae761c8a920463a70b5e8369477a0eda327b4be6995b139e7f
+SIZE (b6c9690f03cf0abb6af577db7294b00c30f4172c..85dd554fc9e21312ee52d7f9eced6b171a6a892b.patch) = 18184
diff --git a/games/CWR-CE/files/cwr-ce.sh b/games/CWR-CE/files/cwr-ce.sh
new file mode 100644
index 000000000000..bb4b232a4a99
--- /dev/null
+++ b/games/CWR-CE/files/cwr-ce.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+# cwr-ce — launcher for PoseidonGame (Arma: Cold War Assault Remastered CE).
+#
+# Resolves the data root (from $CWR_DATA or the default per-user path),
+# verifies a minimal sentinel file exists, then execs PoseidonGame with
+# -C <data-root> and forwards any remaining arguments.
+
+set -u
+
+DATA_ROOT="${CWR_DATA:-${HOME}/.local/share/CWR/base}"
+# The Steam Demo ships bin/CONFIG.BIN (uppercase); an overlay-installed layout
+# may lowercase it. Accept either — the engine itself is case-insensitive.
+if [ -f "${DATA_ROOT}/bin/config.bin" ] || [ -f "${DATA_ROOT}/bin/CONFIG.BIN" ]; then
+	:
+else
+	cat >&2 <<EOF
+cwr-ce: CWR-CE data set not found under: ${DATA_ROOT}
+        (missing: bin/config.bin or bin/CONFIG.BIN)
+
+The engine binaries alone are not enough — you must install a data root
+first. Obtain the Steam "Arma: Cold War Assault Remastered Demo" (free)
+and optionally the retail 2001 game from GOG:
+
+    https://store.steampowered.com/app/4819000/
+    https://www.gog.com/game/arma_cold_war_assault
+
+Then populate the data root with the install helper shipped by this port:
+
+    install-cwr-data --help
+
+By default the helper installs into \$HOME/.local/share/CWR/base. To use a
+different location, set \$CWR_DATA before launching:
+
+    export CWR_DATA=/path/to/data-root
+    cwr-ce
+
+EOF
+	exit 1
+fi
+
+exec PoseidonGame -C "${DATA_ROOT}" "$@"
diff --git a/games/CWR-CE/files/install-cwr-data.sh b/games/CWR-CE/files/install-cwr-data.sh
new file mode 100644
index 000000000000..a20b64bc4b7c
--- /dev/null
+++ b/games/CWR-CE/files/install-cwr-data.sh
@@ -0,0 +1,392 @@
+#!/bin/sh
+# install-cwr-data.sh — populate a CWR-CE data root from:
+# the Steam Demo (default):
+# https://store.steampowered.com/app/4819000/Arma_Cold_War_Assault_Remastered_Demo/
+# and optionally overlay 2001 retail (GOG) content on top:
+# https://www.gog.com/game/arma_cold_war_assault
+#
+# The Poseidon engine reads its data root from $CWD or --work-dir and expects
+# (case-sensitive on POSIX — FindBank/unixPath translates path separators only):
+#   <root>/dta/        (lowercase)
+#   <root>/addons/     (lowercase)
+#   <root>/bin/        (lowercase, holds config.bin/resource.bin/stringtable.csv)
+#   <root>/Campaigns/  (PascalCase)
+#   <root>/Worlds/     <root>/Missions/   <root>/MPMissions/
+#   <root>/Templates/  <root>/SPTemplates/
+#   <root>/fonts/      (cwr_*.ttf — only ships with the Remaster Demo)
+#
+# Per CWR-CE upstream: the binaries are not drop-in compatible with the original
+# 2001 retail folder. The supported layout is "Remaster Demo as base + retail
+# AddOns/DTA/Campaigns/Missions overlaid on top".
+#
+# Config merge (bin/CONFIG.BIN):
+#   The Steam Demo's bin/CONFIG.BIN is the *sole* engine config authority — no
+#   .pbo under dta/ contains a config.bin, they are pure asset archives. The
+#   demo config carries the CWR-CE remaster additions (CfgLandscapeSky, sky
+#   models, fonts, CfgLanguages) that a plain retail BIN/config.bin would
+#   regress, which is why the demo BIN is kept as base and the retail BIN is
+#   NOT overlaid wholesale.
+#
+#   However, the demo config is a strict subset of the retail one:
+#     - CfgVehicles: retail defines ~110 classes the demo omits — Cobra/Mi24/
+#       Mi17 (helicopters), A10 (jet), M1Abrams/T80 (tanks), Skoda/Cessna
+#       (civilians), the SoldierG* resistance roster, reammo boxes, targets,
+#       props, wrecks — everything retail campaigns and missions reference.
+#     - CfgVoice: retail defines Dan/Greg/John/Peter (the classic ARMA:CWA
+#       voice actors); the demo only ships Adam. CfgHQIdentities entries in
+#       the demo config still reference speaker="Dan"/"Greg", so any radio
+#       message crashes on class lookup without the merge.
+#     - CfgGroups: retail group compositions that place the missing vehicles.
+#
+#   With --gog, this script performs a class-level *additive* merge via
+#   'PoseidonTools config merge': base = demo CONFIG.BIN (kept verbatim),
+#   overlay = retail BIN/CONFIG.BIN (adds missing subclasses only, never
+#   overwrites the remaster-specific tweaks). Without this step the engine
+#   crashes the first time a retail mission references an unknown class.
+
+set -eu
+
+usage()
+{
+    cat >&2 <<EOF
+Usage: $0 [options] <demo.zip>
+
+Installs the Steam "Arma: Cold War Assault Remastered Demo" zip into a data
+root the engine can run from. The retail (GOG) game can additionally be
+overlaid on top to enable the full islands and campaign.
+
+Arguments:
+  <demo.zip>            Path to the zipped Steam Demo install folder.
+                        Optional if --gog is provided AND --dest already
+                        holds a previous demo install.
+
+Options:
+  -g, --gog <file>      Path to setup_arma_cold_war_assault_*.exe (GOG retail
+                        installer). Its AddOns/DTA/Campaigns/Missions/... are
+                        overlaid on top of the demo base. Requires innoextract.
+      --dest <dir>      Destination data root (default: \$HOME/.local/share/CWR/base).
+      --force           Overwrite a non-empty destination.
+      --keep-temp       Leave the unpacked staging directory in place.
+  -h, --help            Show this help.
+
+Examples:
+  $0 ~/Arma\\ Cold\\ War\\ Assault\\ Demo.zip
+  $0 ~/demo.zip --gog ~/setup_arma_cold_war_assault_1.0_36421.exe
+  $0 --gog ~/setup.exe --force      # overlay GOG onto an existing demo install
+
+Required tools: unzip (always), innoextract (only when --gog is used).
+
+How to produce <demo.zip> from a Windows machine running Steam:
+  1. In Steam, install "Arma: Cold War Assault Remastered Demo" (free,
+     app 4819000).
+  2. Right-click the title in your Library -> Manage -> Browse local files.
+     That opens the install folder (e.g.
+     C:\\Program Files (x86)\\Steam\\steamapps\\common\\Arma Cold War Assault Demo).
+  3. In Explorer, right-click that folder ->
+     "Send to" -> "Compressed (zipped) folder",
+     or "Compress to" -> "ZIP file" (Windows 11).
+  4. Copy the resulting .zip to this machine (scp, USB stick, ...) and pass
+     its path to this script.
+EOF
+    exit 1
+}
+
+DEST="${HOME}/.local/share/CWR/base"
+FORCE=0
+KEEP_TEMP=0
+DEMO_ZIP=
+GOG_EXE=
+
+while [ $# -gt 0 ]; do
+    case "$1" in
+        -g|--gog)      [ $# -ge 2 ] || usage; GOG_EXE="$2"; shift 2 ;;
+        --dest)        [ $# -ge 2 ] || usage; DEST="$2"; shift 2 ;;
+        --force)       FORCE=1; shift ;;
+        --keep-temp)   KEEP_TEMP=1; shift ;;
+        -h|--help)     usage ;;
+        --)            shift; break ;;
+        -*)            echo "unknown option: $1" >&2; usage ;;
+        *)             [ -z "$DEMO_ZIP" ] || usage; DEMO_ZIP="$1"; shift ;;
+    esac
+done
+
+# Either the demo zip or a pre-existing dest+gog combination must be present.
+if [ -z "$DEMO_ZIP" ] && [ -z "$GOG_EXE" ]; then
+    usage
+fi
+if [ -n "$DEMO_ZIP" ] && [ ! -f "$DEMO_ZIP" ]; then
+    echo "demo zip not found: $DEMO_ZIP" >&2; exit 1
+fi
+if [ -n "$GOG_EXE" ] && [ ! -f "$GOG_EXE" ]; then
+    echo "GOG installer not found: $GOG_EXE" >&2; exit 1
+fi
+
+# Detect the host package manager once and remember the install command.
+# pkg(8) on FreeBSD is checked first because that is the primary platform for
+# this script; the others are best-effort for Linux distros that happen to
+# ship the same tools.
+detect_pkg_install()
+{
+    if command -v pkg >/dev/null 2>&1; then
+        echo "sudo pkg install -y"; return
+    fi
+    if command -v apt-get >/dev/null 2>&1; then
+        echo "sudo apt-get install -y"; return
+    fi
+    if command -v dnf >/dev/null 2>&1; then
+        echo "sudo dnf install -y"; return
+    fi
+    if command -v pacman >/dev/null 2>&1; then
+        echo "sudo pacman -S --noconfirm"; return
+    fi
+    return 1
+}
+
+# Ensure $1 is on PATH. If missing, offer to install it via the host package
+# manager (same package name, which holds for unzip/innoextract on all targets
+# above). Aborts the script if the user declines or the install fails.
+require_tool()
+{
+    name="$1"
+    command -v "$name" >/dev/null 2>&1 && return 0
+
+    install_cmd="$(detect_pkg_install || true)"
+    if [ -z "$install_cmd" ]; then
+        echo "'$name' is required but not found, and no supported package manager" >&2
+        echo "was detected. Install '$name' manually and rerun." >&2
+        exit 1
+    fi
+
+    printf "'%s' is required but missing. Install it with '%s %s'? [Y/n] " \
+        "$name" "$install_cmd" "$name"
+    # Read from the controlling terminal so piped invocations still prompt.
+    if [ -r /dev/tty ]; then
+        read -r reply </dev/tty || reply=n
+    else
+        read -r reply || reply=n
+    fi
+    case "$reply" in
+        ""|y|Y|yes|YES) ;;
+        *) echo "aborted." >&2; exit 1 ;;
+    esac
+
+    # shellcheck disable=SC2086
+    $install_cmd "$name" || {
+        echo "package install failed; aborting." >&2; exit 1; }
+    command -v "$name" >/dev/null 2>&1 || {
+        echo "'$name' still not on PATH after install; aborting." >&2; exit 1; }
+}
+
+# Tool checks scoped to what we actually need this invocation.
+if [ -n "$DEMO_ZIP" ]; then
+    require_tool unzip
+fi
+if [ -n "$GOG_EXE" ]; then
+    require_tool innoextract
+fi
+
+# Non-empty dest is only acceptable when we're explicitly overlaying GOG onto
+# an existing demo, or the caller passed --force.
+if [ "$FORCE" -ne 1 ] && [ -d "$DEST" ] && [ -n "$(ls -A "$DEST" 2>/dev/null || true)" ]; then
+    if [ -n "$DEMO_ZIP" ]; then
+        echo "destination is not empty: $DEST (use --force to overwrite)" >&2
+        exit 1
+    fi
+    # GOG-only run with existing dest: treat as an overlay; require the demo
+    # base to be present, otherwise the engine would still fail on fonts/.
+    if [ ! -d "$DEST/fonts" ] || [ ! -d "$DEST/bin" ]; then
+        echo "destination exists but does not look like a demo install: $DEST" >&2
+        echo "(missing fonts/ or bin/ — install the demo first, or pass <demo.zip>)" >&2
+        exit 1
+    fi
+fi
+mkdir -p "$DEST"
+
+TMP="$(mktemp -d -t cwr-data.XXXXXX)"
+cleanup()
+{
+    if [ "$KEEP_TEMP" -ne 1 ]; then
+        rm -rf "$TMP"
+    else
+        echo "staging left at: $TMP"
+    fi
+}
+trap cleanup EXIT INT TERM
+
+# Locate a directory under $1 case-insensitively (handles both GOG's "DTA"/
+# "AddOns" and the demo's "DTA"/"AddOns" without coupling to either).
+locate_dir()
+{
+    base="$1"; needle="$2"
+    [ -d "$base" ] || return 1
+    lc_needle="$(printf '%s' "$needle" | tr '[:upper:]' '[:lower:]')"
+    for entry in "$base"/*; do
+        [ -d "$entry" ] || continue
+        lc_name="$(printf '%s' "$(basename "$entry")" | tr '[:upper:]' '[:lower:]')"
+        if [ "$lc_name" = "$lc_needle" ]; then
+            printf '%s\n' "$entry"
+            return 0
+        fi
+    done
+    return 1
+}
+
+# Copy a source tree on top of $DEST. Destination name fixed by the caller so
+# we can lowercase DTA/AddOns/BIN regardless of source casing.
+install_dir()
+{
+    src="$1"; dest_name="$2"
+    [ -d "$src" ] || return 0
+    echo "    -> $dest_name/"
+    mkdir -p "$DEST/$dest_name"
+    cp -R "$src/." "$DEST/$dest_name/"
+}
+
+# ---------------------------------------------------------------------------
+# Phase 1: Steam Demo (base)
+# ---------------------------------------------------------------------------
+if [ -n "$DEMO_ZIP" ]; then
+    echo "==> unpacking demo: $(basename "$DEMO_ZIP")"
+    DEMO_STAGE="$TMP/demo"
+    mkdir -p "$DEMO_STAGE"
+    unzip -q "$DEMO_ZIP" -d "$DEMO_STAGE"
+
+    # Steam-shipped zips usually wrap everything in a single top-level folder
+    # ("Arma Cold War Assault Demo/"). If there is exactly one such directory,
+    # descend into it. Otherwise assume the zip was made from inside the dir.
+    DEMO_SRC="$DEMO_STAGE"
+    entries=0
+    only_dir=
+    for e in "$DEMO_STAGE"/*; do
+        entries=$((entries + 1))
+        [ -d "$e" ] && only_dir="$e"
+    done
+    if [ "$entries" -eq 1 ] && [ -n "$only_dir" ]; then
+        DEMO_SRC="$only_dir"
+    fi
+
+    # Lowercase the three engine-literal dirs.
+    for pair in "DTA dta" "AddOns addons" "BIN bin"; do
+        src_name="${pair% *}"; dst_name="${pair#* }"
+        src="$(locate_dir "$DEMO_SRC" "$src_name" || true)"
+        if [ -n "$src" ]; then
+            install_dir "$src" "$dst_name"
+        fi
+    done
+
+    # Strip Windows-only payload from bin/ if present.
+    find "$DEST/bin" -type f \( -iname '*.dll' -o -iname '*.exe' \) -delete 2>/dev/null || true
+
+    # Preserve every other top-level directory verbatim (engine reads each by
+    # exact name). Covers Campaigns/, Worlds/, Missions/, MPMissions/,
+    # Templates/, SPTemplates/, fonts/, demo/, dtaExt/, Music/, …
+    for entry in "$DEMO_SRC"/*; do
+        [ -d "$entry" ] || continue
+        name="$(basename "$entry")"
+        case "$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')" in
+            dta|addons|bin) continue ;;
+        esac
+        install_dir "$entry" "$name"
+    done
+
+    # Loose top-level data files (config.cfg-style metadata). Drop Windows
+    # binaries and license blurbs.
+    for f in "$DEMO_SRC"/*; do
+        [ -f "$f" ] || continue
+        name="$(basename "$f")"
+        case "$name" in
+            *.exe|*.dll|*.url|*.ico|*.pdf|*.txt) continue ;;
+            PoseidonGameDemo|PoseidonGameDemo.*) continue ;;
+        esac
+        echo "    -> $name"
+        cp "$f" "$DEST/$name"
+    done
+fi
+
+# ---------------------------------------------------------------------------
+# Phase 2: GOG retail overlay (optional)
+# ---------------------------------------------------------------------------
+# Per CWR-CE issue #29 the supported merge is: demo as base + retail's
+# AddOns/DTA/Campaigns/Missions/Worlds/Templates overlaid. The retail BIN/ is
+# NOT copied wholesale — it would replace CWR-CE's remaster-aware config
+# (fonts, sky models, CfgLandscapeSky) — but its class definitions ARE merged
+# class-by-class into the demo config, see the header comment and Phase 3.
+if [ -n "$GOG_EXE" ]; then
+    echo "==> extracting GOG installer: $(basename "$GOG_EXE")"
+    GOG_STAGE="$TMP/gog"
+    mkdir -p "$GOG_STAGE"
+    innoextract --silent --gog -d "$GOG_STAGE" "$GOG_EXE"
+
+    # Most GOG bundles drop payload at the extraction root; some create app/
+    # holding only GOG metadata. Pick whichever actually has DTA/AddOns/BIN.
+    GOG_SRC=
+    for candidate in "$GOG_STAGE" "$GOG_STAGE/app"; do
+        for d in DTA dta AddOns addons; do
+            if [ -d "$candidate/$d" ]; then
+                GOG_SRC="$candidate"; break 2
+            fi
+        done
+    done
+    [ -n "$GOG_SRC" ] || { echo "no game data found under $GOG_STAGE" >&2; exit 2; }
+
+    echo "==> overlaying retail content onto $DEST"
+    # Lowercased dirs (overlay merges file-by-file via cp -R).
+    for pair in "DTA dta" "AddOns addons"; do
+        src_name="${pair% *}"; dst_name="${pair#* }"
+        src="$(locate_dir "$GOG_SRC" "$src_name" || true)"
+        if [ -n "$src" ]; then
+            install_dir "$src" "$dst_name"
+        fi
+    done
+
+    # PascalCase content dirs — engine reads literal names. NOT BIN/ — that
+    # dir is handled by the config-merge phase below, not a plain cp.
+    for d in Campaigns Worlds Missions MPMissions Templates SPTemplates; do
+        src="$(locate_dir "$GOG_SRC" "$d" || true)"
+        [ -n "$src" ] || continue
+        name="$(basename "$src")"
+        install_dir "$src" "$name"
+    done
+
+    # -----------------------------------------------------------------------
+    # Phase 3: merge retail CfgVehicles/CfgVoice/CfgGroups into demo CONFIG.BIN
+    # -----------------------------------------------------------------------
+    # 'PoseidonTools config merge base overlay -o out' walks the overlay
+    # class tree and *adds* any subclass the base lacks, without touching
+    # entries the base already defines. That gives us:
+    #   - all retail vehicles, weapons, ammo the campaigns/missions need,
+    #   - the Dan/Greg/John/Peter CfgVoice classes referenced by
+    #     CfgHQIdentities (radio messages crash without them),
+    # while preserving CWR-CE's remaster-only additions (CfgLandscapeSky,
+    # CfgLanguages, remaster sky/font entries) that a wholesale copy of the
+    # retail config would silently drop.
+    GOG_BIN_SRC="$(locate_dir "$GOG_SRC" "BIN" || true)"
+    if [ -n "$GOG_BIN_SRC" ] && [ -f "$GOG_BIN_SRC/CONFIG.BIN" ] \
+        && [ -f "$DEST/bin/CONFIG.BIN" ] \
+        && command -v PoseidonTools >/dev/null 2>&1; then
+        echo "==> merging retail class definitions into demo CONFIG.BIN"
+        cp "$DEST/bin/CONFIG.BIN" "$DEST/bin/CONFIG.BIN.demo-orig"
+        PoseidonTools config merge \
+            "$DEST/bin/CONFIG.BIN.demo-orig" \
+            "$GOG_BIN_SRC/CONFIG.BIN" \
+            -o "$DEST/bin/CONFIG.BIN"
+    elif [ -n "$GOG_BIN_SRC" ] && [ -f "$GOG_BIN_SRC/CONFIG.BIN" ]; then
+        echo "WARNING: PoseidonTools not on PATH; skipping config merge." >&2
+        echo "  Retail missions will crash on missing vehicle/voice classes" >&2
+        echo "  until you run:" >&2
+        echo "    PoseidonTools config merge $DEST/bin/CONFIG.BIN \\" >&2
+        echo "        $GOG_BIN_SRC/CONFIG.BIN -o $DEST/bin/CONFIG.BIN.new" >&2
+    fi
+fi
+
+cat <<EOF
+
+Done. Data root: $DEST
+
+Run the game from that directory:
+    cd "$DEST" && PoseidonGame
+or pass it via --work-dir:
+    PoseidonGame --work-dir "$DEST"
+    PoseidonServer --work-dir "$DEST"
+
+EOF
diff --git a/games/CWR-CE/files/pkg-message.in b/games/CWR-CE/files/pkg-message.in
new file mode 100644
index 000000000000..0d8e83c92184
--- /dev/null
+++ b/games/CWR-CE/files/pkg-message.in
@@ -0,0 +1,31 @@
+[
+{ type: install
+  message: <<EOM
+CWR-CE installs the engine binaries (PoseidonGame, PoseidonServer,
+PoseidonStudio, PoseidonTools, PoseidonTetris, PoseidonGameDemo) but
+no game data. The original "Arma: Cold War Assault" assets are required
+to run anything beyond Tetris and are NOT redistributable.
+
+If you own the game (e.g. via GOG), install the data with:
+
+    pkg install innoextract
+    install-cwr-data /path/to/setup_arma_cold_war_assault_*.exe
+
+This unpacks DTA/, AddOns/, BIN/, Campaigns/, Worlds/, ... into
+~/.local/share/CWR/base/ in the layout the engine expects. Pass
+--dest <dir> to install elsewhere, or --help for the full usage.
+
+Then launch the game with:
+
+    cwr-ce
+
+The launcher resolves the data root and passes it to PoseidonGame.
+If you installed the data elsewhere, set $CWR_DATA before launching:
+
+    CWR_DATA=/path/to/data-root cwr-ce
+
+Any extra arguments are forwarded to PoseidonGame (see --help).
+
+EOM
+}
+]
diff --git a/games/CWR-CE/pkg-descr b/games/CWR-CE/pkg-descr
new file mode 100644
index 000000000000..21da95fb12b0
--- /dev/null
+++ b/games/CWR-CE/pkg-descr
@@ -0,0 +1,15 @@
+Arma: Cold War Assault - Remastered (codename Poseidon) is the engine
+and game source code first released in 2001 as Operation Flashpoint:
+Cold War Crisis. It is the codebase that launched Bohemia Interactive
+and began the technology lineage later grown into Real Virtuality, Arma,
+and Enfusion. The code has been modernized to C++20, built with CMake
+and Clang.
+
+Game assets (models, textures, sounds, missions, voices) are NOT
+included with this port and are NOT GPL. They ship separately under the
+Arma Public License Share Alike (APL-SA).
+A free Demo is available on Steam:
+https://store.steampowered.com/app/4819000
+And this game was tested with the full GOG version game assets too.
+
+Without game data the binaries do nothing useful at runtime.
diff --git a/games/Makefile b/games/Makefile
index 4e6bf3dd3931..79d0c15f6a12 100644
--- a/games/Makefile
+++ b/games/Makefile
@@ -11,6 +11,7 @@
     SUBDIR += 90secondportraits
     SUBDIR += Bugdom
     SUBDIR += Bugdom2
+    SUBDIR += CWR-CE
     SUBDIR += CaribbeanStud
     SUBDIR += DDNet
     SUBDIR += Doom64EX