diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..52579ddc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,349 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + build-linux: + name: Build (linux-${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: x86-64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + + - name: Cache apt packages + uses: actions/cache@v4 + with: + path: /var/cache/apt/archives/*.deb + key: apt-${{ matrix.arch }}-${{ hashFiles('.github/workflows/release.yml') }} + restore-keys: apt-${{ matrix.arch }}- + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake \ + build-essential \ + pkg-config \ + libsdl2-dev \ + libglew-dev \ + libglm-dev \ + libssl-dev \ + zlib1g-dev \ + libvulkan-dev \ + vulkan-tools \ + glslc \ + libavformat-dev \ + libavcodec-dev \ + libswscale-dev \ + libavutil-dev \ + libunicorn-dev \ + libx11-dev + sudo apt-get install -y libstormlib-dev || true + + - name: Configure + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --parallel $(nproc) + + - name: Package + run: | + TAG="${GITHUB_REF_NAME}" + STAGING="wowee-${TAG}-linux-${{ matrix.arch }}" + mkdir -p "${STAGING}" + + # Binary + cp build/bin/wowee "${STAGING}/" + + # Asset extraction tool (if built — requires StormLib) + if [ -f build/bin/asset_extract ]; then + cp build/bin/asset_extract "${STAGING}/" + fi + + # Extraction scripts and GUI + cp extract_assets.sh "${STAGING}/" + cp tools/asset_pipeline_gui.py "${STAGING}/" + + # Assets (exclude proprietary music) + rsync -a --exclude='Original Music' build/bin/assets/ "${STAGING}/assets/" + + # Data directory (git-tracked files only) + git ls-files Data/ | while read -r f; do + mkdir -p "${STAGING}/$(dirname "$f")" + cp "$f" "${STAGING}/$f" + done + + tar czf "${STAGING}.tar.gz" "${STAGING}" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: wowee-linux-${{ matrix.arch }} + path: wowee-*.tar.gz + if-no-files-found: error + + build-macos: + name: Build (macOS arm64) + runs-on: macos-15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + + - name: Install dependencies + run: | + brew install cmake pkg-config sdl2 glew glm openssl@3 zlib ffmpeg unicorn \ + stormlib vulkan-loader vulkan-headers shaderc dylibbundler || true + brew install dylibbundler 2>/dev/null || true + + - name: Configure + run: | + BREW=$(brew --prefix) + export PKG_CONFIG_PATH="$BREW/lib/pkgconfig:$(brew --prefix ffmpeg)/lib/pkgconfig:$(brew --prefix openssl@3)/lib/pkgconfig:$(brew --prefix vulkan-loader)/lib/pkgconfig:$(brew --prefix shaderc)/lib/pkgconfig" + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$BREW" \ + -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" + + - name: Build + run: cmake --build build --parallel $(sysctl -n hw.logicalcpu) + + - name: Create .app bundle + run: | + TAG="${GITHUB_REF_NAME}" + mkdir -p Wowee.app/Contents/{MacOS,Frameworks,Resources} + + # Wrapper launch script + printf '#!/bin/bash\ncd "$(dirname "$0")"\nexec ./wowee_bin "$@"\n' \ + > Wowee.app/Contents/MacOS/wowee + chmod +x Wowee.app/Contents/MacOS/wowee + + # Actual binary + cp build/bin/wowee Wowee.app/Contents/MacOS/wowee_bin + + # Asset extraction tool (if built — requires StormLib) + if [ -f build/bin/asset_extract ]; then + cp build/bin/asset_extract Wowee.app/Contents/MacOS/ + fi + + # Extraction scripts and GUI + cp extract_assets.sh Wowee.app/Contents/MacOS/ + cp tools/asset_pipeline_gui.py Wowee.app/Contents/MacOS/ + + # Assets (exclude proprietary music) + rsync -a --exclude='Original Music' build/bin/assets/ \ + Wowee.app/Contents/MacOS/assets/ + + # Data directory (git-tracked files only) + git ls-files Data/ | while read -r f; do + mkdir -p "Wowee.app/Contents/MacOS/$(dirname "$f")" + cp "$f" "Wowee.app/Contents/MacOS/$f" + done + + # Bundle dylibs + if command -v dylibbundler &>/dev/null; then + dylibbundler -od -b \ + -x Wowee.app/Contents/MacOS/wowee_bin \ + -d Wowee.app/Contents/Frameworks/ \ + -p @executable_path/../Frameworks/ + if [ -f Wowee.app/Contents/MacOS/asset_extract ]; then + dylibbundler -od -b \ + -x Wowee.app/Contents/MacOS/asset_extract \ + -d Wowee.app/Contents/Frameworks/ \ + -p @executable_path/../Frameworks/ + fi + fi + + # Info.plist + cat > Wowee.app/Contents/Info.plist << EOF + + + + CFBundleExecutablewowee + CFBundleIdentifiercom.wowee.app + CFBundleNameWowee + CFBundleVersion${TAG} + CFBundleShortVersionString${TAG} + CFBundlePackageTypeAPPL + + EOF + + # Ad-hoc codesign + codesign --force --deep --sign - Wowee.app + + - name: Create DMG + run: | + TAG="${GITHUB_REF_NAME}" + hdiutil create -volname Wowee -srcfolder Wowee.app -ov -format UDZO \ + "wowee-${TAG}-macos-arm64.dmg" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: wowee-macos-arm64 + path: wowee-*.dmg + if-no-files-found: error + + build-windows: + name: Build (windows-${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: x86-64 + runner: windows-latest + msystem: MINGW64 + prefix: mingw-w64-x86_64 + - arch: arm64 + runner: windows-11-arm + msystem: CLANGARM64 + prefix: mingw-w64-clang-aarch64 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.msystem }} + update: ${{ matrix.arch == 'arm64' }} + install: >- + ${{ matrix.prefix }}-cmake + ${{ matrix.arch == 'x86-64' && format('{0}-gcc', matrix.prefix) || format('{0}-clang', matrix.prefix) }} + ${{ matrix.prefix }}-ninja + ${{ matrix.prefix }}-pkgconf + ${{ matrix.prefix }}-SDL2 + ${{ matrix.prefix }}-glew + ${{ matrix.prefix }}-glm + ${{ matrix.prefix }}-openssl + ${{ matrix.prefix }}-zlib + ${{ matrix.prefix }}-ffmpeg + ${{ matrix.prefix }}-unicorn + ${{ matrix.prefix }}-vulkan-loader + ${{ matrix.prefix }}-vulkan-headers + ${{ matrix.prefix }}-shaderc + git + + - name: Install optional packages + shell: msys2 {0} + run: | + pacman -S --noconfirm --needed ${{ matrix.prefix }}-stormlib || true + pacman -S --noconfirm --needed zip + + - name: Configure + shell: msys2 {0} + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + shell: msys2 {0} + run: cmake --build build --parallel $(nproc) + + - name: Bundle DLLs + shell: msys2 {0} + run: | + for exe in build/bin/wowee.exe build/bin/asset_extract.exe; do + [ -f "$exe" ] || continue + ldd "$exe" \ + | awk '/=> \// { print $3 }' \ + | grep -iv '^/c/Windows' \ + | xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true' + done + + - name: Package + shell: msys2 {0} + run: | + TAG="${GITHUB_REF_NAME}" + STAGING="wowee-${TAG}-windows-${{ matrix.arch }}" + mkdir -p "${STAGING}" + + # Binary and DLLs + cp build/bin/wowee.exe "${STAGING}/" + cp build/bin/*.dll "${STAGING}/" 2>/dev/null || true + + # Asset extraction tool (if built — requires StormLib) + if [ -f build/bin/asset_extract.exe ]; then + cp build/bin/asset_extract.exe "${STAGING}/" + fi + + # Extraction scripts and GUI + cp extract_assets.ps1 "${STAGING}/" + cp extract_assets.bat "${STAGING}/" + cp tools/asset_pipeline_gui.py "${STAGING}/" + + # Assets (exclude proprietary music) + mkdir -p "${STAGING}/assets" + for d in build/bin/assets/*/; do + dirname="$(basename "$d")" + [ "$dirname" = "Original Music" ] && continue + cp -r "$d" "${STAGING}/assets/" + done + # Copy top-level asset files + cp build/bin/assets/* "${STAGING}/assets/" 2>/dev/null || true + + # Data directory (git-tracked files only) + git ls-files Data/ | while read -r f; do + mkdir -p "${STAGING}/$(dirname "$f")" + cp "$f" "${STAGING}/$f" + done + + # Create ZIP + zip -r "${STAGING}.zip" "${STAGING}" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: wowee-windows-${{ matrix.arch }} + path: wowee-*.zip + if-no-files-found: error + + release: + name: Create Release + needs: [build-linux, build-macos, build-windows] + runs-on: ubuntu-latest + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + TAG="${GITHUB_REF_NAME}" + + # Collect all release files + FILES=() + for f in artifacts/*/*; do + FILES+=("$f") + done + + gh release create "${TAG}" \ + --title "Wowee ${TAG}" \ + --generate-notes \ + "${FILES[@]}" diff --git a/src/game/warden_module.cpp b/src/game/warden_module.cpp index bad36430..68a2fc9c 100644 --- a/src/game/warden_module.cpp +++ b/src/game/warden_module.cpp @@ -529,34 +529,23 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at " << moduleMemory_ << '\n'; - // Parse skip/copy pairs - // Format: repeated [2B skip_count][2B copy_count][copy_count bytes data] - // Skip = advance dest pointer (zeros), Copy = copy from source to dest - // Terminates when skip_count == 0 + // Parse copy/skip pairs (MaNGOS/TrinityCore format) + // Format: repeated [2B copy_count][copy_count bytes data][2B skip_count] + // Copy = copy from source to dest, Skip = advance dest pointer (zeros) + // Terminates when copy_count == 0 size_t pos = 4; // Skip 4-byte size header size_t destOffset = 0; int pairCount = 0; while (pos + 2 <= exeData.size()) { - // Read skip count (2 bytes LE) - uint16_t skipCount = exeData[pos] | (exeData[pos + 1] << 8); - pos += 2; - - if (skipCount == 0) { - break; // End of skip/copy pairs - } - - // Advance dest pointer by skipCount (gaps are zero-filled from memset) - destOffset += skipCount; - // Read copy count (2 bytes LE) - if (pos + 2 > exeData.size()) { - std::cerr << "[WardenModule] Unexpected end of data reading copy count" << '\n'; - break; - } uint16_t copyCount = exeData[pos] | (exeData[pos + 1] << 8); pos += 2; + if (copyCount == 0) { + break; // End of copy/skip pairs + } + if (copyCount > 0) { if (pos + copyCount > exeData.size()) { std::cerr << "[WardenModule] Copy section extends beyond data bounds" << '\n'; @@ -589,9 +578,19 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { destOffset += copyCount; } + // Read skip count (2 bytes LE) + uint16_t skipCount = 0; + if (pos + 2 <= exeData.size()) { + skipCount = exeData[pos] | (exeData[pos + 1] << 8); + pos += 2; + } + + // Advance dest pointer by skipCount (gaps are zero-filled from memset) + destOffset += skipCount; + pairCount++; - std::cout << "[WardenModule] Pair " << pairCount << ": skip " << skipCount - << ", copy " << copyCount << " (dest offset=" << destOffset << ")" << '\n'; + std::cout << "[WardenModule] Pair " << pairCount << ": copy " << copyCount + << ", skip " << skipCount << " (dest offset=" << destOffset << ")" << '\n'; } // Save position — remaining decompressed data contains relocation entries diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index bacb3aa5..f7ffbdbb 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -294,10 +294,36 @@ std::shared_ptr AssetManager::loadDBC(const std::string& name) { if (dbcData.empty()) { std::string dbcPath = "DBFilesClient\\" + name; dbcData = readFile(dbcPath); - if (dbcData.empty()) { - LOG_WARNING("DBC not found: ", name); - return nullptr; + } + + // If binary DBC not found and we skipped CSV earlier (forceBinaryForVisualDbc), + // try CSV as a last resort — better than no data at all (e.g. Classic expansion + // where binary DBCs come from MPQ extraction the user may not have done). + if (dbcData.empty() && forceBinaryForVisualDbc && !expansionDataPath_.empty()) { + std::string baseName = name; + auto dot = baseName.rfind('.'); + if (dot != std::string::npos) { + baseName = baseName.substr(0, dot); } + std::string csvPath = expansionDataPath_ + "/db/" + baseName + ".csv"; + if (std::filesystem::exists(csvPath)) { + std::ifstream f(csvPath, std::ios::binary | std::ios::ate); + if (f) { + auto size = f.tellg(); + if (size > 0) { + f.seekg(0); + dbcData.resize(static_cast(size)); + f.read(reinterpret_cast(dbcData.data()), size); + LOG_INFO("Binary DBC not found, using CSV fallback: ", csvPath); + loadedFromCSV = true; + } + } + } + } + + if (dbcData.empty()) { + LOG_WARNING("DBC not found: ", name); + return nullptr; } auto dbc = std::make_shared();