diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..5908573b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,746 @@ +name: Build Release + +on: + release: + types: [published] + push: + tags: + - 'v*' + +jobs: + build-linux: + name: Build Linux AppImage + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + libsdl2-dev \ + libsdl2-image-dev \ + wget \ + curl \ + file \ + fuse + + - name: Build SDLPoP + working-directory: src + run: | + make clean + make all + + - name: Verify build output + run: | + if [ ! -f prince ]; then + echo "Error: prince binary not found in root directory" + echo "Contents of root directory:" + ls -la + echo "Contents of src directory:" + ls -la src/ + exit 1 + fi + echo "✓ Build successful: prince binary found" + file prince + + - name: Download linuxdeploy + run: | + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + - name: Download linuxdeploy SDL2 plugin + run: | + # Try to download the plugin with curl (better redirect handling) + # If it fails, we'll use linuxdeploy's built-in SDL2 detection + if curl -L -f -o linuxdeploy-plugin-sdl2-x86_64.AppImage https://github.com/linuxdeploy/linuxdeploy-plugin-sdl2/releases/download/continuous/linuxdeploy-plugin-sdl2-x86_64.AppImage; then + chmod +x linuxdeploy-plugin-sdl2-x86_64.AppImage + echo "✓ SDL2 plugin downloaded successfully" + echo "PLUGIN_AVAILABLE=true" >> $GITHUB_ENV + else + echo "⚠ SDL2 plugin not available at continuous release" + echo "⚠ Trying alternative: latest release..." + # Try latest release instead of continuous + if curl -L -f -o linuxdeploy-plugin-sdl2-x86_64.AppImage https://github.com/linuxdeploy/linuxdeploy-plugin-sdl2/releases/latest/download/linuxdeploy-plugin-sdl2-x86_64.AppImage; then + chmod +x linuxdeploy-plugin-sdl2-x86_64.AppImage + echo "✓ SDL2 plugin downloaded from latest release" + echo "PLUGIN_AVAILABLE=true" >> $GITHUB_ENV + else + echo "⚠ SDL2 plugin not available, linuxdeploy will use built-in SDL2 detection" + echo "PLUGIN_AVAILABLE=false" >> $GITHUB_ENV + fi + fi + + - name: Create AppDir structure + run: | + mkdir -p AppDir/usr/bin + mkdir -p AppDir/usr/share/applications + mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps + + # Copy binary (Makefile outputs to ../prince, which is root) + if [ ! -f prince ]; then + echo "Error: prince binary not found" + exit 1 + fi + cp prince AppDir/usr/bin/ + chmod +x AppDir/usr/bin/prince + + # Copy data directory + if [ ! -d data ]; then + echo "Error: data directory not found" + exit 1 + fi + cp -r data AppDir/usr/bin/ + + # Copy config file (optional) + if [ -f SDLPoP.ini ]; then + cp SDLPoP.ini AppDir/usr/bin/ + else + echo "# SDLPoP Configuration" > AppDir/usr/bin/SDLPoP.ini + fi + + # Copy README (optional) + if [ -f doc/Readme.txt ]; then + cp doc/Readme.txt AppDir/usr/bin/README.txt + else + echo "SDLPoP" > AppDir/usr/bin/README.txt + fi + + echo "✓ AppDir structure created successfully" + + - name: Create desktop file + run: | + cat > AppDir/usr/share/applications/sdlpop.desktop << 'EOF' + [Desktop Entry] + Type=Application + Name=SDLPoP + Comment=Prince of Persia port + Exec=prince + Icon=sdlpop + Categories=Game; + EOF + + - name: Copy icon (if exists) + run: | + if [ -f data/icon.png ]; then + cp data/icon.png AppDir/usr/share/icons/hicolor/256x256/apps/sdlpop.png + fi + + - name: Create AppImage + env: + VERSION: ${{ github.ref_name }} + run: | + export VERSION=${VERSION#v} # Remove 'v' prefix if present + export ARCH=x86_64 + + # Build linuxdeploy command + LINUXDEPLOY_CMD="./linuxdeploy-x86_64.AppImage \ + --appdir AppDir \ + --executable AppDir/usr/bin/prince \ + --desktop-file AppDir/usr/share/applications/sdlpop.desktop \ + --icon-file AppDir/usr/share/icons/hicolor/256x256/apps/sdlpop.png" + + # Add plugin flag only if plugin is available + if [ "$PLUGIN_AVAILABLE" = "true" ] && [ -f linuxdeploy-plugin-sdl2-x86_64.AppImage ]; then + LINUXDEPLOY_CMD="$LINUXDEPLOY_CMD --plugin sdl2" + echo "Using SDL2 plugin" + else + echo "Using linuxdeploy built-in SDL2 detection" + # Manually copy SDL2 libraries if plugin is not available + if [ -f /usr/lib/x86_64-linux-gnu/libSDL2.so ]; then + mkdir -p AppDir/usr/lib + cp /usr/lib/x86_64-linux-gnu/libSDL2*.so* AppDir/usr/lib/ 2>/dev/null || true + cp /usr/lib/x86_64-linux-gnu/libSDL2_image*.so* AppDir/usr/lib/ 2>/dev/null || true + echo "Copied SDL2 libraries manually" + fi + fi + + LINUXDEPLOY_CMD="$LINUXDEPLOY_CMD --output appimage" + + # Execute linuxdeploy + eval $LINUXDEPLOY_CMD + + # Find the created AppImage and rename if needed + APPIMAGE=$(ls SDLPoP-*.AppImage 2>/dev/null | head -n 1) + TARGET_NAME="SDLPoP-${VERSION}-x86_64.AppImage" + + if [ -z "$APPIMAGE" ]; then + echo "Error: No AppImage file found" + exit 1 + fi + + if [ "$APPIMAGE" != "$TARGET_NAME" ]; then + echo "Renaming $APPIMAGE to $TARGET_NAME" + mv "$APPIMAGE" "$TARGET_NAME" + else + echo "AppImage already has correct name: $TARGET_NAME" + fi + + # Verify the final file exists + if [ ! -f "$TARGET_NAME" ]; then + echo "Error: Final AppImage file not found: $TARGET_NAME" + exit 1 + fi + echo "✓ Created $TARGET_NAME" + ls -lh "$TARGET_NAME" + + # Verify that SDL2 libraries are bundled + echo "Verifying bundled libraries..." + ./"$TARGET_NAME" --appimage-extract 2>/dev/null || true + if [ -d "squashfs-root" ]; then + echo "Checking for SDL2 libraries in AppImage..." + find squashfs-root/usr/lib -name "*SDL2*" -type f 2>/dev/null | head -10 || echo "Note: Libraries may be in different location" + rm -rf squashfs-root + fi + + - name: Upload AppImage artifact + uses: actions/upload-artifact@v4 + with: + name: SDLPoP-Linux-AppImage + path: SDLPoP-*.AppImage + retention-days: 30 + + build-windows: + name: Build Windows x86_64 + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install MSYS2 and dependencies + uses: msys2/setup-msys2@v2 + with: + update: true + install: >- + mingw-w64-x86_64-gcc + mingw-w64-x86_64-SDL2 + mingw-w64-x86_64-SDL2_image + make + zip + + - name: Build SDLPoP + shell: msys2 {0} + working-directory: src + run: | + export SDL2=/mingw64 + export SDL2INC=/mingw64/include/SDL2 + export SDL2LIB=/mingw64/lib + export PATH=/mingw64/bin:$PATH + + # Compile all source files + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c main.c -o main.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c data.c -o data.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg000.c -o seg000.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg001.c -o seg001.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg002.c -o seg002.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg003.c -o seg003.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg004.c -o seg004.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg005.c -o seg005.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg006.c -o seg006.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg007.c -o seg007.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg008.c -o seg008.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seg009.c -o seg009.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c seqtbl.c -o seqtbl.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c replay.c -o replay.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c options.c -o options.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c lighting.c -o lighting.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c screenshot.c -o screenshot.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c menu.c -o menu.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c midi.c -o midi.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c opl3.c -o opl3.o + gcc -Wall -std=c99 -O3 -ffast-math -I"$SDL2INC" -I"$SDL2INC/../include" -c stb_vorbis.c -o stb_vorbis.o + + # Link + gcc -mwindows main.o data.o seg000.o seg001.o seg002.o seg003.o seg004.o seg005.o seg006.o seg007.o seg008.o seg009.o seqtbl.o replay.o options.o lighting.o screenshot.o menu.o midi.o opl3.o stb_vorbis.o -L"$SDL2LIB" -Wl,--whole-archive -lSDL2main -Wl,--no-whole-archive -lSDL2 -lSDL2_image -lm -o ../prince.exe + + - name: Verify build output + shell: msys2 {0} + run: | + if [ ! -f prince.exe ]; then + echo "Error: prince.exe not found in root directory" + echo "Contents of root directory:" + ls -la + echo "Contents of src directory:" + ls -la src/ + exit 1 + fi + echo "✓ Build successful: prince.exe found" + ls -lh prince.exe + + - name: Create release package + shell: msys2 {0} + working-directory: . + run: | + version="${{ github.ref_name }}" + version=${version#v} # Remove 'v' prefix if present + zipName="SDLPoP-${version}-Windows-x86_64.zip" + + # Create release directory + mkdir -p release + + # Verify and copy executable + if [ ! -f prince.exe ]; then + echo "Error: prince.exe not found" + exit 1 + fi + cp prince.exe release/ + echo "✓ Copied prince.exe" + + # Verify and copy DLLs from MSYS2 installation + if [ ! -f /mingw64/bin/SDL2.dll ]; then + echo "Error: SDL2.dll not found at /mingw64/bin/SDL2.dll" + echo "Searching for SDL2.dll..." + find /mingw64 -name "SDL2.dll" 2>/dev/null || true + exit 1 + fi + if [ ! -f /mingw64/bin/SDL2_image.dll ]; then + echo "Error: SDL2_image.dll not found at /mingw64/bin/SDL2_image.dll" + echo "Searching for SDL2_image.dll..." + find /mingw64 -name "SDL2_image.dll" 2>/dev/null || true + exit 1 + fi + cp /mingw64/bin/SDL2.dll release/ + cp /mingw64/bin/SDL2_image.dll release/ + echo "✓ Copied SDL2 DLLs" + + # Verify and copy data directory + if [ ! -d data ]; then + echo "Error: data directory not found" + exit 1 + fi + cp -r data release/ + echo "✓ Copied data directory" + + # Copy config file (optional) + if [ -f SDLPoP.ini ]; then + cp SDLPoP.ini release/ + echo "✓ Copied SDLPoP.ini" + else + echo "# SDLPoP Configuration" > release/SDLPoP.ini + echo "✓ Created default SDLPoP.ini" + fi + + # Copy README (optional) + if [ -f doc/Readme.txt ]; then + cp doc/Readme.txt release/README.txt + echo "✓ Copied README.txt" + fi + + # Create zip using zip command (avoids PowerShell nesting issues) + cd release + zip -r "../$zipName" . -q + cd .. + + if [ ! -f "$zipName" ]; then + echo "Error: Failed to create zip file" + exit 1 + fi + + echo "✓ Created $zipName" + ls -lh "$zipName" + + - name: Upload Windows zip artifact + uses: actions/upload-artifact@v4 + with: + name: SDLPoP-Windows-x86_64 + path: SDLPoP-*.zip + retention-days: 30 + + build-linux-arm64: + name: Build Linux ARM64 + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies and cross-compiler + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + pkg-config \ + wget \ + file \ + fuse \ + autoconf \ + automake \ + libtool \ + cmake \ + ninja-build + + - name: Build SDL2 from source for ARM64 + run: | + # Download and build SDL2 for ARM64 + cd /tmp + wget -q https://github.com/libsdl-org/SDL/releases/download/release-2.30.0/SDL2-2.30.0.tar.gz + tar -xzf SDL2-2.30.0.tar.gz + cd SDL2-2.30.0 + export CC=aarch64-linux-gnu-gcc + export CXX=aarch64-linux-gnu-g++ + # Unset PKG_CONFIG to prevent it from trying to find packages + unset PKG_CONFIG + unset PKG_CONFIG_PATH + ./configure --host=aarch64-linux-gnu --prefix=/usr/aarch64-linux-gnu --disable-shared --enable-static --without-x --disable-video-x11 + make -j$(nproc) + sudo make install + + # Download and build SDL2_image for ARM64 + wget -q https://github.com/libsdl-org/SDL_image/releases/download/release-2.8.2/SDL2_image-2.8.2.tar.gz + tar -xzf SDL2_image-2.8.2.tar.gz + cd SDL2_image-2.8.2 + export CC=aarch64-linux-gnu-gcc + export CXX=aarch64-linux-gnu-g++ + # Unset PKG_CONFIG to prevent it from trying to find packages + unset PKG_CONFIG + unset PKG_CONFIG_PATH + export SDL2_CFLAGS="-I/usr/aarch64-linux-gnu/include/SDL2" + export SDL2_LIBS="-L/usr/aarch64-linux-gnu/lib -lSDL2" + ./configure --host=aarch64-linux-gnu --prefix=/usr/aarch64-linux-gnu --with-sdl-prefix=/usr/aarch64-linux-gnu --disable-shared --enable-static --without-x --disable-png-shared --disable-jpg-shared --disable-tif-shared --disable-webp-shared + make -j$(nproc) + sudo make install + + - name: Build SDLPoP (ARM64) + working-directory: src + run: | + # Set up cross-compilation paths + export CC=aarch64-linux-gnu-gcc + export PKG_CONFIG_PATH=/usr/aarch64-linux-gnu/lib/pkgconfig + export SDL2_CFLAGS="-I/usr/aarch64-linux-gnu/include/SDL2" + export SDL2_LIBS="-L/usr/aarch64-linux-gnu/lib -lSDL2 -lSDL2_image" + + # Compile all source files (ARM64) + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c main.c -o main.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c data.c -o data.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg000.c -o seg000.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg001.c -o seg001.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg002.c -o seg002.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg003.c -o seg003.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg004.c -o seg004.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg005.c -o seg005.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg006.c -o seg006.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg007.c -o seg007.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg008.c -o seg008.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg009.c -o seg009.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seqtbl.c -o seqtbl.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c replay.c -o replay.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c options.c -o options.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c lighting.c -o lighting.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c screenshot.c -o screenshot.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c menu.c -o menu.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c midi.c -o midi.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c opl3.c -o opl3.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c stb_vorbis.c -o stb_vorbis.o + + # Link (ARM64) + $CC main.o data.o seg000.o seg001.o seg002.o seg003.o seg004.o seg005.o seg006.o seg007.o seg008.o seg009.o seqtbl.o replay.o options.o lighting.o screenshot.o menu.o midi.o opl3.o stb_vorbis.o $SDL2_LIBS -lm -o ../prince + + - name: Verify build output (ARM64) + run: | + if [ ! -f prince ]; then + echo "Error: prince binary not found in root directory" + echo "Contents of root directory:" + ls -la + echo "Contents of src directory:" + ls -la src/ + exit 1 + fi + echo "✓ Build successful: prince binary found" + file prince + # Verify it's ARM64 + if ! file prince | grep -q "ARM aarch64"; then + echo "Warning: Binary might not be ARM64" + fi + + - name: Download linuxdeploy for ARM64 + run: | + # Note: linuxdeploy may not have ARM64 AppImage, so we'll create a tarball instead + echo "Creating ARM64 tarball package" + + - name: Create ARM64 package + run: | + version="${{ github.ref_name }}" + version=${version#v} # Remove 'v' prefix if present + packageName="SDLPoP-${version}-Linux-ARM64.tar.gz" + + # Create package directory + mkdir -p package/usr/bin + mkdir -p package/usr/share/applications + mkdir -p package/usr/share/icons/hicolor/256x256/apps + + # Copy binary + if [ ! -f prince ]; then + echo "Error: prince binary not found" + exit 1 + fi + cp prince package/usr/bin/ + chmod +x package/usr/bin/prince + + # Copy data directory + if [ ! -d data ]; then + echo "Error: data directory not found" + exit 1 + fi + cp -r data package/usr/bin/ + + # Copy config file (optional) + if [ -f SDLPoP.ini ]; then + cp SDLPoP.ini package/usr/bin/ + else + echo "# SDLPoP Configuration" > package/usr/bin/SDLPoP.ini + fi + + # Copy README (optional) + if [ -f doc/Readme.txt ]; then + cp doc/Readme.txt package/usr/bin/README.txt + else + echo "SDLPoP" > package/usr/bin/README.txt + fi + + # Create desktop file + cat > package/usr/share/applications/sdlpop.desktop << 'EOF' + [Desktop Entry] + Type=Application + Name=SDLPoP + Comment=Prince of Persia port + Exec=prince + Icon=sdlpop + Categories=Game; + EOF + + # Copy icon (if exists) + if [ -f data/icon.png ]; then + cp data/icon.png package/usr/share/icons/hicolor/256x256/apps/sdlpop.png + fi + + # Create tarball + tar -czf "$packageName" -C package . + + if [ ! -f "$packageName" ]; then + echo "Error: Failed to create tarball" + exit 1 + fi + + echo "✓ Created $packageName" + ls -lh "$packageName" + + - name: Upload ARM64 artifact + uses: actions/upload-artifact@v4 + with: + name: SDLPoP-Linux-ARM64 + path: SDLPoP-*-Linux-ARM64.tar.gz + retention-days: 30 + + build-linux-arm32: + name: Build Linux ARM 32-bit (armhf) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies and cross-compiler + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + gcc-arm-linux-gnueabihf \ + g++-arm-linux-gnueabihf \ + pkg-config \ + wget \ + file \ + fuse + + - name: Build SDL2 from source for ARM 32-bit + run: | + # Download and build SDL2 for ARM 32-bit (armhf) + cd /tmp + wget -q https://github.com/libsdl-org/SDL/releases/download/release-2.30.0/SDL2-2.30.0.tar.gz + tar -xzf SDL2-2.30.0.tar.gz + cd SDL2-2.30.0 + ./configure --host=arm-linux-gnueabihf --prefix=/usr/arm-linux-gnueabihf --disable-shared --enable-static + make -j$(nproc) + sudo make install + + # Download and build SDL2_image for ARM 32-bit + wget -q https://github.com/libsdl-org/SDL_image/releases/download/release-2.8.2/SDL2_image-2.8.2.tar.gz + tar -xzf SDL2_image-2.8.2.tar.gz + cd SDL2_image-2.8.2 + ./configure --host=arm-linux-gnueabihf --prefix=/usr/arm-linux-gnueabihf --with-sdl-prefix=/usr/arm-linux-gnueabihf --disable-shared --enable-static + make -j$(nproc) + sudo make install + + - name: Build SDLPoP (ARM 32-bit) + working-directory: src + run: | + # Set up cross-compilation paths + export CC=arm-linux-gnueabihf-gcc + export PKG_CONFIG_PATH=/usr/arm-linux-gnueabihf/lib/pkgconfig + export SDL2_CFLAGS="-I/usr/arm-linux-gnueabihf/include/SDL2" + export SDL2_LIBS="-L/usr/arm-linux-gnueabihf/lib -lSDL2 -lSDL2_image" + + # Compile all source files (ARM 32-bit) + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c main.c -o main.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c data.c -o data.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg000.c -o seg000.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg001.c -o seg001.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg002.c -o seg002.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg003.c -o seg003.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg004.c -o seg004.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg005.c -o seg005.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg006.c -o seg006.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg007.c -o seg007.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg008.c -o seg008.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seg009.c -o seg009.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c seqtbl.c -o seqtbl.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c replay.c -o replay.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c options.c -o options.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c lighting.c -o lighting.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c screenshot.c -o screenshot.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c menu.c -o menu.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c midi.c -o midi.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c opl3.c -o opl3.o + $CC -Wall -std=c99 -O3 -ffast-math $SDL2_CFLAGS -c stb_vorbis.c -o stb_vorbis.o + + # Link (ARM 32-bit) + $CC main.o data.o seg000.o seg001.o seg002.o seg003.o seg004.o seg005.o seg006.o seg007.o seg008.o seg009.o seqtbl.o replay.o options.o lighting.o screenshot.o menu.o midi.o opl3.o stb_vorbis.o $SDL2_LIBS -lm -o ../prince + + - name: Verify build output (ARM 32-bit) + run: | + if [ ! -f prince ]; then + echo "Error: prince binary not found in root directory" + echo "Contents of root directory:" + ls -la + echo "Contents of src directory:" + ls -la src/ + exit 1 + fi + echo "✓ Build successful: prince binary found" + file prince + # Verify it's ARM 32-bit + if ! file prince | grep -q "ARM"; then + echo "Warning: Binary might not be ARM" + fi + + - name: Create ARM 32-bit package + run: | + version="${{ github.ref_name }}" + version=${version#v} # Remove 'v' prefix if present + packageName="SDLPoP-${version}-Linux-ARM32.tar.gz" + + # Create package directory + mkdir -p package/usr/bin + mkdir -p package/usr/share/applications + mkdir -p package/usr/share/icons/hicolor/256x256/apps + + # Copy binary + if [ ! -f prince ]; then + echo "Error: prince binary not found" + exit 1 + fi + cp prince package/usr/bin/ + chmod +x package/usr/bin/prince + + # Copy data directory + if [ ! -d data ]; then + echo "Error: data directory not found" + exit 1 + fi + cp -r data package/usr/bin/ + + # Copy config file (optional) + if [ -f SDLPoP.ini ]; then + cp SDLPoP.ini package/usr/bin/ + else + echo "# SDLPoP Configuration" > package/usr/bin/SDLPoP.ini + fi + + # Copy README (optional) + if [ -f doc/Readme.txt ]; then + cp doc/Readme.txt package/usr/bin/README.txt + else + echo "SDLPoP" > package/usr/bin/README.txt + fi + + # Create desktop file + cat > package/usr/share/applications/sdlpop.desktop << 'EOF' + [Desktop Entry] + Type=Application + Name=SDLPoP + Comment=Prince of Persia port + Exec=prince + Icon=sdlpop + Categories=Game; + EOF + + # Copy icon (if exists) + if [ -f data/icon.png ]; then + cp data/icon.png package/usr/share/icons/hicolor/256x256/apps/sdlpop.png + fi + + # Create tarball + tar -czf "$packageName" -C package . + + if [ ! -f "$packageName" ]; then + echo "Error: Failed to create tarball" + exit 1 + fi + + echo "✓ Created $packageName" + ls -lh "$packageName" + + - name: Upload ARM 32-bit artifact + uses: actions/upload-artifact@v4 + with: + name: SDLPoP-Linux-ARM32 + path: SDLPoP-*-Linux-ARM32.tar.gz + retention-days: 30 + + create-release: + name: Create GitHub Release + needs: [build-linux, build-windows, build-linux-arm64, build-linux-arm32] + runs-on: ubuntu-latest + if: github.event_name == 'release' || startsWith(github.ref, 'refs/tags/') + + steps: + - name: Download Linux x86_64 artifact + uses: actions/download-artifact@v4 + with: + name: SDLPoP-Linux-AppImage + path: artifacts/linux-x86_64 + + - name: Download Windows x86_64 artifact + uses: actions/download-artifact@v4 + with: + name: SDLPoP-Windows-x86_64 + path: artifacts/windows-x86_64 + + - name: Download Linux ARM64 artifact + uses: actions/download-artifact@v4 + with: + name: SDLPoP-Linux-ARM64 + path: artifacts/linux-arm64 + + - name: Download Linux ARM32 artifact + uses: actions/download-artifact@v4 + with: + name: SDLPoP-Linux-ARM32 + path: artifacts/linux-arm32 + + - name: Upload to Release + uses: softprops/action-gh-release@v1 + with: + files: | + artifacts/linux-x86_64/* + artifacts/windows-x86_64/* + artifacts/linux-arm64/* + artifacts/linux-arm32/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.gitignore b/.gitignore index 99e79bce..c70bc8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -59,7 +59,19 @@ src/*_private.* .idea/ src/.idea/ src/cmake-build*/ + +# Build directories (anywhere in the project) build/ +**/build/ +**/build-*/ +**/cmake-build*/ +**/out/ +**/bin/ +**/obj/ +**/Debug/ +**/Release/ +**/x64/ +**/x86/ # Visual Studio .vs/ @@ -88,3 +100,24 @@ screenshots/ # Configuration file generated by the in-game settings menu *.cfg + +# Temporary build tools and installers (specific files) +7z.exe +vs_buildtools.exe +setup_and_build.ps1 +install_build_tools.ps1 +BUILD_STATUS.md + +# Downloaded dependencies +SDL2-*/ +mingw/ +mingw_temp/ +SDL2_temp/ + +# CMake generated files (additional patterns) +**/CMakeCache.txt +**/CMakeFiles/ +**/cmake_install.cmake +**/Makefile +**/*.cmake +!**/CMakeLists.txt diff --git a/MULTIPLAYER_ANALYSIS.md b/MULTIPLAYER_ANALYSIS.md new file mode 100644 index 00000000..4e86068a --- /dev/null +++ b/MULTIPLAYER_ANALYSIS.md @@ -0,0 +1,170 @@ +# Multiplayer 1v1 Implementation Analysis + +## Complexity Assessment: **MODERATE** (3-4 weeks of development) + +## Current Architecture + +### ✅ What's Already in Place: +1. **Character System**: The game already has `Kid` (player) and `Guard` (opponent) as separate `char_type` structures +2. **Dual Character Processing**: The game loop already processes both characters: + - `play_kid_frame()` - handles Kid + - `play_guard_frame()` - handles Guard (currently AI-controlled) +3. **Input System**: SDL2 already supports multiple gamepads/controllers +4. **Control Functions**: The `control()` function works for any character when `Char` is set appropriately +5. **Sword Fighting**: The combat system already supports two characters fighting + +### ❌ What Needs to Be Added: + +## Implementation Requirements + +### 1. **Input System Extension** (Medium Complexity) +- **Current**: Single set of control variables (`control_x`, `control_y`, `control_shift`, etc.) +- **Needed**: Second set for Player 2 (`control_x_p2`, `control_y_p2`, etc.) +- **Location**: `src/data.h` - add new control variables +- **Files to modify**: + - `src/data.h` - add Player 2 control variables + - `src/seg000.c` - `read_keyb_control()` and `read_joyst_control()` functions + - `src/seg009.c` - `process_events()` for second controller detection + +### 2. **Multiplayer Mode Flag** (Low Complexity) +- **Add**: `is_multiplayer_mode` boolean flag +- **Location**: `src/data.h` +- **Usage**: Check this flag to determine if Guard should use AI or Player 2 input + +### 3. **Guard Control Modification** (Medium Complexity) +- **Current**: `play_guard()` calls `autocontrol_opponent()` (AI) +- **Needed**: In multiplayer mode, call `user_control_p2()` instead +- **Location**: `src/seg006.c` - `play_guard()` function +- **Implementation**: + ```c + if (is_multiplayer_mode) { + user_control_p2(); // Player 2 controls Guard + } else { + autocontrol_opponent(); // AI controls Guard + } + ``` + +### 4. **Player 2 Input Function** (Medium Complexity) +- **Create**: `user_control_p2()` function (similar to `user_control()`) +- **Location**: `src/seg006.c` +- **Logic**: + - Set `Char = Guard` (temporarily) + - Read Player 2 controls into the control variables + - Call `control()` + - Restore `Char` + +### 5. **Initialization** (Low-Medium Complexity) +- **Add**: Function to initialize multiplayer mode +- **Location**: `src/seg003.c` - `init_game()` or new function +- **Tasks**: + - Spawn Kid and Guard facing each other + - Set both characters to active state + - Initialize both with swords drawn (optional) + - Set starting positions + +### 6. **Menu Integration** (Low Complexity) +- **Add**: Menu option to select "Multiplayer 1v1" mode +- **Location**: `src/menu.c` (if menu system is used) or title screen +- **Alternative**: Command-line argument like `prince multiplayer` + +### 7. **Controller Assignment** (Low-Medium Complexity) +- **Detect**: Second controller automatically, or allow manual assignment +- **Fallback**: If only one controller, use keyboard for Player 1, controller for Player 2 +- **Location**: `src/seg009.c` - controller initialization + +## Code Changes Summary + +### Files to Modify: + +1. **src/data.h** + - Add Player 2 control variables + - Add `is_multiplayer_mode` flag + +2. **src/seg000.c** + - Modify `read_user_control()` to read Player 2 input + - Add `read_user_control_p2()` function + +3. **src/seg006.c** + - Modify `play_guard()` to check multiplayer mode + - Add `user_control_p2()` function + +4. **src/seg003.c** + - Add multiplayer initialization function + - Modify `init_game()` or `play_level()` to support multiplayer + +5. **src/seg009.c** + - Enhance controller detection for multiple controllers + - Add keyboard mapping for Player 2 (WASD or separate keys) + +6. **src/config.h** (optional) + - Add `USE_MULTIPLAYER` compile-time flag + +7. **src/proto.h** + - Add function declarations for multiplayer functions + +## Estimated Implementation Steps + +### Phase 1: Core Infrastructure (1 week) +1. Add Player 2 control variables to `data.h` +2. Create `read_user_control_p2()` function +3. Add `is_multiplayer_mode` flag +4. Test input reading for both players + +### Phase 2: Guard Control (1 week) +1. Create `user_control_p2()` function +2. Modify `play_guard()` to use Player 2 input in multiplayer mode +3. Test that Guard responds to Player 2 input + +### Phase 3: Initialization & Setup (1 week) +1. Create multiplayer initialization function +2. Set up starting positions (players facing each other) +3. Add menu/command-line option to start multiplayer +4. Handle controller assignment + +### Phase 4: Polish & Testing (1 week) +1. Test all game mechanics in multiplayer +2. Fix any bugs with collision, sword fighting, etc. +3. Add UI indicators (optional: "Player 1" / "Player 2" labels) +4. Ensure single-player mode is unaffected + +## Potential Challenges + +1. **Input Conflicts**: Need to ensure Player 2 input doesn't interfere with Player 1 +2. **Character State**: The `Char` and `Opp` globals are used throughout - need careful management +3. **Room Transitions**: Both players need to be handled when changing rooms +4. **Win Conditions**: Define what happens when one player dies in multiplayer +5. **Replay System**: May need to handle multiplayer replays differently + +## Advantages of Current Architecture + +✅ **Good News**: +- The game already processes two characters per frame +- Sword fighting system already supports two combatants +- Input system is modular and can be extended +- Character control logic is reusable (`control()` function) +- SDL2 natively supports multiple controllers + +## Complexity Rating Breakdown + +- **Input System**: ⭐⭐⭐ (Medium) - Need to handle two input sources +- **Control Logic**: ⭐⭐ (Low-Medium) - Mostly reusing existing code +- **Initialization**: ⭐⭐ (Low-Medium) - Straightforward setup +- **Testing**: ⭐⭐⭐⭐ (High) - Need to test all game mechanics +- **Integration**: ⭐⭐ (Low-Medium) - Well-isolated changes + +## Recommendation + +**This is very feasible!** The architecture is well-suited for this addition. The main work is: +1. Duplicating the input reading system for Player 2 +2. Replacing AI control with Player 2 input in multiplayer mode +3. Adding initialization and menu options + +The single-player experience can remain completely unchanged by using a compile-time or runtime flag. + +## Next Steps + +Would you like me to: +1. Start implementing the multiplayer mode? +2. Create a detailed implementation plan with code examples? +3. Begin with a specific phase (e.g., input system first)? + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee8da733..622e312c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,18 +1,33 @@ -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 3.5) project(SDLPoP) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -std=c99") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=implicit-function-declaration") +# Add optimization flags for release builds +if (NOT (CMAKE_BUILD_TYPE STREQUAL "Debug")) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -ffast-math -funroll-loops") + # Additional optimizations + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fomit-frame-pointer") + # Link-time optimization for better performance (optional, may not work with all toolchains) + # Check if compiler supports LTO before enabling + include(CheckCCompilerFlag) + check_c_compiler_flag("-flto" COMPILER_SUPPORTS_LTO) + if(COMPILER_SUPPORTS_LTO) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto") + endif() +endif() + # have CMake output binaries to the directory that contains the source files set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${SDLPoP_SOURCE_DIR}/..") # SDLPoP requires the SDL2 and SDL2_image development libraries. -# You can pass the SDL2 location to CMake like so: -DSDL2="C:/work/libraries/SDL2-2.0.8" +# You can pass the SDL2 location to CMake like so: -DSDL2="C:/work/libraries/SDL2-2.30.0" # Or alternatively, specify the SDL2 location below: -#set(SDL2 "C:/work/libraries/SDL2-2.0.8") +#set(SDL2 "C:/work/libraries/SDL2-2.30.0") # On macOS, if you used Homebrew to install SDL2, the location may be something like this: @@ -39,18 +54,38 @@ if (WIN32) # If the location of SDL2 is not specified, we will try to guess it. if (NOT(DEFINED SDL2)) cmake_policy(SET CMP0015 NEW) # suppress warning about relative paths - set(SDL2 "../../SDL2-2.0.8") + # Try MSYS2 installation first (common on Windows) + if(EXISTS "C:/msys64/mingw64") + set(SDL2 "C:/msys64/mingw64") + set(USE_MSYS2_SDL2 TRUE) + elseif(EXISTS "../../SDL2-2.30.0") + set(SDL2 "../../SDL2-2.30.0") + set(USE_MSYS2_SDL2 FALSE) + else() + message(FATAL_ERROR "SDL2 not found. Please install SDL2 via MSYS2 (pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image) or set SDL2 variable.") + endif() + else() + set(USE_MSYS2_SDL2 FALSE) endif() - # Automatically detect whether we need the x86 or x64 version of the SDL2 library. - if (CMAKE_SIZEOF_VOID_P EQUAL 8) - set(SDL2_ARCH "x86_64-w64-mingw32") + if(USE_MSYS2_SDL2) + # MSYS2 installation - headers are in include/SDL2 subdirectory + include_directories(${SDL2}/include) + include_directories(${SDL2}/include/SDL2) + link_directories(${SDL2}/lib) else() - set(SDL2_ARCH "i686-w64-mingw32") + # SDL2 development package - use architecture-specific paths + # Automatically detect whether we need the x86 or x64 version of the SDL2 library. + if (CMAKE_SIZEOF_VOID_P EQUAL 8) + set(SDL2_ARCH "x86_64-w64-mingw32") + else() + set(SDL2_ARCH "i686-w64-mingw32") + endif() + + include_directories(${SDL2}/${SDL2_ARCH}/include) + include_directories(${SDL2}/${SDL2_ARCH}/include/SDL2) + link_directories(${SDL2}/${SDL2_ARCH}/lib) endif() - - include_directories(${SDL2}/${SDL2_ARCH}/include) - link_directories(${SDL2}/${SDL2_ARCH}/lib) endif() set(SOURCE_FILES diff --git a/src/build.bat b/src/build.bat deleted file mode 100644 index ca6a598d..00000000 --- a/src/build.bat +++ /dev/null @@ -1,75 +0,0 @@ -@echo off -setlocal -cd %~dp0 - -:: Check that we have access to the MSVC compiler. - -where /q cl -if ERRORLEVEL 1 ( - echo Problem^: the MSVC compiler ^(cl^) cannot not found. - echo The solution is to run vcvarsall.bat, which sets the necessary environment variables. - echo, - echo Example command for VS2017^: - echo call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" x86 - echo, - echo Example command for VS2015^: - echo call "%%VS140COMNTOOLS%%..\..\VC\vcvarsall.bat" x86 - exit /b -) - -:: To override the directory for SDL2 library files, simply set the SDL environment variable in the command shell. -:: (You could do that from the command-line, or from a wrapper script that calls this one.) - -if [%SDL2%]==[] ( - set SDL2=..\..\SDL2-2.0.6 -) - -if not exist %SDL2% ( - echo Problem^: Could not find SDL2 directory. - echo Tried to look here^: %SDL2% - echo, - echo To specify the SDL2 directory, set the SDL2 environment variable. - echo Example command: - echo set "SDL2=C:\work\libraries\SDL2-2.0.6" - exit /b -) - -:: To choose the build configuration, specify either "debug" or "release" as command-line parameter for this build script. - -if [%1]==[debug] goto build_type_debug -if [%1]==[release] goto build_type_release -echo Build type not specified, compiling in release mode... -echo To specify the build type, run as "build.bat debug" or "build.bat release". -goto build_type_release - -:build_type_debug -set BuildTypeCompilerFlags= /MTd /Od /Z7 -set PreprocessorDefinitions= -DDEBUG=1 -goto compile - -:build_type_release -set BuildTypeCompilerFlags= /MT /O2 -set PreprocessorDefinitions= - -:compile -set SourceFiles= main.c data.c seg000.c seg001.c seg002.c seg003.c seg004.c seg005.c seg006.c seg007.c seg008.c seg009.c seqtbl.c replay.c options.c lighting.c screenshot.c menu.c midi.c opl3.c stb_vorbis.c -set CommonCompilerFlags= /nologo /MP /fp:fast /GR- /wd4048 %PreprocessorDefinitions% /I"%SDL2%\include" -set CommonLinkerFlags= /subsystem:windows,5.01 /libpath:"%SDL2%\lib\%VSCMD_ARG_TGT_ARCH%" SDL2main.lib SDL2.lib SDL2_image.lib Shell32.lib icon.res /out:..\prince.exe - -rc /nologo /fo icon.res icon.rc -cl %BuildTypeCompilerFlags% %CommonCompilerFlags% %SourceFiles% /link %CommonLinkerFlags% - -if %ERRORLEVEL% == 0 (goto success) -echo There were errors. -goto cleanup - -:success -echo Output: ..\prince.exe - -:cleanup -if [%1]==[debug] exit /b -del icon.res 2> NUL -del *.obj 2> NUL -del ..\prince.exp 2> NUL -del ..\prince.lib 2> NUL -del ..\prince.pdb 2> NUL diff --git a/src/data.h b/src/data.h index 5eee25b9..1860b0dc 100644 --- a/src/data.h +++ b/src/data.h @@ -248,6 +248,16 @@ extern sbyte control_y; // data:4612 extern sbyte control_x; +// Multiplayer: Player 2 control variables +extern sbyte control_shift_p2; +extern sbyte control_y_p2; +extern sbyte control_x_p2; +extern sbyte control_shift2_p2; +extern sbyte control_forward_p2; +extern sbyte control_backward_p2; +extern sbyte control_up_p2; +extern sbyte control_down_p2; + #ifdef USE_FADE // data:4CCA extern word is_global_fading; @@ -258,6 +268,8 @@ extern palette_fade_type* fade_palette_buffer; extern char_type Kid; // data:295C extern word is_keyboard_mode INIT(= 0); +// Multiplayer mode flag +extern word is_multiplayer_mode INIT(= 0); // data:4E8A extern word is_paused; // data:42D0 @@ -632,8 +644,11 @@ extern SDL_Texture* texture_blurry; extern SDL_Texture* target_texture; extern SDL_GameController* sdl_controller_ INIT( = 0 ); +extern SDL_GameController* sdl_controller_p2_; // Player 2 controller for multiplayer extern SDL_Joystick* sdl_joystick_; // in case our joystick is not compatible with SDL_GameController +extern SDL_Joystick* sdl_joystick_p2_; // Player 2 joystick for multiplayer extern byte using_sdl_joystick_interface; +extern byte using_sdl_joystick_interface_p2; // Player 2 joystick interface flag extern int joy_axis[JOY_AXIS_NUM]; // hor/ver axes for left/right sticks + left and right triggers (in total 6 axes) extern int joy_axis_max[JOY_AXIS_NUM]; // Same as above, but stores the highest value reached between game updates extern int joy_left_stick_states[2]; // horizontal, vertical @@ -951,6 +966,14 @@ extern int key_down INIT(= SDL_SCANCODE_DOWN); extern int key_jump_left INIT(= SDL_SCANCODE_HOME); extern int key_jump_right INIT(= SDL_SCANCODE_PAGEUP); extern int key_action INIT(= SDL_SCANCODE_RSHIFT); +// Multiplayer: Player 2 controls (default: WASD + F) +extern int key_left_p2 INIT(= SDL_SCANCODE_A); +extern int key_right_p2 INIT(= SDL_SCANCODE_D); +extern int key_up_p2 INIT(= SDL_SCANCODE_W); +extern int key_down_p2 INIT(= SDL_SCANCODE_S); +extern int key_jump_left_p2 INIT(= SDL_SCANCODE_Q); +extern int key_jump_right_p2 INIT(= SDL_SCANCODE_E); +extern int key_action_p2 INIT(= SDL_SCANCODE_X); // menus extern int key_enter INIT(= SDL_SCANCODE_RETURN); extern int key_esc INIT(= SDL_SCANCODE_ESCAPE); diff --git a/src/menu.c b/src/menu.c index 338cf285..e52cbda4 100644 --- a/src/menu.c +++ b/src/menu.c @@ -159,6 +159,7 @@ enum setting_ids { SETTING_ENABLE_FADE, SETTING_ENABLE_FLASH, SETTING_ENABLE_LIGHTING, + SETTING_ENABLE_MULTIPLAYER, SETTING_ENABLE_CHEATS, SETTING_ENABLE_COPYPROT, SETTING_ENABLE_QUICKSAVE, @@ -308,6 +309,14 @@ enum setting_ids { SETTING_KEY_ACTION, SETTING_KEY_ENTER, SETTING_KEY_ESC, + // Multiplayer: Player 2 controls + SETTING_KEY_LEFT_P2, + SETTING_KEY_RIGHT_P2, + SETTING_KEY_UP_P2, + SETTING_KEY_DOWN_P2, + SETTING_KEY_JUMP_LEFT_P2, + SETTING_KEY_JUMP_RIGHT_P2, + SETTING_KEY_ACTION_P2, }; typedef struct setting_type { @@ -409,6 +418,13 @@ setting_type visuals_settings[] = { }; setting_type gameplay_settings[] = { + {.id = SETTING_ENABLE_MULTIPLAYER, .style = SETTING_STYLE_TOGGLE, .linked = &is_multiplayer_mode, + .text = "Multiplayer 1v1 mode", + .explanation = "Enable 1v1 multiplayer mode.\n" + "Player 1 (Kid): Arrow keys + Shift\n" + "Player 2 (Guard): WASD + X\n" + "Both players start facing each other with swords drawn.\n" + "NOTE: Requires restarting the game to take effect."}, {.id = SETTING_ENABLE_CHEATS, .style = SETTING_STYLE_TOGGLE, .linked = &cheats_enabled, .text = "Enable cheats", .explanation = "Turn cheats on or off."/*"\nAlso, display the CHEATS option on the pause menu."*/}, @@ -1054,6 +1070,35 @@ setting_type controls_settings[] = { .linked = &key_action, .number_type = SETTING_INT, .text = "Action", .explanation = ""}, + // Multiplayer: Player 2 controls + {.id = SETTING_KEY_LEFT_P2, .style = SETTING_STYLE_KEY, .required = &is_multiplayer_mode, + .linked = &key_left_p2, .number_type = SETTING_INT, + .text = "Player 2: Left", + .explanation = "Left movement key for Player 2 (Guard)."}, + {.id = SETTING_KEY_RIGHT_P2, .style = SETTING_STYLE_KEY, .required = &is_multiplayer_mode, + .linked = &key_right_p2, .number_type = SETTING_INT, + .text = "Player 2: Right", + .explanation = "Right movement key for Player 2 (Guard)."}, + {.id = SETTING_KEY_UP_P2, .style = SETTING_STYLE_KEY, .required = &is_multiplayer_mode, + .linked = &key_up_p2, .number_type = SETTING_INT, + .text = "Player 2: Up", + .explanation = "Up movement key for Player 2 (Guard)."}, + {.id = SETTING_KEY_DOWN_P2, .style = SETTING_STYLE_KEY, .required = &is_multiplayer_mode, + .linked = &key_down_p2, .number_type = SETTING_INT, + .text = "Player 2: Down", + .explanation = "Down movement key for Player 2 (Guard)."}, + {.id = SETTING_KEY_JUMP_LEFT_P2, .style = SETTING_STYLE_KEY, .required = &is_multiplayer_mode, + .linked = &key_jump_left_p2, .number_type = SETTING_INT, + .text = "Player 2: Jump left", + .explanation = "Jump left key for Player 2 (Guard)."}, + {.id = SETTING_KEY_JUMP_RIGHT_P2, .style = SETTING_STYLE_KEY, .required = &is_multiplayer_mode, + .linked = &key_jump_right_p2, .number_type = SETTING_INT, + .text = "Player 2: Jump right", + .explanation = "Jump right key for Player 2 (Guard)."}, + {.id = SETTING_KEY_ACTION_P2, .style = SETTING_STYLE_KEY, .required = &is_multiplayer_mode, + .linked = &key_action_p2, .number_type = SETTING_INT, + .text = "Player 2: Action", + .explanation = "Action key for Player 2 (Guard)."}, {.id = SETTING_KEY_ENTER, .style = SETTING_STYLE_KEY, .required = NULL, .linked = &key_enter, .number_type = SETTING_INT, .text = "Enter a menu", @@ -1417,6 +1462,60 @@ void turn_setting_on_off(int setting_id, byte new_state, void* linked) { case SETTING_USE_CUSTOM_OPTIONS: turn_custom_options_on_off(new_state); break; + case SETTING_ENABLE_MULTIPLAYER: + // Set the flag first + if (linked != NULL) { + *(word*)linked = new_state; + } + // Multiplayer mode toggled - NEVER spawn a new Guard, only enable controls on existing Guard + if (new_state) { + extern char_type Guard; + extern char_type Kid; + // Only enable controls if Guard is already active in the same room + // If Guard is not active, multiplayer mode will just wait for a Guard to appear naturally + if (Guard.direction != 0x56 && Guard.room == Kid.room) { // Guard is active and in same room + // Guard is already active - just ensure it's set up for multiplayer + // Make sure Guard has a sword if it doesn't have one + if (Guard.sword != 2) { // sword_2_drawn + Guard.sword = 2; // Give Guard a sword if it doesn't have one + } + // Ensure Guard is alive + Guard.alive = -1; + // Set Guard's HP to match Kid's if it's zero + extern word hitp_max; + extern word guardhp_max; + extern word guardhp_curr; + guardhp_max = hitp_max; + if (guardhp_curr == 0) { + guardhp_curr = guardhp_max; + } + // Ensure Guard is in a controllable frame (stand with sword) + // This is important for movement controls to work + // Note: We use a safer approach that doesn't modify global Char + // to avoid affecting Kid's state + extern void loadshad(void); + extern void saveshad(void); + extern char_type Char; + char_type saved_char = Char; // Save current Char (might be Kid) + loadshad(); + Char = Guard; + // If Guard is not in a stand/walk frame, set it to stand with sword + // Check frame numbers directly (158, 170, 171 are stand frames, 157, 165 are walk frames) + if (Char.frame != 158 && Char.frame != 170 && Char.frame != 171 && + Char.frame != 165 && Char.frame != 157) { + extern const word seqtbl_offsets[]; + // seq_90_en_garde is 90, use numeric value + Char.curr_seq = seqtbl_offsets[90]; // stand active (en garde) + extern void play_seq(void); + play_seq(); // Play sequence to set frame + } + Guard = Char; + saveshad(); + Char = saved_char; // Restore Char to avoid affecting Kid + } + // If Guard is not active, do nothing - let the game spawn Guards naturally + } + break; } } @@ -2321,6 +2420,14 @@ void process_ingame_settings_user_managed(SDL_RWops* rw, rw_process_func_type pr process(key_jump_left ); process(key_jump_right); process(key_action ); + // Multiplayer: Player 2 controls + process(key_left_p2 ); + process(key_right_p2 ); + process(key_up_p2 ); + process(key_down_p2 ); + process(key_jump_left_p2 ); + process(key_jump_right_p2); + process(key_action_p2 ); process(key_enter ); process(key_esc ); } diff --git a/src/proto.h b/src/proto.h index 4af7d682..d09522d9 100644 --- a/src/proto.h +++ b/src/proto.h @@ -187,6 +187,7 @@ void init_game(int level); void play_level(int level_number); void do_startpos(void); void set_start_pos(void); +void init_multiplayer_mode(void); void find_start_level_door(void); void draw_level_first(void); void redraw_screen(int drawing_different_room); @@ -333,6 +334,11 @@ void save_ctrl_1(void); void rest_ctrl_1(void); void clear_saved_ctrl(void); void read_user_control(void); +// Multiplayer: Player 2 input functions +void read_user_control_p2(void); +void read_keyb_control_p2(void); +void read_joyst_control_p2(void); +void user_control_p2(void); int can_grab(void); int wall_type(byte tiletype); int get_tile_above_char(void); diff --git a/src/seg000.c b/src/seg000.c index 17a24718..556a0b98 100644 --- a/src/seg000.c +++ b/src/seg000.c @@ -107,6 +107,10 @@ void pop_main() { show_loading(); set_joy_mode(); + + // Multiplayer: Enable multiplayer mode if requested via command line + is_multiplayer_mode = (check_param("multiplayer") != NULL || check_param("mp") != NULL); + cheats_enabled = check_param("megahit") != NULL; #ifdef USE_DEBUG_CHEATS debug_cheats_enabled = check_param("debug") != NULL; @@ -1807,6 +1811,131 @@ void read_keyb_control() { #endif } +// Multiplayer: Player 2 keyboard controls (configurable keys, default WASD) +void read_keyb_control_p2() { + int key_state; + if (fixes->fix_register_quick_input) { + key_state = KEYSTATE_HELD | KEYSTATE_HELD_NEW; + } else { + key_state = KEYSTATE_HELD; + } + + // Initialize control values + control_y_p2 = CONTROL_RELEASED; + control_x_p2 = CONTROL_RELEASED; + control_shift_p2 = CONTROL_RELEASED; + + // Player 2 uses configurable keys (default: W=up, S=down, A=left, D=right, Space=action) + if (key_states[key_up_p2] & key_state || key_states[key_jump_left_p2] & key_state || key_states[key_jump_right_p2] & key_state) { + control_y_p2 = CONTROL_HELD_UP; + } else if (key_states[key_down_p2] & key_state) { + control_y_p2 = CONTROL_HELD_DOWN; + } + + if (key_states[key_left_p2] & key_state || key_states[key_jump_left_p2] & key_state) { + control_x_p2 = CONTROL_HELD_LEFT; + } else if (key_states[key_right_p2] & key_state || key_states[key_jump_right_p2] & key_state) { + control_x_p2 = CONTROL_HELD_RIGHT; + } + + if (key_states[key_action_p2] & key_state) { + control_shift_p2 = CONTROL_HELD; + } +} + +// Multiplayer: Player 2 joystick controls (second controller) +void read_joyst_control_p2() { + if (sdl_controller_p2_ == NULL && sdl_joystick_p2_ == NULL) { + // No controller for Player 2 - don't clear keyboard controls! + // Keyboard controls are handled by read_keyb_control_p2() + // Only clear joystick-specific controls if we had a controller before + return; + } + + int key_state; + int joy_axis_p2[JOY_AXIS_NUM]; + int joy_left_stick_states_p2[2]; + int joy_right_stick_states_p2[2]; + int joy_button_states_p2[JOYINPUT_NUM]; + + if (fixes->fix_register_quick_input) { + key_state = KEYSTATE_HELD | KEYSTATE_HELD_NEW; + } else { + key_state = KEYSTATE_HELD; + } + + // Read axes from Player 2 controller + if (sdl_controller_p2_ != NULL && !using_sdl_joystick_interface_p2) { + joy_axis_p2[SDL_CONTROLLER_AXIS_LEFTX] = SDL_GameControllerGetAxis(sdl_controller_p2_, SDL_CONTROLLER_AXIS_LEFTX); + joy_axis_p2[SDL_CONTROLLER_AXIS_LEFTY] = SDL_GameControllerGetAxis(sdl_controller_p2_, SDL_CONTROLLER_AXIS_LEFTY); + joy_axis_p2[SDL_CONTROLLER_AXIS_RIGHTX] = SDL_GameControllerGetAxis(sdl_controller_p2_, SDL_CONTROLLER_AXIS_RIGHTX); + joy_axis_p2[SDL_CONTROLLER_AXIS_RIGHTY] = SDL_GameControllerGetAxis(sdl_controller_p2_, SDL_CONTROLLER_AXIS_RIGHTY); + joy_axis_p2[SDL_CONTROLLER_AXIS_TRIGGERLEFT] = SDL_GameControllerGetAxis(sdl_controller_p2_, SDL_CONTROLLER_AXIS_TRIGGERLEFT); + joy_axis_p2[SDL_CONTROLLER_AXIS_TRIGGERRIGHT] = SDL_GameControllerGetAxis(sdl_controller_p2_, SDL_CONTROLLER_AXIS_TRIGGERRIGHT); + + // Read buttons + joy_button_states_p2[JOYINPUT_DPAD_LEFT] = SDL_GameControllerGetButton(sdl_controller_p2_, SDL_CONTROLLER_BUTTON_DPAD_LEFT) ? key_state : 0; + joy_button_states_p2[JOYINPUT_DPAD_RIGHT] = SDL_GameControllerGetButton(sdl_controller_p2_, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) ? key_state : 0; + joy_button_states_p2[JOYINPUT_DPAD_UP] = SDL_GameControllerGetButton(sdl_controller_p2_, SDL_CONTROLLER_BUTTON_DPAD_UP) ? key_state : 0; + joy_button_states_p2[JOYINPUT_DPAD_DOWN] = SDL_GameControllerGetButton(sdl_controller_p2_, SDL_CONTROLLER_BUTTON_DPAD_DOWN) ? key_state : 0; + joy_button_states_p2[JOYINPUT_X] = SDL_GameControllerGetButton(sdl_controller_p2_, SDL_CONTROLLER_BUTTON_X) ? key_state : 0; + joy_button_states_p2[JOYINPUT_Y] = SDL_GameControllerGetButton(sdl_controller_p2_, SDL_CONTROLLER_BUTTON_Y) ? key_state : 0; + joy_button_states_p2[JOYINPUT_A] = SDL_GameControllerGetButton(sdl_controller_p2_, SDL_CONTROLLER_BUTTON_A) ? key_state : 0; + } else if (sdl_joystick_p2_ != NULL) { + // Use SDL_Joystick interface (fallback for non-GameController devices) + joy_axis_p2[SDL_CONTROLLER_AXIS_LEFTX] = SDL_JoystickGetAxis(sdl_joystick_p2_, 0); + joy_axis_p2[SDL_CONTROLLER_AXIS_LEFTY] = SDL_JoystickGetAxis(sdl_joystick_p2_, 1); + joy_axis_p2[SDL_CONTROLLER_AXIS_RIGHTX] = SDL_JoystickGetAxis(sdl_joystick_p2_, 2); + joy_axis_p2[SDL_CONTROLLER_AXIS_RIGHTY] = SDL_JoystickGetAxis(sdl_joystick_p2_, 3); + joy_axis_p2[SDL_CONTROLLER_AXIS_TRIGGERLEFT] = SDL_JoystickGetAxis(sdl_joystick_p2_, 4); + joy_axis_p2[SDL_CONTROLLER_AXIS_TRIGGERRIGHT] = SDL_JoystickGetAxis(sdl_joystick_p2_, 5); + + // Read buttons (assuming standard mapping) + joy_button_states_p2[JOYINPUT_DPAD_LEFT] = SDL_JoystickGetButton(sdl_joystick_p2_, 11) ? key_state : 0; + joy_button_states_p2[JOYINPUT_DPAD_RIGHT] = SDL_JoystickGetButton(sdl_joystick_p2_, 12) ? key_state : 0; + joy_button_states_p2[JOYINPUT_DPAD_UP] = SDL_JoystickGetButton(sdl_joystick_p2_, 13) ? key_state : 0; + joy_button_states_p2[JOYINPUT_DPAD_DOWN] = SDL_JoystickGetButton(sdl_joystick_p2_, 14) ? key_state : 0; + joy_button_states_p2[JOYINPUT_X] = SDL_JoystickGetButton(sdl_joystick_p2_, 2) ? key_state : 0; + joy_button_states_p2[JOYINPUT_Y] = SDL_JoystickGetButton(sdl_joystick_p2_, 3) ? key_state : 0; + joy_button_states_p2[JOYINPUT_A] = SDL_JoystickGetButton(sdl_joystick_p2_, 0) ? key_state : 0; + } else { + return; // No controller + } + + // Process stick states + if (joystick_only_horizontal) { + get_joystick_state_hor_only(joy_axis_p2[SDL_CONTROLLER_AXIS_LEFTX], joy_left_stick_states_p2); + get_joystick_state_hor_only(joy_axis_p2[SDL_CONTROLLER_AXIS_RIGHTX], joy_right_stick_states_p2); + } else { + get_joystick_state(joy_axis_p2[SDL_CONTROLLER_AXIS_LEFTX], joy_axis_p2[SDL_CONTROLLER_AXIS_LEFTY], joy_left_stick_states_p2); + get_joystick_state(joy_axis_p2[SDL_CONTROLLER_AXIS_RIGHTX], joy_axis_p2[SDL_CONTROLLER_AXIS_RIGHTY], joy_right_stick_states_p2); + } + + // Apply stick/button input to Player 2 controls + if (joy_left_stick_states_p2[0] == -1 || joy_right_stick_states_p2[0] == -1 || joy_button_states_p2[JOYINPUT_DPAD_LEFT] & key_state) + control_x_p2 = CONTROL_HELD_LEFT; + else if (joy_left_stick_states_p2[0] == 1 || joy_right_stick_states_p2[0] == 1 || joy_button_states_p2[JOYINPUT_DPAD_RIGHT] & key_state) + control_x_p2 = CONTROL_HELD_RIGHT; + else + control_x_p2 = CONTROL_RELEASED; + + if (joy_left_stick_states_p2[1] == -1 || joy_right_stick_states_p2[1] == -1 || joy_button_states_p2[JOYINPUT_DPAD_UP] & key_state || joy_button_states_p2[JOYINPUT_Y] & key_state) + control_y_p2 = CONTROL_HELD_UP; + else if (joy_left_stick_states_p2[1] == 1 || joy_right_stick_states_p2[1] == 1 || joy_button_states_p2[JOYINPUT_DPAD_DOWN] & key_state || joy_button_states_p2[JOYINPUT_A] & key_state) + control_y_p2 = CONTROL_HELD_DOWN; + else + control_y_p2 = CONTROL_RELEASED; + + if (joy_button_states_p2[JOYINPUT_X] & key_state || + joy_axis_p2[SDL_CONTROLLER_AXIS_TRIGGERLEFT] > 8000 || + joy_axis_p2[SDL_CONTROLLER_AXIS_TRIGGERRIGHT] > 8000) + { + control_shift_p2 = CONTROL_HELD; + } else { + control_shift_p2 = CONTROL_RELEASED; + } +} + // We need a version of showmessage() which can detect modifier keys as well, in case someone wants to configure such a key for controls. int showmessage_any_key(char *text,int arg_4,void *arg_0) { word key; diff --git a/src/seg003.c b/src/seg003.c index f2a80b80..2e29ef72 100644 --- a/src/seg003.c +++ b/src/seg003.c @@ -102,7 +102,13 @@ void play_level(int level_number) { Guard.charid = charid_2_guard; Guard.direction = dir_56_none; do_startpos(); + have_sword = /*(level_number != 1)*/ (level_number == 0 || level_number >= custom->have_sword_from_level); + + // Multiplayer: Initialize multiplayer mode if enabled (after have_sword is set) + if (is_multiplayer_mode) { + init_multiplayer_mode(); + } find_start_level_door(); // busy waiting? while (check_sound_playing() && !do_paused()) idle(); @@ -201,6 +207,83 @@ void set_start_pos() { savekid(); } +// Multiplayer: Initialize players facing each other for 1v1 mode +// NOTE: This function does NOT spawn enemies - it only sets up existing Guards +void init_multiplayer_mode() { + // Only set up if Guard is already active - DO NOT spawn new enemies + // Guard.direction == 0x56 (dir_56_none) means Guard is inactive + if (Guard.direction == 0x56) { + // No Guard active - don't spawn one, just return + // Multiplayer will work when a Guard appears naturally in the level + return; + } + + // Guard is already active - just ensure it's set up for multiplayer + Guard.alive = -1; + Guard.room = Kid.room; + + // Only reposition if Guard is not in the same room as Kid + if (Guard.room != Kid.room) { + Guard.room = Kid.room; + // Position Guard to face Kid (place Guard a few tiles to the right of Kid) + Guard.curr_col = Kid.curr_col + 3; // 3 tiles to the right + if (Guard.curr_col >= SCREEN_TILECOUNTX) { + Guard.curr_col = SCREEN_TILECOUNTX - 1; // Keep within screen bounds + } + Guard.curr_row = Kid.curr_row; + + // Calculate Guard's x position + Guard.x = x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX; + Guard.y = y_land[Guard.curr_row + 1]; + + // Make Guard face left (toward Kid) + Guard.direction = dir_FF_left; + } + + // Give both characters swords if they don't have them + if (Kid.sword != sword_2_drawn) { + Kid.sword = sword_2_drawn; + } + if (Guard.sword != sword_2_drawn) { + Guard.sword = sword_2_drawn; + } + + // Set Guard's HP (same as Kid's max HP for fair fight) + guardhp_max = hitp_max; + if (guardhp_curr == 0) { + guardhp_curr = guardhp_max; + } + + // Initialize Guard's state + Guard.fall_x = 0; + Guard.fall_y = 0; + Guard.action = actions_1_run_jump; + Guard.repeat = 0; + + // Load Guard sprite data and set to stand with sword pose + loadshad(); + Char = Guard; // Set Char to Guard temporarily + seqtbl_offset_char(seq_90_en_garde); // stand active (en garde) + play_seq(); // Play sequence to set frame + Guard = Char; // Restore Guard from Char + saveshad(); + + // Also ensure Kid has sword drawn and is in correct pose + loadkid(); + Char = Kid; + if (Kid.sword == sword_2_drawn) { + seqtbl_offset_char(seq_55_draw_sword); // draw sword (will set to stand with sword) + play_seq(); + } + savekid(); + + // Make Kid face right (toward Guard) if they're in the same room + if (Kid.room == Guard.room) { + Kid.direction = dir_0_right; + savekid(); + } +} + // seg003:02E6 void find_start_level_door() { get_room_address(Kid.room); diff --git a/src/seg005.c b/src/seg005.c index 13a09744..e840bf9c 100644 --- a/src/seg005.c +++ b/src/seg005.c @@ -344,6 +344,17 @@ void control_standing() { if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD && check_get_item()) { return; } + // Multiplayer: For Player 2, allow drawing sword with attack button when sheathed + // Check raw input directly (like down+forward does) to bypass state management issues + extern word is_multiplayer_mode; + extern sbyte control_shift_p2; + if (is_multiplayer_mode && Char.charid >= charid_2_guard && Char.sword == sword_0_sheathed) { + // Check both the processed state and raw input to ensure it works + if (control_shift == CONTROL_HELD || control_shift2 == CONTROL_HELD || control_shift_p2 == CONTROL_HELD) { + draw_sword(); + return; + } + } if (Char.charid != charid_0_kid && control_down == CONTROL_HELD && control_forward == CONTROL_HELD) { draw_sword(); return; @@ -922,7 +933,18 @@ void run_jump() { // sseg005:0BB5 void back_with_sword() { short frame = Char.frame; - if (frame == frame_158_stand_with_sword || frame == frame_170_stand_with_sword || frame == frame_171_stand_with_sword) { + // Multiplayer: In multiplayer mode, allow movement from any frame for Guard + extern word is_multiplayer_mode; + int can_move = (frame == frame_158_stand_with_sword || frame == frame_170_stand_with_sword || frame == frame_171_stand_with_sword); + if (is_multiplayer_mode && Char.charid >= charid_2_guard) { + // In multiplayer, allow movement from any frame (stand, walk, or other) + // This ensures Player 2 can always control the Guard + can_move = 1; // Always allow movement in multiplayer + } else { + // Normal mode: only allow from stand frames + can_move = can_move || (frame == frame_165_walk_with_sword || frame == frame_157_walk_with_sword); + } + if (can_move) { control_backward = CONTROL_IGNORE; // disable automatic repeat seqtbl_offset_char(seq_57_back_with_sword); // back with sword } @@ -931,7 +953,18 @@ void back_with_sword() { // seg005:0BE3 void forward_with_sword() { short frame = Char.frame; - if (frame == frame_158_stand_with_sword || frame == frame_170_stand_with_sword || frame == frame_171_stand_with_sword) { + // Multiplayer: In multiplayer mode, allow movement from any frame for Guard + extern word is_multiplayer_mode; + int can_move = (frame == frame_158_stand_with_sword || frame == frame_170_stand_with_sword || frame == frame_171_stand_with_sword); + if (is_multiplayer_mode && Char.charid >= charid_2_guard) { + // In multiplayer, allow movement from any frame (stand, walk, or other) + // This ensures Player 2 can always control the Guard + can_move = 1; // Always allow movement in multiplayer + } else { + // Normal mode: only allow from stand frames + can_move = can_move || (frame == frame_165_walk_with_sword || frame == frame_157_walk_with_sword); + } + if (can_move) { control_forward = CONTROL_IGNORE; // disable automatic repeat if (Char.charid != charid_0_kid) { seqtbl_offset_char(seq_56_guard_forward_with_sword); // forward with sword (Guard) @@ -945,6 +978,12 @@ void forward_with_sword() { void draw_sword() { word seq_id = seq_55_draw_sword; // draw sword control_forward = control_shift2 = release_arrows(); + // Multiplayer: Also set control_shift2_p2 to IGNORE to prevent repeat + extern word is_multiplayer_mode; + extern sbyte control_shift2_p2; + if (is_multiplayer_mode && Char.charid >= charid_2_guard) { + control_shift2_p2 = CONTROL_IGNORE; // Prevent repeat for Player 2 + } #ifdef FIX_UNINTENDED_SWORD_STRIKE if (fixes->fix_unintended_sword_strike) { ctrl1_shift2 = CONTROL_IGNORE; // prevent restoring control_shift2 to CONTROL_HELD in rest_ctrl_1() @@ -963,6 +1002,23 @@ void draw_sword() { // seg005:0C67 void control_with_sword() { if (Char.action < actions_2_hang_climb) { + // Multiplayer: In multiplayer mode, allow movement controls for Guard but also auto-turn + extern word is_multiplayer_mode; + if (is_multiplayer_mode && Char.charid >= charid_2_guard) { + // In multiplayer, check distance and auto-turn if needed (like Player 1 does) + short distance = char_opp_dist(); + if (distance < 0) { + // Opponent is behind - turn to face them + if ((word)distance < (word)-4) { + seqtbl_offset_char(seq_60_turn_with_sword); // turn with sword (after switching places) + return; + } + } + // In multiplayer, Guard can move freely - go directly to swordfight() which handles movement + swordfight(); + return; + } + if (get_tile_at_char() == tiles_11_loose || can_guard_see_kid >= 2) { short distance = char_opp_dist(); if ((word)distance < (word)90) { @@ -1013,7 +1069,22 @@ void swordfight() { if (control_down == CONTROL_HELD) { if (frame == frame_158_stand_with_sword || frame == frame_170_stand_with_sword || frame == frame_171_stand_with_sword) { control_down = CONTROL_IGNORE; // disable automatic repeat + // Multiplayer: Also set control_down_p2 to IGNORE to prevent repeat + extern word is_multiplayer_mode; + extern sbyte control_down_p2; + if (is_multiplayer_mode && charid >= charid_2_guard) { + control_down_p2 = CONTROL_IGNORE; // Prevent repeat for Player 2 + } Char.sword = sword_0_sheathed; + // Multiplayer: For Player 2, reset attack state when sheathing sword + // This allows re-entering attack stance after sheathing + extern word is_multiplayer_mode; + extern sbyte control_shift2_p2; + if (is_multiplayer_mode && charid >= charid_2_guard) { + control_shift2_p2 = CONTROL_RELEASED; // Reset attack state to allow re-entering attack stance + // Also reset control_shift2 to ensure it's cleared + control_shift2 = CONTROL_RELEASED; + } if (charid == charid_0_kid) { offguard = 1; guard_refrac = 9; @@ -1029,8 +1100,20 @@ void swordfight() { } else if (control_up == CONTROL_HELD) { parry(); } else if (control_forward == CONTROL_HELD) { + // Multiplayer: For Player 2, set to IGNORE immediately to prevent repeat + extern word is_multiplayer_mode; + if (is_multiplayer_mode && charid >= charid_2_guard) { + control_forward = CONTROL_IGNORE; // Prevent continuous movement + control_forward_p2 = CONTROL_IGNORE; + } forward_with_sword(); } else if (control_backward == CONTROL_HELD) { + // Multiplayer: For Player 2, set to IGNORE immediately to prevent repeat + extern word is_multiplayer_mode; + if (is_multiplayer_mode && charid >= charid_2_guard) { + control_backward = CONTROL_IGNORE; // Prevent continuous movement + control_backward_p2 = CONTROL_IGNORE; + } back_with_sword(); } } @@ -1057,6 +1140,12 @@ void sword_strike() { return; } control_shift2 = CONTROL_IGNORE; // disable automatic repeat + // Multiplayer: Also set control_shift2_p2 to IGNORE to prevent repeat + extern word is_multiplayer_mode; + extern sbyte control_shift2_p2; + if (is_multiplayer_mode && Char.charid >= charid_2_guard) { + control_shift2_p2 = CONTROL_IGNORE; // Prevent repeat for Player 2 + } seqtbl_offset_char(seq_id); } @@ -1074,7 +1163,9 @@ void parry() { char_frame == frame_168_back || // back? char_frame == frame_165_walk_with_sword // walk with sword ) { - if (char_opp_dist() >= 32 && char_charid != charid_0_kid) { + // Multiplayer: Allow parry for Player 2 even when opponent is far away + extern word is_multiplayer_mode; + if (char_opp_dist() >= 32 && char_charid != charid_0_kid && !(is_multiplayer_mode && char_charid >= charid_2_guard)) { back_with_sword(); return; } else if (char_charid == charid_0_kid) { @@ -1092,13 +1183,24 @@ void parry() { } } } else { - if (opp_frame != frame_152_strike_2) return; + // Multiplayer: Allow parry for Player 2 even if opponent is not striking + if (is_multiplayer_mode && char_charid >= charid_2_guard) { + // Allow parry for Player 2 - don't check opponent frame + } else if (opp_frame != frame_152_strike_2) { + return; + } } } else { if (char_frame != frame_167_blocked) return; seq_id = seq_61_parry_after_strike; // parry after striking with sword } control_up = CONTROL_IGNORE; // disable automatic repeat + // Multiplayer: Also set control_up_p2 to IGNORE to prevent repeat + extern word is_multiplayer_mode; + extern sbyte control_up_p2; + if (is_multiplayer_mode && Char.charid >= charid_2_guard) { + control_up_p2 = CONTROL_IGNORE; // Prevent repeat for Player 2 + } seqtbl_offset_char(seq_id); if (do_play_seq) { play_seq(); diff --git a/src/seg006.c b/src/seg006.c index 1a885abd..6c7dbdfa 100644 --- a/src/seg006.c +++ b/src/seg006.c @@ -1489,8 +1489,20 @@ void play_guard() { clear_char(); } loc_7A65: - autocontrol_opponent(); - control(); + // Multiplayer: Use Player 2 input instead of AI if in multiplayer mode + if (is_multiplayer_mode) { + // Char is already set to Guard by loadshad_and_opp() in play_guard_frame() + // Disable AI control by clearing autocontrol variables + can_guard_see_kid = 0; // Disable AI seeing Kid + // Read Player 2 input and apply it + read_user_control_p2(); + user_control_p2(); + // Save Char back to Guard (in case user_control_p2 modified it) + Guard = Char; + } else { + autocontrol_opponent(); + control(); + } } } @@ -1591,6 +1603,335 @@ void read_user_control() { } } +// Multiplayer: Player 2 version of read_user_control +void read_user_control_p2() { + // Read keyboard and joystick input for Player 2 first + read_keyb_control_p2(); + read_joyst_control_p2(); + + // Convert control_x_p2 (LEFT/RIGHT) to forward/backward based on Guard's direction + // This is similar to how flip_control_x() works, but we need to do it for Player 2 + // Use Char.direction since Char is set to Guard in play_guard_frame() before this is called + sbyte effective_x_p2 = control_x_p2; + if (Char.direction >= dir_0_right) { + // Guard facing right: right = forward, left = backward + if (control_x_p2 == CONTROL_HELD_RIGHT) { + effective_x_p2 = CONTROL_HELD_FORWARD; + } else if (control_x_p2 == CONTROL_HELD_LEFT) { + effective_x_p2 = CONTROL_HELD_BACKWARD; + } + } else { + // Guard facing left: left = forward, right = backward + if (control_x_p2 == CONTROL_HELD_LEFT) { + effective_x_p2 = CONTROL_HELD_FORWARD; + } else if (control_x_p2 == CONTROL_HELD_RIGHT) { + effective_x_p2 = CONTROL_HELD_BACKWARD; + } + } + + // Process Player 2 controls - set directly based on input + // For single-step movement: Only trigger on NEW key presses (not held keys) + // Save previous state to detect transitions + static sbyte prev_forward_p2 = CONTROL_RELEASED; + static sbyte prev_backward_p2 = CONTROL_RELEASED; + + // Initialize to CONTROL_RELEASED first, but preserve IGNORE state + // Only reset if not IGNORE (preserve IGNORE state during animations) + if (control_up_p2 != CONTROL_IGNORE) { + control_up_p2 = CONTROL_RELEASED; + } + if (control_down_p2 != CONTROL_IGNORE) { + control_down_p2 = CONTROL_RELEASED; + } + // Don't reset control_shift2_p2 here if it's IGNORE (attack in progress) or sword is sheathed + extern word is_multiplayer_mode; + if (!(is_multiplayer_mode && Char.charid >= charid_2_guard && Char.sword == sword_0_sheathed)) { + // Only reset if not IGNORE (preserve IGNORE state during attack animation) + if (control_shift2_p2 != CONTROL_IGNORE) { + control_shift2_p2 = CONTROL_RELEASED; + } + } + + // For forward/backward: Only set to HELD if transitioning from RELEASED (new press) + // This prevents continuous movement when key is held + if (effective_x_p2 == CONTROL_HELD_FORWARD) { + // Check if this is a new press (previous was RELEASED and current is not IGNORE) + if (prev_forward_p2 == CONTROL_RELEASED && control_forward_p2 != CONTROL_IGNORE) { + // New press - trigger movement + control_forward_p2 = CONTROL_HELD; + } else if (control_forward_p2 == CONTROL_IGNORE) { + // Movement in progress - keep IGNORE (don't change it) + } else { + // Key held but already processed - keep RELEASED to prevent repeat + control_forward_p2 = CONTROL_RELEASED; + } + } else { + // Key not pressed + if (control_forward_p2 == CONTROL_IGNORE) { + // Movement was in progress, now key released - reset to RELEASED + control_forward_p2 = CONTROL_RELEASED; + } else { + // Already RELEASED - keep it + control_forward_p2 = CONTROL_RELEASED; + } + } + // Update previous state for next frame + prev_forward_p2 = control_forward_p2; + + if (effective_x_p2 == CONTROL_HELD_BACKWARD) { + // Check if this is a new press (previous was RELEASED and current is not IGNORE) + if (prev_backward_p2 == CONTROL_RELEASED && control_backward_p2 != CONTROL_IGNORE) { + // New press - trigger movement + control_backward_p2 = CONTROL_HELD; + } else if (control_backward_p2 == CONTROL_IGNORE) { + // Movement in progress - keep IGNORE (don't change it) + } else { + // Key held but already processed - keep RELEASED to prevent repeat + control_backward_p2 = CONTROL_RELEASED; + } + } else { + // Key not pressed + if (control_backward_p2 == CONTROL_IGNORE) { + // Movement was in progress, now key released - reset to RELEASED + control_backward_p2 = CONTROL_RELEASED; + } else { + // Already RELEASED - keep it + control_backward_p2 = CONTROL_RELEASED; + } + } + // Update previous state for next frame + prev_backward_p2 = control_backward_p2; + + // For up/down: Single-step mode (same as forward/backward/attack) + static sbyte prev_up_p2 = CONTROL_RELEASED; + static sbyte prev_down_p2 = CONTROL_RELEASED; + + // For up (parry/block): Single-step mode + if (control_y_p2 == CONTROL_HELD_UP) { + // If already IGNORE, keep it IGNORE (parry animation in progress) + if (control_up_p2 == CONTROL_IGNORE) { + // Parry in progress - keep IGNORE (don't change it) + prev_up_p2 = CONTROL_IGNORE; + } else if (prev_up_p2 == CONTROL_RELEASED && control_up_p2 != CONTROL_IGNORE) { + // New press - trigger parry + control_up_p2 = CONTROL_HELD; + prev_up_p2 = control_up_p2; + } else { + // Key held but already processed - keep RELEASED to prevent repeat + control_up_p2 = CONTROL_RELEASED; + prev_up_p2 = control_up_p2; + } + } else { + // Key not pressed + if (control_up_p2 == CONTROL_IGNORE) { + // Parry was in progress, now key released - reset to RELEASED + control_up_p2 = CONTROL_RELEASED; + prev_up_p2 = control_up_p2; + } else { + // Already RELEASED - keep it + control_up_p2 = CONTROL_RELEASED; + prev_up_p2 = control_up_p2; + } + } + + // For down (sheath sword): Single-step mode + if (control_y_p2 == CONTROL_HELD_DOWN) { + // If already IGNORE, keep it IGNORE (sheath animation in progress) + if (control_down_p2 == CONTROL_IGNORE) { + // Sheath in progress - keep IGNORE (don't change it) + prev_down_p2 = CONTROL_IGNORE; + } else if (prev_down_p2 == CONTROL_RELEASED && control_down_p2 != CONTROL_IGNORE) { + // New press - trigger sheath + control_down_p2 = CONTROL_HELD; + prev_down_p2 = control_down_p2; + } else { + // Key held but already processed - keep RELEASED to prevent repeat + control_down_p2 = CONTROL_RELEASED; + prev_down_p2 = control_down_p2; + } + } else { + // Key not pressed + if (control_down_p2 == CONTROL_IGNORE) { + // Sheath was in progress, now key released - reset to RELEASED + control_down_p2 = CONTROL_RELEASED; + prev_down_p2 = control_down_p2; + } else { + // Already RELEASED - keep it + control_down_p2 = CONTROL_RELEASED; + prev_down_p2 = control_down_p2; + } + } + + // For action/attack: Single-step mode (same as forward/backward) + static sbyte prev_shift2_p2 = CONTROL_RELEASED; + extern word is_multiplayer_mode; + // Multiplayer: Special handling when sword is sheathed - allow attack button to draw sword + // But still use single-step logic to prevent repeat + if (is_multiplayer_mode && Char.charid >= charid_2_guard && Char.sword == sword_0_sheathed) { + // When sword is sheathed, use single-step logic to prevent repeat + if (control_shift_p2 == CONTROL_HELD) { + // Check if this is a new press (previous was RELEASED and current is not IGNORE) + if (prev_shift2_p2 == CONTROL_RELEASED && control_shift2_p2 != CONTROL_IGNORE) { + // New press - trigger attack to draw sword + control_shift2_p2 = CONTROL_HELD; + } else if (control_shift2_p2 == CONTROL_IGNORE) { + // Attack in progress - keep IGNORE (don't change it) + } else { + // Key held but already processed - keep RELEASED to prevent repeat + control_shift2_p2 = CONTROL_RELEASED; + } + } else { + // Key not pressed + if (control_shift2_p2 == CONTROL_IGNORE) { + // Attack was in progress, now key released - reset to RELEASED + control_shift2_p2 = CONTROL_RELEASED; + } else { + // Already RELEASED - keep it + control_shift2_p2 = CONTROL_RELEASED; + } + } + // Update previous state for next frame + prev_shift2_p2 = control_shift2_p2; + } else { + // Normal attack logic when sword is drawn or not multiplayer + if (control_shift_p2 == CONTROL_HELD) { + // If already IGNORE, keep it IGNORE (attack animation in progress) + if (control_shift2_p2 == CONTROL_IGNORE) { + // Attack in progress - keep IGNORE (don't change it) + // Keep prev_shift2_p2 as IGNORE to prevent re-triggering while key is held + prev_shift2_p2 = CONTROL_IGNORE; + } else if (prev_shift2_p2 == CONTROL_RELEASED) { + // New press - trigger attack + control_shift2_p2 = CONTROL_HELD; + prev_shift2_p2 = control_shift2_p2; + } else { + // Key held but already processed - keep RELEASED to prevent repeat + control_shift2_p2 = CONTROL_RELEASED; + prev_shift2_p2 = control_shift2_p2; + } + } else { + // Key not pressed + if (control_shift2_p2 == CONTROL_IGNORE) { + // Attack was in progress, now key released - reset to RELEASED + control_shift2_p2 = CONTROL_RELEASED; + prev_shift2_p2 = control_shift2_p2; + } else { + // Already RELEASED - keep it + control_shift2_p2 = CONTROL_RELEASED; + prev_shift2_p2 = control_shift2_p2; + } + } + // Note: prev_shift2_p2 is already updated in each branch above + } +} + +// Multiplayer: Apply Player 2 input to Guard character +void user_control_p2() { + // IMPORTANT: Save Player 1's control state BEFORE swapping + // This ensures Player 1's controls are preserved and not affecting Guard + sbyte temp_x = control_x; + sbyte temp_y = control_y; + sbyte temp_shift = control_shift; + sbyte temp_forward = control_forward; + sbyte temp_backward = control_backward; + sbyte temp_up = control_up; + sbyte temp_down = control_down; + sbyte temp_shift2 = control_shift2; + + // Convert control_x_p2 (LEFT/RIGHT) to FORWARD/BACKWARD for control() function + // This is the same conversion we did in read_user_control_p2() + sbyte control_x_effective = control_x_p2; + if (Char.direction >= dir_0_right) { + // Guard facing right: right = forward, left = backward + if (control_x_p2 == CONTROL_HELD_RIGHT) { + control_x_effective = CONTROL_HELD_FORWARD; + } else if (control_x_p2 == CONTROL_HELD_LEFT) { + control_x_effective = CONTROL_HELD_BACKWARD; + } + } else { + // Guard facing left: left = forward, right = backward + if (control_x_p2 == CONTROL_HELD_LEFT) { + control_x_effective = CONTROL_HELD_FORWARD; + } else if (control_x_p2 == CONTROL_HELD_RIGHT) { + control_x_effective = CONTROL_HELD_BACKWARD; + } + } + + // IMPORTANT: Clear Player 1's controls first, then set Player 2 controls + // This ensures Player 1's input doesn't leak into Guard control + // BUT: Preserve IGNORE state for up/down/attack to prevent repeat + sbyte saved_up = control_up; // Save IGNORE state if present + sbyte saved_down = control_down; // Save IGNORE state if present + sbyte saved_shift2 = control_shift2; // Save IGNORE state if present + control_x = CONTROL_RELEASED; + control_y = CONTROL_RELEASED; + control_shift = CONTROL_RELEASED; + control_forward = CONTROL_RELEASED; + control_backward = CONTROL_RELEASED; + control_up = CONTROL_RELEASED; + control_down = CONTROL_RELEASED; + control_shift2 = CONTROL_RELEASED; + // Restore IGNORE state if it was set (from previous frame's parry/sheath/strike) + if (saved_up == CONTROL_IGNORE) { + control_up = CONTROL_IGNORE; + } + if (saved_down == CONTROL_IGNORE) { + control_down = CONTROL_IGNORE; + } + if (saved_shift2 == CONTROL_IGNORE) { + control_shift2 = CONTROL_IGNORE; + } + + // Now set Player 2 controls as active + control_x = control_x_effective; // Use converted FORWARD/BACKWARD value + control_y = control_y_p2; + control_shift = control_shift_p2; + control_forward = control_forward_p2; + control_backward = control_backward_p2; + // IMPORTANT: For up (parry) and down (sheath), preserve IGNORE state (same as attack) + // Priority: IGNORE state must be preserved to prevent repeat + if (control_up == CONTROL_IGNORE || control_up_p2 == CONTROL_IGNORE) { + // Either is IGNORE - set both to IGNORE to prevent repeat + control_up = CONTROL_IGNORE; + control_up_p2 = CONTROL_IGNORE; + } else { + // Neither is IGNORE - map from control_up_p2 + control_up = control_up_p2; + } + if (control_down == CONTROL_IGNORE || control_down_p2 == CONTROL_IGNORE) { + // Either is IGNORE - set both to IGNORE to prevent repeat + control_down = CONTROL_IGNORE; + control_down_p2 = CONTROL_IGNORE; + } else { + // Neither is IGNORE - map from control_down_p2 + control_down = control_down_p2; + } + // IMPORTANT: For attack, preserve IGNORE state (same as forward/backward) + // Priority: IGNORE state must be preserved to prevent repeat attacks + if (control_shift2 == CONTROL_IGNORE || control_shift2_p2 == CONTROL_IGNORE) { + // Either is IGNORE - set both to IGNORE to prevent repeat + control_shift2 = CONTROL_IGNORE; + control_shift2_p2 = CONTROL_IGNORE; + } else { + // Neither is IGNORE - map from control_shift2_p2 + control_shift2 = control_shift2_p2; + } + + // Apply control to Guard (Char should already be set to Guard) + // Note: We don't need to flip_control_x() here because we already converted control_x + control(); + + // Restore Player 1 controls exactly as they were + control_x = temp_x; + control_y = temp_y; + control_shift = temp_shift; + control_forward = temp_forward; + control_backward = temp_backward; + control_up = temp_up; + control_down = temp_down; + control_shift2 = temp_shift2; +} + // seg006:0F55 int can_grab() { // Can char grab curr_tile2 through through_tile? @@ -2110,6 +2451,21 @@ void add_sword_to_objtable() { // seg006:1827 void control_guard_inactive() { + // Multiplayer: Allow attack button to draw sword when sheathed + extern word is_multiplayer_mode; + extern sbyte control_shift_p2; + extern sbyte control_shift2_p2; + if (is_multiplayer_mode && Char.charid >= charid_2_guard && Char.sword == sword_0_sheathed) { + // Check attack button (same as down+forward check) + // Only trigger if control_shift2_p2 is HELD (single-step logic ensures it's only HELD on new press) + if (control_shift2 == CONTROL_HELD) { + draw_sword(); + // Set to IGNORE to prevent repeat (same as draw_sword() does internally) + control_shift2 = CONTROL_IGNORE; + control_shift2_p2 = CONTROL_IGNORE; + return; + } + } if (Char.frame == frame_166_stand_inactive && control_down == CONTROL_HELD) { if (control_forward == CONTROL_HELD) { draw_sword(); diff --git a/src/seg009.c b/src/seg009.c index a6c496e7..0aa68896 100644 --- a/src/seg009.c +++ b/src/seg009.c @@ -561,8 +561,11 @@ chtab_type* load_sprites_from_file(int resource,int palette_bits, int quit_on_er int n_images = shpl->n_images; size_t alloc_size = sizeof(chtab_type) + sizeof(void *) * n_images; - chtab_type* chtab = (chtab_type*) malloc(alloc_size); - memset(chtab, 0, alloc_size); + chtab_type* chtab = (chtab_type*) calloc(1, alloc_size); // Use calloc instead of malloc+memset + if (chtab == NULL) { + if (quit_on_error) quit(1); + return NULL; + } chtab->n_images = n_images; for (int i = 1; i <= n_images; i++) { SDL_Surface* image = load_image(resource + i, pal_ptr); @@ -683,9 +686,8 @@ void decompress_rle_ud(byte* destination,const byte* source,int dest_length,int // seg009:90FA byte* decompress_lzg_lr(byte* dest,const byte* source,int dest_length) { - byte* window = (byte*) malloc(0x400); - if (window == NULL) return NULL; - memset(window, 0, 0x400); + // Use stack allocation for small window buffer (1024 bytes) to reduce heap fragmentation + byte window[0x400] = {0}; byte* window_pos = window + 0x400 - 0x42; // bx short remaining = dest_length; // cx byte* window_end = window + 0x400; // dx @@ -725,15 +727,13 @@ byte* decompress_lzg_lr(byte* dest,const byte* source,int dest_length) { } } while (remaining); // end: - free(window); return dest; } // seg009:91AD byte* decompress_lzg_ud(byte* dest,const byte* source,int dest_length,int stride,int height) { - byte* window = (byte*) malloc(0x400); - if (window == NULL) return NULL; - memset(window, 0, 0x400); + // Use stack allocation for small window buffer (1024 bytes) to reduce heap fragmentation + byte window[0x400] = {0}; byte* window_pos = window + 0x400 - 0x42; // bx short remaining = height; // cx byte* window_end = window + 0x400; // dx @@ -784,7 +784,6 @@ byte* decompress_lzg_ud(byte* dest,const byte* source,int dest_length,int stride } } while (dest_length); // end: - free(window); return dest; } @@ -846,8 +845,8 @@ image_type* decode_image(image_data_type* image_data, dat_pal_type* palette) { int cmeth = (flags >> 8) & 0x0F; int stride = calc_stride(image_data); int dest_size = stride * height; - byte* dest = (byte*) malloc(dest_size); - memset(dest, 0, dest_size); + byte* dest = (byte*) calloc(1, dest_size); // Use calloc instead of malloc+memset for better performance + if (dest == NULL) return NULL; decompr_img(dest, image_data, dest_size, cmeth, stride); byte* image_8bpp = conv_to_8bpp(dest, width, height, stride, depth); free(dest); dest = NULL; @@ -983,6 +982,29 @@ int set_joy_mode() { } is_keyboard_mode = !is_joyst_mode; + + // Multiplayer: Initialize Player 2 controller if available + sdl_controller_p2_ = NULL; + sdl_joystick_p2_ = NULL; + using_sdl_joystick_interface_p2 = 0; + if (SDL_NumJoysticks() >= 2) { + if (SDL_IsGameController(1)) { + sdl_controller_p2_ = SDL_GameControllerOpen(1); + if (sdl_controller_p2_ == NULL) { + // Try joystick interface as fallback + sdl_joystick_p2_ = SDL_JoystickOpen(1); + if (sdl_joystick_p2_ != NULL) { + using_sdl_joystick_interface_p2 = 1; + } + } + } else { + sdl_joystick_p2_ = SDL_JoystickOpen(1); + if (sdl_joystick_p2_ != NULL) { + using_sdl_joystick_interface_p2 = 1; + } + } + } + return is_joyst_mode; } @@ -1021,7 +1043,16 @@ void set_hc_pal() { // seg009:2446 void flip_not_ega(byte* memory,int height,int stride) { - byte* row_buffer = (byte*) malloc(stride); + // Use stack allocation for row buffer if stride is reasonable (typically <= 320 for 320x200 screen) + // For larger strides, fall back to heap allocation + byte stack_buffer[320]; + byte* row_buffer; + if (stride <= (int)sizeof(stack_buffer)) { + row_buffer = stack_buffer; + } else { + row_buffer = (byte*) malloc(stride); + if (row_buffer == NULL) return; // Early return on allocation failure + } byte* top_ptr; byte* bottom_ptr; bottom_ptr = top_ptr = memory; @@ -1035,7 +1066,9 @@ void flip_not_ega(byte* memory,int height,int stride) { bottom_ptr -= stride; --rem_rows; } while (rem_rows); - free(row_buffer); + if (row_buffer != stack_buffer) { + free(row_buffer); + } } // seg009:19B1 diff --git a/src/types.h b/src/types.h index 393af629..3f414c15 100644 --- a/src/types.h +++ b/src/types.h @@ -24,15 +24,14 @@ The authors of this program may be contacted at https://forum.princed.org #define STB_VORBIS_HEADER_ONLY #include "stb_vorbis.c" -#if !defined(_MSC_VER) -# include -# include -#else -// These headers for SDL seem to be the pkgconfig/meson standard as per the -// latest versions. If the old ones should be used, the ifdef must be used -// to compare versions. +#if defined(_MSC_VER) +// MSVC uses direct includes # include # include +#else +// MinGW/Linux/other - use SDL2 subdirectory (standard for SDL2) +# include +# include #endif #if SDL_BYTEORDER != SDL_LIL_ENDIAN