This commit is contained in:
Executable
+238
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/build.sh stable
|
||||
./scripts/build.sh dxvk-git
|
||||
./scripts/build.sh all
|
||||
|
||||
Builds one or both Elsword Proton-GE compatibility tool directories in dist/
|
||||
and validates them before packaging.
|
||||
EOF
|
||||
}
|
||||
|
||||
prepare_toolchain_wrappers() {
|
||||
local workdir="$1"
|
||||
local toolchain_dir="$workdir/toolchain"
|
||||
local pthread_dir="$workdir/mingw-pthread"
|
||||
local mcfg32 mcfg64
|
||||
|
||||
mkdir -p "$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
|
||||
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.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.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
|
||||
|
||||
printf "%s\n" "$toolchain_dir"
|
||||
}
|
||||
|
||||
build_dxvk_variant() {
|
||||
local variant="$1"
|
||||
local dxvk_src="$2"
|
||||
local workdir="$3"
|
||||
local toolchain_dir="$4"
|
||||
local dxvk_build_dir="$workdir/dxvk-out/$variant"
|
||||
|
||||
mkdir -p "$dxvk_build_dir"
|
||||
|
||||
say "Build DXVK for $variant"
|
||||
(
|
||||
cd "$dxvk_src"
|
||||
meson setup --cross-file "$toolchain_dir/build-win64.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.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"
|
||||
|
||||
printf "%s\n" "$dxvk_build_dir"
|
||||
}
|
||||
|
||||
finalize_tool_tree() {
|
||||
local variant="$1"
|
||||
local tool_root="$2"
|
||||
local tool_name="$3"
|
||||
local dxvk_build_dir="$4"
|
||||
local build_time="$5"
|
||||
local dxvk_src="$6"
|
||||
local tool_dir="$DIST_DIR/$tool_name"
|
||||
local metadata_log="$tool_root/runtime-deps.log"
|
||||
local dxvk_channel dxvk_tag dxvk_commit dxvk_describe
|
||||
|
||||
dxvk_channel="$(dxvk_channel_for_variant "$variant")"
|
||||
dxvk_tag="$(dxvk_tag_for_variant "$variant")"
|
||||
dxvk_commit="$(git -C "$dxvk_src" rev-parse HEAD)"
|
||||
dxvk_describe="$(git -C "$dxvk_src" describe --always --tags --dirty)"
|
||||
|
||||
say "Patch tool tree for $tool_name"
|
||||
ensure_tool_tree_writable "$tool_root"
|
||||
replace_dxvk_payload "$dxvk_build_dir" "$tool_root"
|
||||
mkdir -p "$tool_dir"
|
||||
copy_required_runtime_dlls "$tool_root" "$dxvk_build_dir" "$metadata_log"
|
||||
inject_wrapper_defaults "$tool_root/proton"
|
||||
patch_tool_manifests "$tool_root" "$tool_name"
|
||||
printf "%s dxvk (%s)\n" "$dxvk_commit" "$dxvk_describe" > "$tool_root/files/lib/wine/dxvk/version"
|
||||
|
||||
cat > "$tool_root/BUILD-INFO.txt" <<EOF
|
||||
Tool name: ${tool_name}
|
||||
Proton base: ${PROTON_TAG}
|
||||
Wine series: ${WINE_SERIES}
|
||||
Runtime appid: ${RUNTIME_APPID}
|
||||
DXVK channel: ${dxvk_channel}
|
||||
DXVK tag: ${dxvk_tag}
|
||||
DXVK commit: ${dxvk_commit}
|
||||
DXVK describe: ${dxvk_describe}
|
||||
Build time: ${build_time}
|
||||
EOF
|
||||
|
||||
rm -rf "$tool_dir"
|
||||
mv "$tool_root" "$tool_dir"
|
||||
|
||||
write_build_metadata \
|
||||
"$variant" \
|
||||
"$tool_name" \
|
||||
"$tool_dir" \
|
||||
"$dxvk_channel" \
|
||||
"$dxvk_tag" \
|
||||
"$dxvk_commit" \
|
||||
"$dxvk_describe" \
|
||||
"$build_time" \
|
||||
"$(archive_name_for_variant "$variant")" \
|
||||
"$(archive_alias_for_variant "$variant")"
|
||||
|
||||
"$SCRIPT_DIR/validate-build.sh" "$tool_dir" "$tool_name"
|
||||
}
|
||||
|
||||
build_variant() {
|
||||
local variant="$1"
|
||||
local workdir proton_tarball proton_root dxvk_src toolchain_dir dxvk_build_dir tool_name build_time
|
||||
|
||||
tool_name="$(tool_name_for_variant "$variant")"
|
||||
build_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
workdir="$(workdir_for_variant "$variant")"
|
||||
trap "cleanup_workdir '$workdir'" RETURN
|
||||
|
||||
proton_tarball="$(download_proton_tarball)"
|
||||
proton_root="$(extract_proton_tool "$workdir" "$proton_tarball")"
|
||||
dxvk_src="$(prepare_dxvk_source "$variant" "$workdir")"
|
||||
toolchain_dir="$(prepare_toolchain_wrappers "$workdir")"
|
||||
dxvk_build_dir="$(build_dxvk_variant "$variant" "$dxvk_src" "$workdir" "$toolchain_dir")"
|
||||
finalize_tool_tree "$variant" "$proton_root" "$tool_name" "$dxvk_build_dir" "$build_time" "$dxvk_src"
|
||||
}
|
||||
|
||||
main() {
|
||||
local mode="${1:-all}"
|
||||
|
||||
case "$mode" in
|
||||
stable|dxvk-git|all) ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
require_toolchain_commands
|
||||
ensure_repo_layout
|
||||
|
||||
if [[ "$mode" == "all" ]]; then
|
||||
build_variant stable
|
||||
build_variant dxvk-git
|
||||
else
|
||||
build_variant "$mode"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+460
@@ -0,0 +1,460 @@
|
||||
#!/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" <<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
|
||||
|
||||
printf "%s %s\n" "$(date +%s)" "$tool_name" > "$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")" <<EOF
|
||||
VARIANT='${variant}'
|
||||
TOOL_NAME='${tool_name}'
|
||||
PROTON_TAG='${PROTON_TAG}'
|
||||
PROTON_ASSET='${PROTON_ASSET}'
|
||||
WINE_SERIES='${WINE_SERIES}'
|
||||
RUNTIME_APPID='${RUNTIME_APPID}'
|
||||
DXVK_CHANNEL='${dxvk_channel}'
|
||||
DXVK_TAG='${dxvk_tag}'
|
||||
DXVK_COMMIT='${dxvk_commit}'
|
||||
DXVK_DESCRIBE='${dxvk_describe}'
|
||||
BUILD_TIME='${build_time}'
|
||||
TOOL_DIR='${tool_root}'
|
||||
ARCHIVE_NAME='${archive_name}'
|
||||
ARCHIVE_ALIAS='${archive_alias}'
|
||||
SAFE_LAUNCH_OPTIONS='${SAFE_LAUNCH_OPTIONS}'
|
||||
PERFORMANCE_LAUNCH_OPTIONS='${PERF_LAUNCH_OPTIONS}'
|
||||
EOF
|
||||
}
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
main() {
|
||||
local stable_meta git_meta now stable_alias git_alias
|
||||
|
||||
ensure_repo_layout
|
||||
"$SCRIPT_DIR/package.sh" all
|
||||
|
||||
stable_meta="$(metadata_file_for_variant stable)"
|
||||
git_meta="$(metadata_file_for_variant dxvk-git)"
|
||||
[[ -f "$stable_meta" && -f "$git_meta" ]] || die "build metadata missing; run ./scripts/build.sh all first"
|
||||
|
||||
source "$stable_meta"
|
||||
local stable_tool_name="$TOOL_NAME"
|
||||
local stable_dxvk="$DXVK_DESCRIBE"
|
||||
local stable_commit="$DXVK_COMMIT"
|
||||
local stable_archive="$ARCHIVE_NAME"
|
||||
stable_alias="$ARCHIVE_ALIAS"
|
||||
|
||||
source "$git_meta"
|
||||
local git_tool_name="$TOOL_NAME"
|
||||
local git_dxvk="$DXVK_DESCRIBE"
|
||||
local git_commit="$DXVK_COMMIT"
|
||||
local git_archive="$ARCHIVE_NAME"
|
||||
git_alias="$ARCHIVE_ALIAS"
|
||||
|
||||
say "Generate checksums"
|
||||
(
|
||||
cd "$DIST_DIR"
|
||||
sha256sum \
|
||||
"$stable_archive" \
|
||||
"$git_archive" \
|
||||
"$stable_alias" \
|
||||
"$git_alias" > SHA256SUMS.txt
|
||||
awk '{print $1 " " $2}' SHA256SUMS.txt | while read -r sum file; do
|
||||
printf "%s %s\n" "$sum" "$file" > "${file}.sha256"
|
||||
done
|
||||
)
|
||||
|
||||
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
cat > "$DIST_DIR/release-notes.md" <<EOF
|
||||
# $(release_title)
|
||||
|
||||
Build timestamp: \`${now}\`
|
||||
|
||||
## Variants
|
||||
|
||||
- \`${stable_tool_name}\`
|
||||
- Proton base: \`${PROTON_TAG}\`
|
||||
- Wine series: \`${WINE_SERIES}\`
|
||||
- DXVK: \`${stable_dxvk}\`
|
||||
- DXVK commit: \`${stable_commit}\`
|
||||
- Archive: \`${stable_archive}\`
|
||||
|
||||
- \`${git_tool_name}\`
|
||||
- Proton base: \`${PROTON_TAG}\`
|
||||
- Wine series: \`${WINE_SERIES}\`
|
||||
- DXVK: \`${git_dxvk}\`
|
||||
- DXVK commit: \`${git_commit}\`
|
||||
- Archive: \`${git_archive}\`
|
||||
- Alias: \`${git_alias}\`
|
||||
|
||||
## Checksums
|
||||
|
||||
See \`SHA256SUMS.txt\` and the matching \`.sha256\` files in this release.
|
||||
|
||||
## Recommended Elsword launch options
|
||||
|
||||
### Safe / GameGuard Test
|
||||
|
||||
\`\`\`bash
|
||||
${SAFE_LAUNCH_OPTIONS}
|
||||
\`\`\`
|
||||
|
||||
### Performance / Low-Latency Test
|
||||
|
||||
\`\`\`bash
|
||||
${PERF_LAUNCH_OPTIONS}
|
||||
\`\`\`
|
||||
EOF
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STEAM_COMPAT_DIR="${STEAM_COMPAT_DIR:-$HOME/.local/share/Steam/compatibilitytools.d}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/install-to-steam.sh dist/Elsword-GE-11-1.tar.zst
|
||||
./scripts/install-to-steam.sh dist/Elsword-GE-11-1+dxvk-git.tar.zst
|
||||
EOF
|
||||
}
|
||||
|
||||
archive="${1:-}"
|
||||
[[ -n "$archive" ]] || {
|
||||
usage >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -f "$archive" ]] || {
|
||||
printf "Archive not found: %s\n" "$archive" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdir -p "$STEAM_COMPAT_DIR"
|
||||
mapfile -t archive_entries < <(tar -tf "$archive")
|
||||
tool_name="${archive_entries[0]%%/*}"
|
||||
[[ -n "$tool_name" ]] || {
|
||||
printf "Could not determine tool name from archive: %s\n" "$archive" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
install_path="$STEAM_COMPAT_DIR/$tool_name"
|
||||
rm -rf "$install_path"
|
||||
tar --zstd -xf "$archive" -C "$STEAM_COMPAT_DIR"
|
||||
|
||||
printf "Installed tool: %s\n" "$tool_name"
|
||||
printf "Installed path: %s\n" "$install_path"
|
||||
printf "Steam fully restart required.\n"
|
||||
printf "In Steam, open Elsword and select this compatibility tool.\n"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/package.sh stable
|
||||
./scripts/package.sh dxvk-git
|
||||
./scripts/package.sh all
|
||||
EOF
|
||||
}
|
||||
|
||||
package_variant() {
|
||||
local variant="$1"
|
||||
local metadata_file tool_name tool_dir archive_name archive_alias archive_path alias_path
|
||||
|
||||
metadata_file="$(metadata_file_for_variant "$variant")"
|
||||
[[ -f "$metadata_file" ]] || die "missing metadata for variant $variant, run build first"
|
||||
source "$metadata_file"
|
||||
|
||||
tool_name="$(tool_name_for_variant "$variant")"
|
||||
tool_dir="$DIST_DIR/$tool_name"
|
||||
archive_name="$(archive_name_for_variant "$variant")"
|
||||
archive_alias="$(archive_alias_for_variant "$variant")"
|
||||
archive_path="$DIST_DIR/$archive_name"
|
||||
alias_path="$DIST_DIR/$archive_alias"
|
||||
|
||||
[[ -d "$tool_dir" ]] || die "missing built tool dir: $tool_dir"
|
||||
|
||||
say "Package $tool_name"
|
||||
rm -f "$archive_path" "$alias_path"
|
||||
tar --zstd -cf "$archive_path" -C "$DIST_DIR" "$tool_name"
|
||||
|
||||
if [[ "$archive_alias" != "$archive_name" ]]; then
|
||||
cp -f "$archive_path" "$alias_path"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
local mode="${1:-all}"
|
||||
ensure_repo_layout
|
||||
|
||||
case "$mode" in
|
||||
stable) package_variant stable ;;
|
||||
dxvk-git) package_variant dxvk-git ;;
|
||||
all)
|
||||
package_variant stable
|
||||
package_variant dxvk-git
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/validate-build.sh dist/Elsword-GE-11-1 Elsword-GE-11-1
|
||||
EOF
|
||||
}
|
||||
|
||||
tool_dir="${1:-}"
|
||||
tool_name="${2:-}"
|
||||
[[ -n "$tool_dir" && -n "$tool_name" ]] || {
|
||||
usage >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
pass() {
|
||||
printf "[ok] %s\n" "$1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf "[fail] %s\n" "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_exists() {
|
||||
local path="$1"
|
||||
local label="$2"
|
||||
[[ -e "$path" ]] && pass "$label" || fail "$label"
|
||||
}
|
||||
|
||||
check_contains() {
|
||||
local path="$1"
|
||||
local needle="$2"
|
||||
local label="$3"
|
||||
grep -Fq "$needle" "$path" && pass "$label" || fail "$label"
|
||||
}
|
||||
|
||||
dump_dll_report() {
|
||||
local dll_path="$1"
|
||||
local label="$2"
|
||||
local deps arch
|
||||
|
||||
arch="$(objdump -f "$dll_path" | awk -F'file format ' '/file format/ {print $2}' | head -n1)"
|
||||
deps="$(objdump -p "$dll_path" | awk '/DLL Name:/ {print $3}' | sort -u | paste -sd',' -)"
|
||||
printf "[dll] %s arch=%s deps=%s\n" "$label" "${arch:-unknown}" "${deps:-none}"
|
||||
}
|
||||
|
||||
check_packaged_runtime_deps() {
|
||||
local dll_path="$1"
|
||||
local search_dir="$2"
|
||||
local dep
|
||||
|
||||
while read -r dep; do
|
||||
[[ -n "$dep" ]] || continue
|
||||
if ! printf '%s\n' "${runtime_dll_names[@]}" | grep -Fxq "$dep"; then
|
||||
printf "[dep] %s requires %s ... provided by Wine/Windows runtime\n" "$(basename "$dll_path")" "$dep"
|
||||
continue
|
||||
fi
|
||||
printf "[dep] %s requires %s ... " "$(basename "$dll_path")" "$dep"
|
||||
if [[ -f "$search_dir/$dep" || -f "$(dirname "$dll_path")/$dep" ]]; then
|
||||
printf "present\n"
|
||||
else
|
||||
printf "missing\n" >&2
|
||||
exit 1
|
||||
fi
|
||||
done < <(objdump -p "$dll_path" | awk '/DLL Name:/ {print $3}')
|
||||
}
|
||||
|
||||
check_exists "$tool_dir/compatibilitytool.vdf" "compatibilitytool.vdf exists"
|
||||
check_exists "$tool_dir/proton" "proton wrapper exists"
|
||||
[[ -x "$tool_dir/proton" ]] && pass "proton wrapper executable" || fail "proton wrapper executable"
|
||||
check_contains "$tool_dir/compatibilitytool.vdf" "\"$tool_name\"" "tool name in compatibilitytool.vdf matches"
|
||||
check_contains "$tool_dir/compatibilitytool.vdf" "\"display_name\" \"$tool_name\"" "display_name in compatibilitytool.vdf matches"
|
||||
|
||||
for dll in d3d9.dll dxgi.dll d3d10core.dll d3d11.dll; do
|
||||
check_exists "$tool_dir/files/lib/wine/dxvk/i386-windows/$dll" "$dll x86 exists"
|
||||
check_exists "$tool_dir/files/lib/wine/dxvk/x86_64-windows/$dll" "$dll x64 exists"
|
||||
done
|
||||
|
||||
check_contains "$tool_dir/proton" 'os.environ.setdefault("PROTON_DISCORD_BRIDGE", "0")' "PROTON_DISCORD_BRIDGE default 0 in wrapper"
|
||||
check_contains "$tool_dir/proton" 'os.environ.setdefault("PROTON_USE_OPTISCALER", "0")' "PROTON_USE_OPTISCALER default 0 in wrapper"
|
||||
check_contains "$tool_dir/proton" 'os.environ.setdefault("PROTON_USE_D7VK", "0")' "PROTON_USE_D7VK default 0 in wrapper"
|
||||
check_contains "$tool_dir/proton" 'os.environ.setdefault("MANGOHUD", "0")' "MANGOHUD default 0 in wrapper"
|
||||
check_contains "$tool_dir/proton" 'os.environ.setdefault("ENABLE_VKBASALT", "0")' "ENABLE_VKBASALT default 0 in wrapper"
|
||||
if grep -Fq 'LOW_LATENCY_LAYER' "$tool_dir/proton" && grep -Fq 'LOW_LATENCY_LAYER", "0"' "$tool_dir/proton"; then
|
||||
fail "LOW_LATENCY_LAYER must not be hard-disabled"
|
||||
else
|
||||
pass "LOW_LATENCY_LAYER not hard-disabled"
|
||||
fi
|
||||
|
||||
for dll in d3d9.dll dxgi.dll d3d10core.dll d3d11.dll; do
|
||||
dump_dll_report "$tool_dir/files/lib/wine/dxvk/i386-windows/$dll" "x86 $dll"
|
||||
dump_dll_report "$tool_dir/files/lib/wine/dxvk/x86_64-windows/$dll" "x64 $dll"
|
||||
check_packaged_runtime_deps "$tool_dir/files/lib/wine/dxvk/i386-windows/$dll" "$tool_dir/files/share/default_pfx/drive_c/windows/syswow64"
|
||||
check_packaged_runtime_deps "$tool_dir/files/lib/wine/dxvk/x86_64-windows/$dll" "$tool_dir/files/share/default_pfx/drive_c/windows/system32"
|
||||
done
|
||||
|
||||
for runtime_dll in "${runtime_dll_names[@]}"; do
|
||||
if objdump -p "$tool_dir/files/lib/wine/dxvk/i386-windows/"*.dll | grep -Fq "$runtime_dll"; then
|
||||
check_exists "$tool_dir/files/share/default_pfx/drive_c/windows/syswow64/$runtime_dll" "$runtime_dll present for x86 if needed"
|
||||
fi
|
||||
if objdump -p "$tool_dir/files/lib/wine/dxvk/x86_64-windows/"*.dll | grep -Fq "$runtime_dll"; then
|
||||
check_exists "$tool_dir/files/share/default_pfx/drive_c/windows/system32/$runtime_dll" "$runtime_dll present for x64 if needed"
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user