#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CACHE_DIR="${CACHE_DIR:-$REPO_ROOT/.cache}" DIST_DIR="${DIST_DIR:-$REPO_ROOT/dist}" WORK_ROOT="${WORK_ROOT:-$CACHE_DIR/work}" DXVK_CACHE_DIR="${DXVK_CACHE_DIR:-$CACHE_DIR/dxvk}" METADATA_DIR="${METADATA_DIR:-$DIST_DIR/metadata}" STEAM_COMPAT_DIR_DEFAULT="${STEAM_COMPAT_DIR_DEFAULT:-$HOME/.local/share/Steam/compatibilitytools.d}" UPSTREAM_REPO="${UPSTREAM_REPO:-GloriousEggroll/proton-ge-custom}" ELSWORD_GE_VERSION="${ELSWORD_GE_VERSION:-11-1}" PROTON_TAG="${PROTON_TAG:-GE-Proton${ELSWORD_GE_VERSION}}" PROTON_ASSET="${PROTON_ASSET:-x86_64}" RUNTIME_APPID="${RUNTIME_APPID:-1628350}" WINE_SERIES="${WINE_SERIES:-Wine 11}" DXVK_STABLE_TAG_DEFAULT="${DXVK_STABLE_TAG_DEFAULT:-latest}" DXVK_GIT_REF="${DXVK_GIT_REF:-origin/master}" MINGW_PTHREAD_ROOT="${MINGW_PTHREAD_ROOT:-}" SAFE_LAUNCH_OPTIONS='LD_PRELOAD="" PROTON_DISCORD_BRIDGE=0 PROTON_USE_OPTISCALER=0 PROTON_USE_D7VK=0 LOW_LATENCY_LAYER=0 MANGOHUD=0 ENABLE_VKBASALT=0 VK_INSTANCE_LAYERS="" DXVK_HUD=0 %command%' PERF_LAUNCH_OPTIONS='LD_PRELOAD="" PROTON_DISCORD_BRIDGE=0 PROTON_USE_OPTISCALER=0 PROTON_USE_D7VK=0 LOW_LATENCY_LAYER=1 MANGOHUD=0 ENABLE_VKBASALT=0 VK_INSTANCE_LAYERS="" DXVK_STATE_CACHE=1 DXVK_LOG_LEVEL=none %command%' DEFAULT_ENV_KEYS=( PROTON_DISCORD_BRIDGE PROTON_USE_OPTISCALER PROTON_USE_D7VK MANGOHUD ENABLE_VKBASALT ) runtime_dll_names=( libstdc++-6.dll libgcc_s_seh-1.dll libgcc_s_dw2-1.dll libwinpthread-1.dll libmcfgthread-2.dll ) say() { printf "\n==> %s\n" "$1" >&2 } die() { printf "Error: %s\n" "$1" >&2 exit 1 } require_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" } require_toolchain_commands() { local cmd for cmd in \ bash \ curl \ git \ jq \ meson \ ninja \ objdump \ python3 \ tar \ zstd \ glslangValidator \ x86_64-w64-mingw32-gcc \ x86_64-w64-mingw32-g++ \ i686-w64-mingw32-gcc \ i686-w64-mingw32-g++; do require_cmd "$cmd" done } ensure_repo_layout() { mkdir -p "$CACHE_DIR" "$WORK_ROOT" "$DIST_DIR" "$METADATA_DIR" } tool_name_for_variant() { case "$1" in stable) printf "Elsword-GE-%s\n" "$ELSWORD_GE_VERSION" ;; dxvk-git) printf "Elsword-GE-%s+dxvk-git\n" "$ELSWORD_GE_VERSION" ;; *) die "unsupported variant: $1" ;; esac } variant_label() { case "$1" in stable) printf "stable\n" ;; dxvk-git) printf "dxvk-git\n" ;; *) die "unsupported variant: $1" ;; esac } dxvk_channel_for_variant() { case "$1" in stable) printf "stable\n" ;; dxvk-git) printf "git\n" ;; *) die "unsupported variant: $1" ;; esac } dxvk_tag_for_variant() { case "$1" in stable) printf "%s\n" "$DXVK_STABLE_TAG_DEFAULT" ;; dxvk-git) printf "git\n" ;; *) die "unsupported variant: $1" ;; esac } archive_name_for_variant() { case "$1" in stable) printf "Elsword-GE-%s.tar.zst\n" "$ELSWORD_GE_VERSION" ;; dxvk-git) printf "Elsword-GE-%s+dxvk-git.tar.zst\n" "$ELSWORD_GE_VERSION" ;; *) die "unsupported variant: $1" ;; esac } archive_alias_for_variant() { case "$1" in stable) printf "%s\n" "$(archive_name_for_variant "$1")" ;; dxvk-git) printf "Elsword-GE-%s-dxvk-git.tar.zst\n" "$ELSWORD_GE_VERSION" ;; *) die "unsupported variant: $1" ;; esac } metadata_file_for_variant() { printf "%s/%s.env\n" "$METADATA_DIR" "$1" } release_title() { printf "Elsword Proton-GE %s\n" "$ELSWORD_GE_VERSION" } workdir_for_variant() { local variant="$1" mktemp -d "$WORK_ROOT/${variant}.XXXXXX" } cleanup_workdir() { local dir="$1" if [[ -n "$dir" && -d "$dir" ]]; then rm -rf "$dir" fi } get_latest_dxvk_tag() { curl -fsSL https://api.github.com/repos/doitsujin/dxvk/releases/latest \ | jq -r '.tag_name' } prepare_dxvk_cache() { if [[ ! -d "$DXVK_CACHE_DIR/.git" ]]; then say "Clone DXVK cache" 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 "$DXVK_GIT_REF" >&2 git -C "$DXVK_CACHE_DIR" submodule update --init --recursive >&2 git -C "$DXVK_CACHE_DIR" clean -fdx >&2 fi } prepare_dxvk_source() { local variant="$1" local workdir="$2" local channel tag checkout_dir channel="$(dxvk_channel_for_variant "$variant")" tag="$(dxvk_tag_for_variant "$variant")" checkout_dir="$workdir/dxvk-src" prepare_dxvk_cache say "Prepare DXVK source ($(variant_label "$variant"))" git clone --recursive "$DXVK_CACHE_DIR" "$checkout_dir" >&2 if [[ "$channel" == "stable" ]]; then if [[ "$tag" == "latest" ]]; then tag="$(get_latest_dxvk_tag)" fi [[ -n "$tag" ]] || die "failed to resolve DXVK stable tag" git -C "$checkout_dir" fetch --tags https://github.com/doitsujin/dxvk.git >&2 git -C "$checkout_dir" checkout "refs/tags/$tag" >&2 git -C "$checkout_dir" submodule update --init --recursive >&2 fi printf "%s\n" "$checkout_dir" } download_proton_tarball() { local asset proton_tarball proton_url if [[ "$PROTON_ASSET" == "aarch64" ]]; then asset="${PROTON_TAG}-aarch64.tar.gz" else asset="${PROTON_TAG}.tar.gz" fi proton_tarball="$CACHE_DIR/$asset" proton_url="https://github.com/${UPSTREAM_REPO}/releases/download/${PROTON_TAG}/${asset}" if [[ ! -f "$proton_tarball" ]]; then say "Download Proton-GE ${PROTON_TAG}" curl -fL "$proton_url" -o "$proton_tarball" else say "Reuse cached Proton tarball" fi printf "%s\n" "$proton_tarball" } extract_proton_tool() { local workdir="$1" local tarball="$2" local proton_root tool_root proton_root="$workdir/proton" mkdir -p "$proton_root" tar -xf "$tarball" -C "$proton_root" tool_root="$(find "$proton_root" -mindepth 1 -maxdepth 1 -type d | head -n1)" [[ -n "$tool_root" ]] || die "could not find extracted Proton root" printf "%s\n" "$tool_root" } inject_wrapper_defaults() { local proton_wrapper="$1" [[ -f "$proton_wrapper" ]] || die "missing proton wrapper: $proton_wrapper" python3 - "$proton_wrapper" <<'PY' from pathlib import Path import sys path = Path(sys.argv[1]) text = path.read_text() marker = 'os.environ.setdefault("PROTON_DISCORD_BRIDGE", "0")' if marker in text: sys.exit(0) block = ( 'os.environ.setdefault("PROTON_DISCORD_BRIDGE", "0")\n' 'os.environ.setdefault("PROTON_USE_OPTISCALER", "0")\n' 'os.environ.setdefault("PROTON_USE_D7VK", "0")\n' 'os.environ.setdefault("MANGOHUD", "0")\n' 'os.environ.setdefault("ENABLE_VKBASALT", "0")\n' ) needle = "import os\n" if needle not in text: raise SystemExit("Could not patch proton wrapper: missing import os") text = text.replace(needle, needle + "\n" + block + "\n", 1) path.write_text(text) PY } resolve_runtime_dll_path() { local arch="$1" local dll="$2" local compiler result nix_attr store_path case "$arch" in x86_64) compiler="x86_64-w64-mingw32-g++" ;; x86) compiler="i686-w64-mingw32-g++" ;; *) die "unsupported arch for runtime lookup: $arch" ;; esac result="$("$compiler" -print-file-name="$dll")" if [[ -n "$result" && "$result" != "$dll" && -f "$result" ]]; then printf "%s\n" "$result" return 0 fi if [[ "$dll" == "libmcfgthread-2.dll" ]]; then case "$arch" in x86_64) nix_attr="pkgsCross.mingwW64.windows.mcfgthreads" ;; x86) nix_attr="pkgsCross.mingw32.windows.mcfgthreads" ;; esac store_path="$(NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1 nix path-info --impure "nixpkgs#${nix_attr}")" result="$(find "$store_path" -name "$dll" -type f | head -n1 || true)" if [[ -n "$result" && -f "$result" ]]; then printf "%s\n" "$result" return 0 fi fi return 1 } copy_dll_to_targets() { local dll_path="$1" shift local target_dir for target_dir in "$@"; do mkdir -p "$target_dir" cp -f "$dll_path" "$target_dir/$(basename "$dll_path")" done } replace_dxvk_payload() { local dxvk_build_dir="$1" local tool_root="$2" local source_x64="$dxvk_build_dir/x64" local source_x32="$dxvk_build_dir/x32" local dll mkdir -p \ "$tool_root/files/lib/wine/dxvk/x86_64-windows" \ "$tool_root/files/lib/wine/dxvk/i386-windows" \ "$tool_root/files/lib/wine/x86_64-windows" \ "$tool_root/files/lib/wine/i386-windows" \ "$tool_root/files/share/default_pfx/drive_c/windows/system32" \ "$tool_root/files/share/default_pfx/drive_c/windows/syswow64" for dll in d3d8.dll d3d9.dll d3d10core.dll d3d11.dll dxgi.dll; do cp -f "$source_x64/$dll" "$tool_root/files/lib/wine/dxvk/x86_64-windows/$dll" cp -f "$source_x32/$dll" "$tool_root/files/lib/wine/dxvk/i386-windows/$dll" cp -f "$source_x64/$dll" "$tool_root/files/lib/wine/x86_64-windows/$dll" cp -f "$source_x32/$dll" "$tool_root/files/lib/wine/i386-windows/$dll" cp -f "$source_x64/$dll" "$tool_root/files/share/default_pfx/drive_c/windows/system32/$dll" cp -f "$source_x32/$dll" "$tool_root/files/share/default_pfx/drive_c/windows/syswow64/$dll" done } patch_tool_manifests() { local tool_root="$1" local tool_name="$2" cat > "$tool_root/compatibilitytool.vdf" < "$tool_root/toolmanifest.vdf" < "$tool_root/version" } ensure_tool_tree_writable() { local tool_root="$1" local path local paths=( "$tool_root/proton" "$tool_root/compatibilitytool.vdf" "$tool_root/toolmanifest.vdf" "$tool_root/version" "$tool_root/files/lib/wine/dxvk/version" "$tool_root/BUILD-INFO.txt" ) for path in "${paths[@]}"; do if [[ -e "$path" ]]; then chmod u+w "$path" fi done } copy_required_runtime_dlls() { local tool_root="$1" local dxvk_build_dir="$2" local metadata_log="$3" local arch dll source_path required local deps targets source_dir dxvk_dll : > "$metadata_log" for arch in x86_64 x86; do if [[ "$arch" == "x86_64" ]]; then source_dir="$dxvk_build_dir/x64" targets=( "$tool_root/files/lib/wine/dxvk/x86_64-windows" "$tool_root/files/lib/wine/x86_64-windows" "$tool_root/files/share/default_pfx/drive_c/windows/system32" ) printf "[x86_64]\n" >> "$metadata_log" else source_dir="$dxvk_build_dir/x32" targets=( "$tool_root/files/lib/wine/dxvk/i386-windows" "$tool_root/files/lib/wine/i386-windows" "$tool_root/files/share/default_pfx/drive_c/windows/syswow64" ) printf "[x86]\n" >> "$metadata_log" fi deps="" for dxvk_dll in d3d9.dll dxgi.dll d3d10core.dll d3d11.dll; do deps+=$'\n'"$(objdump -p "$source_dir/$dxvk_dll" | grep 'DLL Name:' | awk '{print $3}')" done deps="$(printf "%s\n" "$deps" | sed '/^$/d' | sort -u)" printf "%s\n" "$deps" >> "$metadata_log" for dll in "${runtime_dll_names[@]}"; do required=0 if grep -Fxq "$dll" <<<"$deps"; then required=1 fi if [[ "$required" -eq 1 ]]; then source_path="$(resolve_runtime_dll_path "$arch" "$dll" || true)" [[ -n "$source_path" ]] || die "missing runtime dependency $dll for $arch" copy_dll_to_targets "$source_path" "${targets[@]}" fi done done } write_build_metadata() { local variant="$1" local tool_name="$2" local tool_root="$3" local dxvk_channel="$4" local dxvk_tag="$5" local dxvk_commit="$6" local dxvk_describe="$7" local build_time="$8" local archive_name="$9" local archive_alias="${10}" cat > "$(metadata_file_for_variant "$variant")" <