#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CACHE_DIR="$REPO_ROOT/.cache" DIST_DIR="$REPO_ROOT/dist" WORK_BASE="$CACHE_DIR/work" DXVK_CACHE_DIR="$CACHE_DIR/dxvk" STEAM_COMPAT_DIR="${HOME}/.local/share/Steam/compatibilitytools.d" UPSTREAM_REPO="GloriousEggroll/proton-ge-custom" PROTON_TAG="GE-Proton10-34" PROTON_ASSET="x86_64" DXVK_CHANNEL="git" DXVK_TAG="latest" RUNTIME_APPID="1628350" TOOL_PREFIX="Elsword-GE" TOOL_NAME="" KEEP_WORK=0 DXVK_SRC_OVERRIDE="" MINGW_PTHREAD_ROOT="${MINGW_PTHREAD_ROOT:-}" INSTALL_TOOL=1 DXVK_DESCRIBE="" DXVK_FULL_SHA="" DXVK_SHORT_SHA="" usage() { cat <<'EOF' Usage: ./build.sh [options] Build a custom Proton-GE compatibility tool for Elsword and replace its bundled DXVK with a chosen DXVK build, defaulting to GE-Proton10-34 plus dxvk-git on Steam Linux Runtime 3.0 (sniper). Options: --proton-tag TAG Build a specific Proton-GE release tag, default: GE-Proton10-34 --proton-asset x86_64|aarch64 Choose which Proton-GE asset to use --dxvk-channel git|stable Choose DXVK source channel, default: git --dxvk-tag TAG Stable DXVK tag to use when --dxvk-channel stable --tool-prefix PREFIX Tool name prefix, default: Elsword-GE --tool-name NAME Override final tool name completely --runtime-appid APPID Steam Runtime appid, default: 1628350 (sniper) --steam-compat-dir PATH Override compatibilitytools.d path --dxvk-src PATH Use an existing dxvk checkout instead of cache --mingw-pthread-root PATH Path with x32/libpthread.a and x64/libpthread.a --dist-dir PATH Output directory for sendable archives --no-install Build/package only, do not install into Steam --keep-work Keep temporary work directory -h, --help Show this help EOF } while [[ $# -gt 0 ]]; do case "$1" in --proton-tag) PROTON_TAG="${2:?missing value for --proton-tag}" shift 2 ;; --proton-asset) PROTON_ASSET="${2:?missing value for --proton-asset}" shift 2 ;; --dxvk-channel) DXVK_CHANNEL="${2:?missing value for --dxvk-channel}" shift 2 ;; --dxvk-tag) DXVK_TAG="${2:?missing value for --dxvk-tag}" shift 2 ;; --tool-prefix) TOOL_PREFIX="${2:?missing value for --tool-prefix}" shift 2 ;; --tool-name) TOOL_NAME="${2:?missing value for --tool-name}" shift 2 ;; --runtime-appid) RUNTIME_APPID="${2:?missing value for --runtime-appid}" shift 2 ;; --steam-compat-dir) STEAM_COMPAT_DIR="${2:?missing value for --steam-compat-dir}" shift 2 ;; --dxvk-src) DXVK_SRC_OVERRIDE="${2:?missing value for --dxvk-src}" shift 2 ;; --mingw-pthread-root) MINGW_PTHREAD_ROOT="${2:?missing value for --mingw-pthread-root}" shift 2 ;; --dist-dir) DIST_DIR="${2:?missing value for --dist-dir}" shift 2 ;; --no-install) INSTALL_TOOL=0 shift ;; --keep-work) KEEP_WORK=1 shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; esac done case "$PROTON_ASSET" in x86_64|aarch64) ;; *) echo "Unsupported --proton-asset: $PROTON_ASSET" >&2 exit 1 ;; esac case "$DXVK_CHANNEL" in git|stable) ;; *) echo "Unsupported --dxvk-channel: $DXVK_CHANNEL" >&2 exit 1 ;; esac require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2 exit 1 } } for cmd in \ curl \ git \ nix \ tar \ meson \ ninja \ python3 \ realpath \ glslangValidator \ x86_64-w64-mingw32-gcc \ x86_64-w64-mingw32-g++ \ i686-w64-mingw32-gcc \ i686-w64-mingw32-g++ \ wine; do require_cmd "$cmd" done mkdir -p "$CACHE_DIR" "$WORK_BASE" "$DIST_DIR" if [[ "$INSTALL_TOOL" -eq 1 ]]; then mkdir -p "$STEAM_COMPAT_DIR" fi WORK_DIR="$(mktemp -d "$WORK_BASE/build.XXXXXX")" cleanup() { if [[ "$KEEP_WORK" -ne 1 ]]; then rm -rf "$WORK_DIR" fi } trap cleanup EXIT say() { printf '\n==> %s\n' "$1" >&2 } tool_version_from_tag() { local tag="$1" local version="${tag#GE-Proton}" if [[ "$version" == "$tag" ]]; then printf '%s\n' "$tag" else printf '%s\n' "$version" fi } get_latest_proton_tag() { curl -fsSL "https://api.github.com/repos/${UPSTREAM_REPO}/releases/latest" \ | grep -oP '"tag_name":\s*"\K[^"]+' \ | head -n1 } get_latest_dxvk_tag() { curl -fsSL https://api.github.com/repos/doitsujin/dxvk/releases/latest \ | grep -oP '"tag_name":\s*"\K[^"]+' \ | head -n1 } prepare_dxvk_cache() { if [[ ! -d "$DXVK_CACHE_DIR/.git" ]]; then say "Clone dxvk" git clone --recursive https://github.com/doitsujin/dxvk.git "$DXVK_CACHE_DIR" >&2 else say "Update dxvk cache" git -C "$DXVK_CACHE_DIR" fetch --tags origin >&2 git -C "$DXVK_CACHE_DIR" checkout master >&2 git -C "$DXVK_CACHE_DIR" reset --hard origin/master >&2 git -C "$DXVK_CACHE_DIR" submodule update --init --recursive >&2 git -C "$DXVK_CACHE_DIR" clean -fdx >&2 fi } prepare_dxvk_repo() { local source_repo ref_name checkout_dir checkout_dir="$WORK_DIR/dxvk-src" if [[ -n "$DXVK_SRC_OVERRIDE" ]]; then source_repo="$DXVK_SRC_OVERRIDE" else prepare_dxvk_cache source_repo="$DXVK_CACHE_DIR" fi if [[ "$DXVK_CHANNEL" == "stable" ]]; then ref_name="$DXVK_TAG" if [[ -z "$ref_name" || "$ref_name" == "latest" ]]; then ref_name="$(get_latest_dxvk_tag)" fi if [[ -z "$ref_name" ]]; then echo "Failed to resolve DXVK stable tag" >&2 exit 1 fi else ref_name="HEAD" fi say "Prepare DXVK source ($DXVK_CHANNEL)" git clone --recursive "$source_repo" "$checkout_dir" >&2 if [[ "$DXVK_CHANNEL" == "stable" ]]; then git -C "$checkout_dir" fetch --tags https://github.com/doitsujin/dxvk.git >&2 git -C "$checkout_dir" checkout "refs/tags/$ref_name" >&2 git -C "$checkout_dir" submodule update --init --recursive >&2 fi echo "$checkout_dir" } build_dxvk() { local dxvk_src="$1" local dxvk_build_dir="$WORK_DIR/dxvk-out/dxvk-${DXVK_CHANNEL}" local toolchain_dir="$WORK_DIR/toolchain" local pthread_dir="$WORK_DIR/mingw-pthread" DXVK_SHORT_SHA="$(git -C "$dxvk_src" rev-parse --short HEAD)" DXVK_FULL_SHA="$(git -C "$dxvk_src" rev-parse HEAD)" DXVK_DESCRIBE="$(git -C "$dxvk_src" describe --always --tags --dirty)" say "Build DXVK ($DXVK_DESCRIBE)" rm -rf "$WORK_DIR/dxvk-out" "$toolchain_dir" "$pthread_dir" mkdir -p "$dxvk_build_dir" "$toolchain_dir" "$pthread_dir/x32" "$pthread_dir/x64" if [[ -n "$MINGW_PTHREAD_ROOT" \ && -f "$MINGW_PTHREAD_ROOT/x32/libpthread.a" \ && -f "$MINGW_PTHREAD_ROOT/x64/libpthread.a" ]]; then ln -sf "$MINGW_PTHREAD_ROOT/x32/libpthread.a" "$pthread_dir/x32/libpthread.a" ln -sf "$MINGW_PTHREAD_ROOT/x64/libpthread.a" "$pthread_dir/x64/libpthread.a" else local mcfg32 mcfg64 mcfg32="$(NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1 nix path-info --impure nixpkgs#pkgsCross.mingw32.windows.mcfgthreads)" mcfg64="$(NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1 nix path-info --impure nixpkgs#pkgsCross.mingwW64.windows.mcfgthreads)" ln -sf "$mcfg32/lib/libmcfgthread.a" "$pthread_dir/x32/libpthread.a" ln -sf "$mcfg64/lib/libmcfgthread.a" "$pthread_dir/x64/libpthread.a" fi cat > "$toolchain_dir/x86_64-w64-mingw32-gcc" < "$toolchain_dir/x86_64-w64-mingw32-g++" < "$toolchain_dir/i686-w64-mingw32-gcc" < "$toolchain_dir/i686-w64-mingw32-g++" < "$toolchain_dir/build-win64-mcfg.txt" < "$toolchain_dir/build-win32-mcfg.txt" <&2 ninja -C "$dxvk_build_dir/build.64" install >&2 meson setup --cross-file "$toolchain_dir/build-win32-mcfg.txt" \ --buildtype release \ --prefix "$dxvk_build_dir" \ --strip \ --bindir x32 \ --libdir x32 \ -Db_ndebug=if-release \ "$dxvk_build_dir/build.32" >&2 ninja -C "$dxvk_build_dir/build.32" install >&2 ) rm -f "$dxvk_build_dir/x64/"*.a "$dxvk_build_dir/x32/"*.a rm -rf "$dxvk_build_dir/build.64" "$dxvk_build_dir/build.32" echo "$DXVK_SHORT_SHA" } download_proton() { local proton_tag="$1" local asset_file local proton_tarball local proton_url if [[ "$PROTON_ASSET" == "aarch64" ]]; then asset_file="${proton_tag}-aarch64.tar.gz" else asset_file="${proton_tag}.tar.gz" fi proton_tarball="$CACHE_DIR/${asset_file}" proton_url="https://github.com/${UPSTREAM_REPO}/releases/download/${proton_tag}/${asset_file}" if [[ ! -f "$proton_tarball" ]]; then say "Download Proton-GE ${proton_tag} (${PROTON_ASSET})" curl -fL "$proton_url" -o "$proton_tarball" else say "Reuse cached Proton tarball" fi echo "$proton_tarball" } replace_tree() { local source_dir="$1" local target_dir="$2" mkdir -p "$target_dir" for dll in d3d8.dll d3d9.dll d3d10core.dll d3d11.dll dxgi.dll openvr_api_dxvk.dll; do if [[ -f "$source_dir/$dll" ]]; then cp -f "$source_dir/$dll" "$target_dir/$dll" fi done } replace_base_dlls() { local source_dir="$1" local target_dir="$2" mkdir -p "$target_dir" for dll in d3d8.dll d3d9.dll d3d10core.dll d3d11.dll ddraw.dll dxgi.dll; do if [[ -f "$source_dir/$dll" ]]; then cp -f "$source_dir/$dll" "$target_dir/$dll" fi done } patch_tool() { local tool_root="$1" local tool_name="$2" local proton_tag="$3" local dxvk_src="$4" local dxvk_short_dir="$WORK_DIR/dxvk-out/dxvk-${DXVK_CHANNEL}" local timestamp timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" say "Replace DXVK payload" replace_tree "$dxvk_short_dir/x64" "$tool_root/files/lib/wine/dxvk/x86_64-windows" replace_tree "$dxvk_short_dir/x32" "$tool_root/files/lib/wine/dxvk/i386-windows" replace_base_dlls "$dxvk_short_dir/x64" "$tool_root/files/lib/wine/x86_64-windows" replace_base_dlls "$dxvk_short_dir/x32" "$tool_root/files/lib/wine/i386-windows" replace_base_dlls "$dxvk_short_dir/x64" "$tool_root/files/share/default_pfx/drive_c/windows/system32" replace_base_dlls "$dxvk_short_dir/x32" "$tool_root/files/share/default_pfx/drive_c/windows/syswow64" chmod u+w \ "$tool_root/files/lib/wine/dxvk/version" \ "$tool_root/version" \ "$tool_root/toolmanifest.vdf" 2>/dev/null || true printf '%s dxvk (%s)\n' "$DXVK_FULL_SHA" "$DXVK_DESCRIBE" > "$tool_root/files/lib/wine/dxvk/version" printf '%s %s\n' "$(date +%s)" "$tool_name" > "$tool_root/version" cat > "$tool_root/compatibilitytool.vdf" < "$tool_root/toolmanifest.vdf" < "$tool_root/BUILD-INFO.txt" < "$DIST_DIR/package-info.env" < "$DIST_DIR/release-notes.md" </dev/null PROTON_TARBALL="$(download_proton "$PROTON_TAG")" say "Extract Proton-GE" mkdir -p "$WORK_DIR/proton" tar -xf "$PROTON_TARBALL" -C "$WORK_DIR/proton" TOOL_ROOT="$(find "$WORK_DIR/proton" -mindepth 1 -maxdepth 1 -type d | head -n1)" if [[ -z "$TOOL_ROOT" ]]; then echo "Could not find extracted Proton tool root" >&2 exit 1 fi patch_tool "$TOOL_ROOT" "$TOOL_NAME" "$PROTON_TAG" "$DXVK_SRC" ARCHIVE_PATH="$(package_tool "$TOOL_ROOT" "$TOOL_NAME")" write_dist_metadata "$ARCHIVE_PATH" "$TOOL_NAME" "$PROTON_TAG" if [[ "$INSTALL_TOOL" -eq 1 ]]; then INSTALLED_PATH="$(install_tool "$TOOL_ROOT" "$TOOL_NAME")" fi say "Done" printf 'Archive: %s\n' "$ARCHIVE_PATH" if [[ "$INSTALL_TOOL" -eq 1 ]]; then printf 'Installed: %s\n' "$INSTALLED_PATH" fi printf 'Proton-GE: %s (%s)\n' "$PROTON_TAG" "$PROTON_ASSET" printf 'DXVK: %s\n' "$DXVK_DESCRIBE" printf 'Runtime: %s\n' "$RUNTIME_APPID" if [[ "$INSTALL_TOOL" -eq 1 ]]; then printf '\nRestart Steam and choose "%s" as compatibility tool.\n' "$TOOL_NAME" else printf '\nShare the archive or unpack it into compatibilitytools.d on another system.\n' fi