diff --git a/.gitignore b/.gitignore index 1c9c525ae12..28e1b849e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,11 @@ thumbs.db .clang-format /.vscode /Dependencies/MaxSDK/maxsdk +/tmp/ + +## AI assistant config (user-specific) +/.claude/ +/CLAUDE.md ## IntelliJ, CLion, etc. /.idea diff --git a/README.md b/README.md index 27149141ca0..84093136b79 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,23 @@ report bugs, and contribute to the project! ## Building the Game Yourself -We provide support for building the project using Visual Studio 6 (VS6) and Visual Studio 2022. For detailed build -instructions, check the [Wiki](https://github.com/TheSuperHackers/GeneralsGameCode/wiki/build_guides), which also -includes guides for building with Docker, CLion, and links to forks supporting additional versions. +We provide support for building the project on Windows and Linux. For detailed build instructions, check the +[Wiki](https://github.com/TheSuperHackers/GeneralsGameCode/wiki/build_guides), which includes guides for VS6, VS2022, +Docker, CLion, and links to forks supporting additional versions. + +### Quick Start + +**Windows (Visual Studio 2022)** +```bash +cmake --preset win32 +cmake --build build/win32 --config Release +``` + +**Linux (via Docker)** +```bash +./scripts/docker-build.sh # Build using Docker +./scripts/docker-install.sh --detect # Install to your game +``` ### Dependency management diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh new file mode 100755 index 00000000000..46acd855bdf --- /dev/null +++ b/scripts/docker-build.sh @@ -0,0 +1,274 @@ +#!/usr/bin/env bash +# +# Build script for compiling Generals/Zero Hour on Linux using Docker +# +# This script builds Windows executables using a Docker container with Wine and VC6. +# The resulting binaries can be run on Linux using Wine or on Windows natively. +# +# Usage: +# ./scripts/docker-build.sh # Full build (both games) +# ./scripts/docker-build.sh --game zh # Build Zero Hour only +# ./scripts/docker-build.sh --game generals # Build Generals only +# ./scripts/docker-build.sh --target generalszh # Build specific target +# ./scripts/docker-build.sh --clean # Clean build directory +# ./scripts/docker-build.sh --interactive # Enter container shell +# + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BUILD_DIR="$PROJECT_DIR/build/docker" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_header() { + echo -e "${BLUE}======================================${NC}" + echo -e "${BLUE} Generals Game Code - Linux Builder${NC}" + echo -e "${BLUE}======================================${NC}" + echo "" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +usage() { + cat </dev/null; then + print_error "Docker is not installed. Please install Docker first." + echo " See: https://docs.docker.com/engine/install/" + exit 1 + fi + + if ! docker info &>/dev/null; then + print_error "Docker daemon is not running or you don't have permission." + echo " Try: sudo systemctl start docker" + echo " Or add your user to the docker group: sudo usermod -aG docker \$USER" + exit 1 + fi + + print_success "All dependencies satisfied" +} + +build_docker_image() { + print_info "Building Docker image (this may take a while on first run)..." + + docker build \ + --build-arg UID="$(id -u)" \ + --build-arg GID="$(id -g)" \ + "$PROJECT_DIR/resources/dockerbuild" \ + -t zerohour-build \ + || { + print_error "Failed to build Docker image" + exit 1 + } + + print_success "Docker image ready" +} + +run_build() { + local force_cmake="$1" + local target="$2" + local interactive="$3" + + local docker_flags="" + if [[ "$interactive" == "true" ]]; then + docker_flags="-it --entrypoint bash" + fi + + print_info "Starting build..." + if [[ -n "$target" ]]; then + print_info "Target: $target" + fi + + # shellcheck disable=SC2086 + docker run \ + -u "$(id -u):$(id -g)" \ + -e MAKE_TARGET="$target" \ + -e FORCE_CMAKE="$force_cmake" \ + -v "$PROJECT_DIR:/build/cnc" \ + --rm \ + $docker_flags \ + zerohour-build \ + || { + print_error "Build failed" + exit 1 + } + + if [[ "$interactive" != "true" ]]; then + print_success "Build completed" + fi +} + +clean_build() { + print_info "Cleaning build directory..." + rm -rf "$BUILD_DIR" + print_success "Build directory cleaned" +} + +list_outputs() { + echo "" + print_info "Build outputs:" + echo "" + + if [[ -d "$BUILD_DIR/GeneralsMD" ]]; then + echo "Zero Hour (GeneralsMD):" + find "$BUILD_DIR/GeneralsMD" -maxdepth 1 -name "*.exe" -printf " %f (%s bytes)\n" 2>/dev/null || true + fi + + if [[ -d "$BUILD_DIR/Generals" ]]; then + echo "" + echo "Generals:" + find "$BUILD_DIR/Generals" -maxdepth 1 -name "*.exe" -printf " %f (%s bytes)\n" 2>/dev/null || true + fi +} + +# Parse arguments +GAME="all" +TARGET="" +CLEAN=false +INTERACTIVE=false +FORCE_CMAKE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + -g|--game) + GAME="$2" + shift 2 + ;; + -t|--target) + TARGET="$2" + shift 2 + ;; + -c|--clean) + CLEAN=true + shift + ;; + -i|--interactive) + INTERACTIVE=true + shift + ;; + --cmake) + FORCE_CMAKE=true + shift + ;; + -j|--jobs) + # Jobs are handled by ninja in the container + shift 2 + ;; + -*) + print_error "Unknown option: $1" + usage + exit 1 + ;; + *) + # Positional argument treated as target + TARGET="$TARGET $1" + shift + ;; + esac +done + +# Set target based on game selection +if [[ -z "$TARGET" ]]; then + case "$GAME" in + zh|zerohour) + TARGET="generalszh generalszh_tools" + ;; + generals) + TARGET="generalsv generalsv_tools" + ;; + all) + TARGET="" # Build all + ;; + *) + print_error "Unknown game: $GAME (use 'zh', 'generals', or 'all')" + exit 1 + ;; + esac +fi + +# Main execution +print_header +check_dependencies + +if [[ "$CLEAN" == "true" ]]; then + clean_build +fi + +build_docker_image +run_build "$FORCE_CMAKE" "$TARGET" "$INTERACTIVE" + +if [[ "$INTERACTIVE" != "true" ]]; then + list_outputs + + echo "" + print_info "To run the game with Wine:" + echo " wine $BUILD_DIR/GeneralsMD/generalszh.exe" +fi diff --git a/scripts/docker-install.sh b/scripts/docker-install.sh new file mode 100755 index 00000000000..12135de56c8 --- /dev/null +++ b/scripts/docker-install.sh @@ -0,0 +1,405 @@ +#!/usr/bin/env bash +# +# Install built executables to an existing Generals/Zero Hour installation +# +# This script copies the built executables and tools to a game installation, +# allowing you to test your compiled version with the full game data. +# +# Usage: +# ./scripts/docker-install.sh /path/to/game/installation +# ./scripts/docker-install.sh --detect # Auto-detect game location +# ./scripts/docker-install.sh --restore # Restore original files +# + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BUILD_DIR="$PROJECT_DIR/build/docker" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +print_error() { echo -e "${RED}[ERROR]${NC} $1"; } +print_info() { echo -e "${BLUE}[INFO]${NC} $1"; } + +usage() { + cat </dev/null; then + for key in \ + "HKLM\\SOFTWARE\\Electronic Arts\\EA Games\\Command and Conquer Generals Zero Hour" \ + "HKLM\\SOFTWARE\\WOW6432Node\\Electronic Arts\\EA Games\\Command and Conquer Generals Zero Hour"; do + local install_path + install_path=$(reg.exe query "$key" //v "InstallPath" 2>/dev/null | grep -oP 'REG_SZ\s+\K.*' | tr -d '\r') + if [ -n "$install_path" ] && [ -d "$install_path/Data" ]; then + candidates+=("$install_path") + fi + done + fi + + # Check Wine registry (for Linux with Wine) + for reg_file in ~/.wine/system.reg ~/.wine32/system.reg; do + if [ -f "$reg_file" ]; then + local install_path + install_path=$(grep -A1 '\[Software\\\\Electronic Arts\\\\EA Games\\\\Command and Conquer Generals Zero Hour\]' "$reg_file" 2>/dev/null | grep -oP '"InstallPath"="\K[^"]+' | sed 's/\\\\x0//g; s/\\\\/\//g; s/^C:/~\/.wine\/drive_c/') + if [ -n "$install_path" ] && [ -d "$install_path/Data" ]; then + candidates+=("$install_path") + fi + fi + done + + # Fallback: Common Linux Wine locations + for prefix in ~/.wine ~/.wine32 ~/.wine-*; do + if [ -d "$prefix" ]; then + for path in \ + "$prefix/drive_c/Program Files/EA Games/Command and Conquer Generals Zero Hour" \ + "$prefix/drive_c/Program Files (x86)/EA Games/Command and Conquer Generals Zero Hour" \ + "$prefix/drive_c/Program Files/DODI-Repacks/Generals Zero Hour" \ + "$prefix/drive_c/GOG Games/Command Conquer Generals Zero Hour"; do + if [ -d "$path/Data" ]; then + candidates+=("$path") + fi + done + fi + done + + # Steam Proton prefixes (for games run through Proton) + for prefix in ~/.local/share/Steam/steamapps/compatdata/*/pfx; do + if [ -d "$prefix" ]; then + for path in \ + "$prefix/drive_c/Program Files/EA Games/Command and Conquer Generals Zero Hour" \ + "$prefix/drive_c/Program Files (x86)/EA Games/Command and Conquer Generals Zero Hour"; do + if [ -d "$path/Data" ]; then + candidates+=("$path") + fi + done + fi + done + + # Steam native game installations (steamapps/common) + for path in \ + ~/.local/share/Steam/steamapps/common/"Command and Conquer Generals Zero Hour" \ + ~/.local/share/Steam/steamapps/common/"Command & Conquer Generals - Zero Hour" \ + ~/.steam/steam/steamapps/common/"Command and Conquer Generals Zero Hour" \ + ~/.steam/steam/steamapps/common/"Command & Conquer Generals - Zero Hour"; do + if [ -d "$path/Data" ]; then + candidates+=("$path") + fi + done + + # Fallback: Windows paths (for Git Bash/WSL) + for path in \ + "/c/Program Files/EA Games/Command and Conquer Generals Zero Hour" \ + "/c/Program Files (x86)/EA Games/Command and Conquer Generals Zero Hour" \ + "C:/Program Files/EA Games/Command and Conquer Generals Zero Hour" \ + "C:/Program Files (x86)/EA Games/Command and Conquer Generals Zero Hour"; do + if [ -d "$path/Data" ]; then + candidates+=("$path") + fi + done + + if [ ${#candidates[@]} -eq 0 ]; then + print_error "No game installation found" >&2 + echo "" >&2 + echo "Please specify the game directory manually:" >&2 + echo " $(basename "$0") /path/to/game/installation" >&2 + exit 1 + fi + + if [ ${#candidates[@]} -eq 1 ]; then + echo "${candidates[0]}" + return + fi + + echo "Found multiple installations:" >&2 + for i in "${!candidates[@]}"; do + echo " $((i+1)). ${candidates[$i]}" >&2 + done + echo "" >&2 + read -p "Select installation (1-${#candidates[@]}): " selection >&2 + if [[ ! "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt "${#candidates[@]}" ]; then + print_error "Invalid selection" >&2 + exit 1 + fi + echo "${candidates[$((selection-1))]}" +} + +check_build() { + if [ ! -d "$BUILD_DIR" ]; then + print_error "Build directory not found: $BUILD_DIR" + echo "" + echo "Please build the project first:" + echo " ./scripts/docker-build.sh" + exit 1 + fi + + if [ ! -f "$BUILD_DIR/GeneralsMD/generalszh.exe" ]; then + print_error "Built executables not found" + echo "" + echo "Please build the project first:" + echo " ./scripts/docker-build.sh" + exit 1 + fi + + print_success "Build directory found" +} + +backup_file() { + local file="$1" + local backup="${file}.backup" + + if [ -f "$file" ] && [ ! -f "$backup" ]; then + cp "$file" "$backup" + print_info "Backed up: $(basename "$file")" + fi +} + +install_file() { + local src="$1" + local dest="$2" + local dry_run="${3:-false}" + + if [ "$dry_run" == "true" ]; then + echo " Would copy: $(basename "$src") -> $dest" + else + backup_file "$dest" + cp "$src" "$dest" + print_success "Installed: $(basename "$src")" + fi +} + +install_game() { + local game_dir="$1" + local game_type="$2" # "zh" or "generals" + local dry_run="${3:-false}" + + local data_dir="$game_dir/Data" + local build_game_dir + local exe_name + local tools_prefix + + if [ "$game_type" == "zh" ]; then + build_game_dir="$BUILD_DIR/GeneralsMD" + exe_name="generalszh.exe" + tools_prefix="ZH" + else + build_game_dir="$BUILD_DIR/Generals" + exe_name="generalsv.exe" + tools_prefix="V" + fi + + if [ ! -d "$build_game_dir" ]; then + print_warning "Build for $game_type not found, skipping" + return + fi + + print_info "Installing $game_type executables..." + + # Main game executable + if [ -f "$build_game_dir/$exe_name" ]; then + install_file "$build_game_dir/$exe_name" "$data_dir/$exe_name" "$dry_run" + fi + + # Tools + for tool in WorldBuilder$tools_prefix W3DView$tools_prefix guiedit imagepacker mapcachebuilder wdump; do + local tool_exe="${tool}.exe" + if [ -f "$build_game_dir/$tool_exe" ]; then + install_file "$build_game_dir/$tool_exe" "$data_dir/$tool_exe" "$dry_run" + fi + done + + # DLLs (but NOT mss32.dll or binkw32.dll - keep originals) + for dll in DebugWindow.dll ParticleEditor.dll; do + if [ -f "$build_game_dir/$dll" ]; then + install_file "$build_game_dir/$dll" "$data_dir/$dll" "$dry_run" + fi + done + + # Also check Core directory for DLLs + if [ -f "$BUILD_DIR/Core/DebugWindow.dll" ]; then + install_file "$BUILD_DIR/Core/DebugWindow.dll" "$data_dir/DebugWindow.dll" "$dry_run" + fi +} + +restore_files() { + local game_dir="$1" + local data_dir="$game_dir/Data" + + print_info "Restoring original files..." + + local restored=0 + for backup in "$data_dir"/*.backup; do + if [ -f "$backup" ]; then + local original="${backup%.backup}" + cp "$backup" "$original" + print_success "Restored: $(basename "$original")" + ((restored++)) + fi + done + + if [ $restored -eq 0 ]; then + print_warning "No backup files found to restore" + else + print_success "Restored $restored files" + fi +} + +# Parse arguments +GAME_DIR="" +DETECT=false +RESTORE=false +GAME_TYPE="zh" +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + -d|--detect) + DETECT=true + shift + ;; + -r|--restore) + RESTORE=true + shift + ;; + -g|--game) + GAME_TYPE="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + -*) + print_error "Unknown option: $1" + usage + exit 1 + ;; + *) + GAME_DIR="$1" + shift + ;; + esac +done + +# Main execution +echo "" +echo "===================================" +echo " Generals Game Code - Installer" +echo "===================================" +echo "" + +# Detect or validate game directory +if [ "$DETECT" == "true" ] || [ -z "$GAME_DIR" ]; then + print_info "Searching for game installations..." + GAME_DIR=$(detect_game) +fi + +print_info "Game directory: $GAME_DIR" + +# Validate game directory +if [ ! -d "$GAME_DIR/Data" ]; then + print_error "Invalid game directory (Data/ subdirectory not found)" + echo "" + echo "Expected directory structure:" + echo " $GAME_DIR/" + echo " Data/" + echo " generalszh.exe" + echo " *.big files" + exit 1 +fi + +# Restore mode +if [ "$RESTORE" == "true" ]; then + restore_files "$GAME_DIR" + exit 0 +fi + +# Check build exists +check_build + +# Install +if [ "$DRY_RUN" == "true" ]; then + print_info "Dry run - showing what would be installed:" +fi + +case "$GAME_TYPE" in + zh|zerohour) + install_game "$GAME_DIR" "zh" "$DRY_RUN" + ;; + generals) + install_game "$GAME_DIR" "generals" "$DRY_RUN" + ;; + both|all) + install_game "$GAME_DIR" "zh" "$DRY_RUN" + install_game "$GAME_DIR" "generals" "$DRY_RUN" + ;; + *) + print_error "Unknown game type: $GAME_TYPE" + exit 1 + ;; +esac + +if [ "$DRY_RUN" != "true" ]; then + echo "" + print_success "Installation complete!" + echo "" + echo "To run the game:" + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + echo " wine \"$GAME_DIR/Data/generalszh.exe\"" + else + echo " cd \"$GAME_DIR/Data\" && generalszh.exe" + fi + echo "" + echo "To restore original files:" + echo " $(basename "$0") --restore \"$GAME_DIR\"" +fi diff --git a/scripts/dockerbuild.sh b/scripts/dockerbuild.sh index ec8ae69856f..73e876acfb9 100755 --- a/scripts/dockerbuild.sh +++ b/scripts/dockerbuild.sh @@ -1,5 +1,15 @@ -#!/bin/bash #!/usr/bin/env bash +# +# Legacy Docker build script - consider using docker-build.sh instead +# +# docker-build.sh provides a more user-friendly interface with: +# - Game selection (--game zh/generals/all) +# - Colored output and progress messages +# - Target selection (--target) +# - Clean build option (--clean) +# +# This script is kept for backwards compatibility. +# set -euo pipefail