feat: initial Elsword-GE builder

This commit is contained in:
magges
2026-06-27 10:26:21 +02:00
commit f3817f7c82
6 changed files with 822 additions and 0 deletions
Executable
+570
View File
@@ -0,0 +1,570 @@
#!/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" <<EOF
#!/usr/bin/env bash
exec "$(command -v x86_64-w64-mingw32-gcc)" "\$@" -L"$pthread_dir/x64" -lmcfgthread -lpthread
EOF
cat > "$toolchain_dir/x86_64-w64-mingw32-g++" <<EOF
#!/usr/bin/env bash
exec "$(command -v x86_64-w64-mingw32-g++)" "\$@" -L"$pthread_dir/x64" -lmcfgthread -lpthread
EOF
cat > "$toolchain_dir/i686-w64-mingw32-gcc" <<EOF
#!/usr/bin/env bash
exec "$(command -v i686-w64-mingw32-gcc)" "\$@" -L"$pthread_dir/x32" -lmcfgthread -lpthread
EOF
cat > "$toolchain_dir/i686-w64-mingw32-g++" <<EOF
#!/usr/bin/env bash
exec "$(command -v i686-w64-mingw32-g++)" "\$@" -L"$pthread_dir/x32" -lmcfgthread -lpthread
EOF
chmod +x \
"$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++"
cat > "$toolchain_dir/build-win64-mcfg.txt" <<EOF
[binaries]
c = '$toolchain_dir/x86_64-w64-mingw32-gcc'
cpp = '$toolchain_dir/x86_64-w64-mingw32-g++'
ar = '$(command -v x86_64-w64-mingw32-ar)'
strip = '$(command -v x86_64-w64-mingw32-strip)'
windres = '$(command -v x86_64-w64-mingw32-windres)'
[properties]
needs_exe_wrapper = true
[host_machine]
system = 'windows'
cpu_family = 'x86_64'
cpu = 'x86_64'
endian = 'little'
EOF
cat > "$toolchain_dir/build-win32-mcfg.txt" <<EOF
[binaries]
c = '$toolchain_dir/i686-w64-mingw32-gcc'
cpp = '$toolchain_dir/i686-w64-mingw32-g++'
ar = '$(command -v i686-w64-mingw32-ar)'
strip = '$(command -v i686-w64-mingw32-strip)'
windres = '$(command -v i686-w64-mingw32-windres)'
[properties]
needs_exe_wrapper = true
[host_machine]
system = 'windows'
cpu_family = 'x86'
cpu = 'x86'
endian = 'little'
EOF
(
cd "$dxvk_src"
meson setup --cross-file "$toolchain_dir/build-win64-mcfg.txt" \
--buildtype release \
--prefix "$dxvk_build_dir" \
--strip \
--bindir x64 \
--libdir x64 \
-Db_ndebug=if-release \
"$dxvk_build_dir/build.64" >&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" <<EOF
"compatibilitytools"
{
"compat_tools"
{
"${tool_name}"
{
"install_path" "."
"display_name" "${tool_name}"
"from_oslist" "windows"
"to_oslist" "linux"
}
}
}
EOF
cat > "$tool_root/toolmanifest.vdf" <<EOF
"manifest"
{
"version" "2"
"commandline" "/proton %verb%"
"require_tool_appid" "${RUNTIME_APPID}"
"use_sessions" "1"
"compatmanager_layer_name" "proton"
}
EOF
cat > "$tool_root/BUILD-INFO.txt" <<EOF
Tool name: ${tool_name}
Built at: ${timestamp}
Upstream repo: ${UPSTREAM_REPO}
Proton-GE tag: ${proton_tag}
Proton asset: ${PROTON_ASSET}
DXVK source: ${dxvk_src}
DXVK commit: ${DXVK_FULL_SHA}
DXVK describe: ${DXVK_DESCRIBE}
DXVK channel: ${DXVK_CHANNEL}
Runtime appid: ${RUNTIME_APPID}
EOF
}
install_tool() {
local tool_root="$1"
local tool_name="$2"
local destination="$STEAM_COMPAT_DIR/$tool_name"
say "Install tool"
rm -rf "$destination"
mv "$tool_root" "$destination"
echo "$destination"
}
package_tool() {
local tool_root="$1"
local tool_name="$2"
local archive_path="$DIST_DIR/${tool_name}-dxvk-${DXVK_SHORT_SHA}.tar.xz"
say "Create shareable archive"
mkdir -p "$DIST_DIR"
rm -f "$archive_path"
tar -C "$(dirname "$tool_root")" -cJf "$archive_path" "$(basename "$tool_root")"
echo "$archive_path"
}
write_dist_metadata() {
local archive_path="$1"
local tool_name="$2"
local proton_tag="$3"
mkdir -p "$DIST_DIR"
cat > "$DIST_DIR/package-info.env" <<EOF
TOOL_NAME=${tool_name}
PROTON_TAG=${proton_tag}
PROTON_VERSION=$(tool_version_from_tag "$proton_tag")
PROTON_ASSET=${PROTON_ASSET}
DXVK_CHANNEL=${DXVK_CHANNEL}
DXVK_TAG=${DXVK_TAG}
DXVK_COMMIT=${DXVK_FULL_SHA}
DXVK_DESCRIBE=${DXVK_DESCRIBE}
RUNTIME_APPID=${RUNTIME_APPID}
ARCHIVE_FILE=$(basename "$archive_path")
RELEASE_TAG=${tool_name}
EOF
cat > "$DIST_DIR/release-notes.md" <<EOF
# ${tool_name}
- Proton-GE: \`${proton_tag}\`
- DXVK: \`${DXVK_DESCRIBE}\`
- Runtime appid: \`${RUNTIME_APPID}\`
- Archive: \`$(basename "$archive_path")\`
This release is built for Elsword and uses Steam Linux Runtime 3.0 (sniper).
EOF
}
if [[ -z "$TOOL_NAME" ]]; then
TOOL_NAME="${TOOL_PREFIX}-$(tool_version_from_tag "$PROTON_TAG")"
fi
DXVK_SRC="$(prepare_dxvk_repo)"
build_dxvk "$DXVK_SRC" >/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