From 23e0101e20c94ca47512c0dfe871f27188a4a038 Mon Sep 17 00:00:00 2001 From: magges Date: Sat, 27 Jun 2026 11:23:40 +0200 Subject: [PATCH] feat: add Elsword GE build project --- .gitea/workflows/build.yml | 93 +++-- .gitignore | 3 +- LICENSE | 21 + README.md | 298 +++++++++++--- build.sh | 570 -------------------------- dist/.gitkeep | 1 + flake.nix | 20 +- patches/proton-defaults-elsword.patch | 36 ++ scripts/build.sh | 238 +++++++++++ scripts/common.sh | 460 +++++++++++++++++++++ scripts/create-release-archive.sh | 88 ++++ scripts/install-to-steam.sh | 41 ++ scripts/package.sh | 65 +++ scripts/validate-build.sh | 111 +++++ 14 files changed, 1378 insertions(+), 667 deletions(-) create mode 100644 LICENSE delete mode 100755 build.sh create mode 100644 dist/.gitkeep create mode 100644 patches/proton-defaults-elsword.patch create mode 100755 scripts/build.sh create mode 100755 scripts/common.sh create mode 100755 scripts/create-release-archive.sh create mode 100755 scripts/install-to-steam.sh create mode 100755 scripts/package.sh create mode 100755 scripts/validate-build.sh diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 47496c7..ca7681e 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,15 +1,18 @@ name: build-elsword-ge on: - schedule: - - cron: "0 4 * * *" + push: + branches: + - main + tags: + - "v*" workflow_dispatch: permissions: contents: write jobs: - build-release: + build: runs-on: self-hosted steps: - name: Checkout repository @@ -20,61 +23,71 @@ jobs: git -c http.extraHeader="Authorization: token ${{ secrets.GITEA_TOKEN }}" fetch --depth 1 origin "${{ gitea.sha }}" git checkout --detach FETCH_HEAD - - name: Build Elsword Proton-GE 10-34 tool + - name: Build both Elsword Proton variants run: | - nix develop -c ./build.sh --no-install + nix develop -c ./scripts/build.sh all + + - name: Package release archives + run: | + nix develop -c ./scripts/create-release-archive.sh + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: elsword-ge-builds + path: | + dist/*.tar.zst + dist/*.sha256 + dist/SHA256SUMS.txt + dist/release-notes.md + profiles/elsword-launch-options.md + if-no-files-found: error - name: Create or update Gitea release + if: startsWith(gitea.ref, 'refs/tags/v') env: GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_TAG: ${{ gitea.ref_name }} GITEA_SHA: ${{ gitea.sha }} run: | nix develop -c bash -lc ' set -euo pipefail - set -a - . dist/package-info.env - set +a RELEASE_API="${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" - RELEASE_TAG_URL="$(python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))" "$RELEASE_TAG")" - ARCHIVE_URL_NAME="$(python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))" "$ARCHIVE_FILE")" + RELEASE_TAG_URL="$(python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))" "$GITEA_TAG")" HTTP_CODE="$(curl -s -o /tmp/release.json -w "%{http_code}" \ -H "Authorization: token ${GITEA_TOKEN}" \ "${RELEASE_API}/tags/${RELEASE_TAG_URL}")" if [ "$HTTP_CODE" = "200" ]; then - RELEASE_ID="$(python3 -c "import json; print(json.load(open(\"/tmp/release.json\"))[\"id\"])" )" + RELEASE_ID="$(jq -r ".id" /tmp/release.json)" elif [ "$HTTP_CODE" = "404" ]; then - python3 -c "import json, pathlib, os; pathlib.Path(\"/tmp/release-create.json\").write_text(json.dumps({ - \"tag_name\": os.environ[\"RELEASE_TAG\"], - \"target_commitish\": os.environ[\"GITEA_SHA\"], - \"name\": os.environ[\"RELEASE_TAG\"], - \"body\": pathlib.Path(\"dist/release-notes.md\").read_text(), - \"draft\": False, - \"prerelease\": False, - }))" + jq -n \ + --arg tag "$GITEA_TAG" \ + --arg sha "$GITEA_SHA" \ + --arg body "$(cat dist/release-notes.md)" \ + "{tag_name: \$tag, target_commitish: \$sha, name: \$tag, body: \$body, draft: false, prerelease: false}" \ + > /tmp/release-create.json curl --fail-with-body \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @/tmp/release-create.json \ "${RELEASE_API}" > /tmp/release.json - RELEASE_ID="$(python3 -c "import json; print(json.load(open(\"/tmp/release.json\"))[\"id\"])" )" + RELEASE_ID="$(jq -r ".id" /tmp/release.json)" else cat /tmp/release.json exit 1 fi - python3 -c "import json, pathlib, os; pathlib.Path(\"/tmp/release-update.json\").write_text(json.dumps({ - \"tag_name\": os.environ[\"RELEASE_TAG\"], - \"target_commitish\": os.environ[\"GITEA_SHA\"], - \"name\": os.environ[\"RELEASE_TAG\"], - \"body\": pathlib.Path(\"dist/release-notes.md\").read_text(), - \"draft\": False, - \"prerelease\": False, - }))" + jq -n \ + --arg tag "$GITEA_TAG" \ + --arg sha "$GITEA_SHA" \ + --arg body "$(cat dist/release-notes.md)" \ + "{tag_name: \$tag, target_commitish: \$sha, name: \$tag, body: \$body, draft: false, prerelease: false}" \ + > /tmp/release-update.json curl --fail-with-body \ -X PATCH \ -H "Authorization: token ${GITEA_TOKEN}" \ @@ -86,16 +99,20 @@ jobs: -H "Authorization: token ${GITEA_TOKEN}" \ "${RELEASE_API}/${RELEASE_ID}/assets" > /tmp/assets.json - EXISTING_ASSET_ID="$(python3 -c "import json, os; data=json.load(open(\"/tmp/assets.json\")); target=os.environ[\"ARCHIVE_FILE\"]; print(next((str(item[\"id\"]) for item in data if item.get(\"name\") == target), \"\"))")" - if [ -n "$EXISTING_ASSET_ID" ]; then + for asset in dist/*.tar.zst dist/*.sha256 dist/SHA256SUMS.txt dist/release-notes.md; do + [ -f "$asset" ] || continue + name="$(basename "$asset")" + existing_id="$(jq -r --arg name "$name" ".[] | select(.name == \$name) | .id" /tmp/assets.json | head -n1)" + if [ -n "$existing_id" ]; then + curl --fail-with-body \ + -X DELETE \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${RELEASE_API}/${RELEASE_ID}/assets/${existing_id}" + fi + encoded_name="$(python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))" "$name")" curl --fail-with-body \ - -X DELETE \ -H "Authorization: token ${GITEA_TOKEN}" \ - "${RELEASE_API}/${RELEASE_ID}/assets/${EXISTING_ASSET_ID}" - fi - - curl --fail-with-body \ - -H "Authorization: token ${GITEA_TOKEN}" \ - --data-binary @"dist/${ARCHIVE_FILE}" \ - "${RELEASE_API}/${RELEASE_ID}/assets?name=${ARCHIVE_URL_NAME}" + --data-binary @"${asset}" \ + "${RELEASE_API}/${RELEASE_ID}/assets?name=${encoded_name}" + done ' diff --git a/.gitignore b/.gitignore index c98b99c..b080c68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .cache/ -dist/ +dist/* +!dist/.gitkeep diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..14fac91 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3b5a5f8..cedd46c 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,281 @@ -# Elsword Proton-GE Builder +# Elsword-GE Build Project -Dieses Repo baut per Gitea Actions versionierte Proton-GE-Kompatibilitätstools -für Elsword mit `dxvk-git` und Steam Linux Runtime 3.0 (`sniper`). +This repository builds two custom Proton-GE 11 compatibility tools for Elsword on Linux/Steam: -## Was gebaut wird +- `Elsword-GE-11-1` +- `Elsword-GE-11-1+dxvk-git` -- Tool-Name: `Elsword-GE-(Version)` -- Beispiel: `Elsword-GE-11-1` -- Runtime: `1628350` (`Steam Linux Runtime 3.0`) -- DXVK: standardmäßig `dxvk-git` -- Ausgabe: versionierte `.tar.xz`-Archive in `dist/` +It is designed for NixOS and for hosting on a private or public Gitea server with Gitea Actions. -Ältere lokal installierte Versionen wie `Elsword-GE-10-30` bleiben bestehen, -weil jede Build ihren eigenen Tool-Namen bekommt. +## What this project does -## Ablauf +- Builds a Proton-GE 11 / Wine 11 based compatibility tool for Elsword. +- Uses Steam Linux Runtime 3.0 (`sniper`, appid `1628350`). +- Builds DXVK for both x86 and x64. +- Produces release archives that other users can install into Steam. +- Disables optional extras that are known to be risky for game stability by default: + - `PROTON_DISCORD_BRIDGE=0` + - `PROTON_USE_OPTISCALER=0` + - `PROTON_USE_D7VK=0` + - `MANGOHUD=0` + - `ENABLE_VKBASALT=0` -1. Das Repo nutzt standardmäßig `GE-Proton10-34`. -2. DXVK wird aus Upstream gebaut, standardmäßig als `dxvk-git`. -3. Die GE-Basis wird auf `sniper` umgestellt. -4. Die DXVK-DLLs in Proton-GE werden ersetzt. -5. Das Ergebnis wird als eigenes Steam-Compatibility-Tool paketiert. -6. Die Gitea Action erstellt oder aktualisiert ein Release und lädt das Archiv hoch. +## What this project does not do -## Standardverhalten +- No GameGuard bypass. +- No anti-cheat bypass. +- No patching or hooking of GameGuard. +- No hard override of `LOW_LATENCY_LAYER`. +- No global hard override of `LD_PRELOAD`. -Ohne Parameter baut `build.sh`: +## Variant difference -- `GE-Proton10-34` -- mit `dxvk-git` -- als Tool `Elsword-GE-(Version)` -- mit `sniper` +- `Elsword-GE-11-1` + - Proton-GE 11 base + - DXVK stable/latest release tag -## Lokaler Build +- `Elsword-GE-11-1+dxvk-git` + - Proton-GE 11 base + - DXVK from current upstream Git -```bash -nix develop -c ./build.sh +## Repository layout + +```text +. +├── flake.nix +├── README.md +├── LICENSE +├── .gitignore +├── .gitea/ +│ └── workflows/ +│ └── build.yml +├── scripts/ +│ ├── common.sh +│ ├── build.sh +│ ├── package.sh +│ ├── install-to-steam.sh +│ ├── validate-build.sh +│ └── create-release-archive.sh +├── patches/ +│ └── proton-defaults-elsword.patch +├── profiles/ +│ └── elsword-launch-options.md +└── dist/ ``` -Nur Archiv bauen, nicht lokal installieren: +## NixOS prerequisites + +Use the provided flake: ```bash -nix develop -c ./build.sh --no-install +nix develop ``` -Einen bestimmten GE-Tag bauen: +The shell includes build dependencies such as: + +- git +- bash +- coreutils +- findutils +- gnused +- gnugrep +- gawk +- jq +- curl +- wget +- tar +- zstd +- meson +- ninja +- python3 +- pkg-config +- cmake +- perl +- patchelf +- binutils +- mingw-w64 toolchains for `i686` and `x86_64` +- Wine / wow64 support + +## Local build + +Build the stable DXVK variant: ```bash -nix develop -c ./build.sh --proton-tag GE-Proton11-1 +nix develop -c ./scripts/build.sh stable ``` -Eigenen Tool-Namen setzen: +Build the DXVK Git variant: ```bash -nix develop -c ./build.sh --tool-name Elsword-GE-test +nix develop -c ./scripts/build.sh dxvk-git ``` -## Wichtige Optionen +Build both: -- `--proton-tag TAG` -- `--proton-asset x86_64|aarch64` -- `--dxvk-channel git|stable` -- `--dxvk-tag TAG` -- `--tool-prefix PREFIX` -- `--tool-name NAME` -- `--runtime-appid APPID` -- `--no-install` +```bash +nix develop -c ./scripts/build.sh all +``` -## Gitea Actions +Package both into release archives: -Der Workflow liegt unter: +```bash +nix develop -c ./scripts/create-release-archive.sh +``` -- `.gitea/workflows/build.yml` +## Output files -Er läuft bei: +After packaging, the main release files are: -- manuellem Start per `workflow_dispatch` -- automatischem Zeitplan täglich +```text +dist/Elsword-GE-11-1.tar.zst +dist/Elsword-GE-11-1+dxvk-git.tar.zst +dist/Elsword-GE-11-1-dxvk-git.tar.zst +dist/SHA256SUMS.txt +dist/release-notes.md +``` -Für Release-Uploads braucht der Workflow ein Secret: +If `+` causes issues in a filesystem or upload target, the safe alias is: + +```text +Elsword-GE-11-1-dxvk-git.tar.zst +``` + +The Steam compatibility tool name still remains exactly: + +```text +Elsword-GE-11-1+dxvk-git +``` + +## Install into Steam + +Install a built archive: + +```bash +./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 +``` + +Target path: + +```text +~/.local/share/Steam/compatibilitytools.d/ +``` + +After install: + +1. Fully restart Steam. +2. Open Elsword properties in Steam. +3. Select the matching compatibility tool. + +## Recommended Elsword launch options + +The documented profiles live in [profiles/elsword-launch-options.md](/home/magges/git/Elsword-GE/profiles/elsword-launch-options.md:1). + +Safe / GameGuard test: + +```bash +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% +``` + +Performance / low-latency test: + +```bash +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% +``` + +Operational advice: + +- Disable Steam Overlay for Elsword if possible. +- Do not launch with `gamemoderun`. +- Keep MangoHud disabled. +- Keep vkBasalt disabled. +- Keep Discord Bridge disabled. +- Keep OptiScaler disabled. +- Keep D7VK disabled. +- Test `LOW_LATENCY_LAYER=1` separately. +- If GameGuard kicks the game, go back to the Safe profile first. + +## Debugging + +For Proton logs: + +```bash +PROTON_LOG=1 %command% +``` + +## Gitea Actions behavior + +Workflow file: + +- [.gitea/workflows/build.yml](/home/magges/git/Elsword-GE/.gitea/workflows/build.yml:1) + +Behavior: + +- Push to `main`: builds both variants and stores build artifacts. +- Tag matching `v*`: builds both variants, packages them, generates checksums, and creates or updates a Gitea release with uploaded assets. + +Expected secrets: - `GITEA_TOKEN` +- `GITEA_SERVER_URL` +- `GITEA_REPOSITORY` -Dieses Token muss Release-Schreibrechte auf das Repo haben. +No tokens or passwords are stored in the repository. -## Release-Ausgabe +## Manual fallback if release upload needs to be done by hand -Nach einem Build schreibt das Skript nach `dist/`: +If your Gitea release upload path differs or the API step needs adjustment for your instance, build locally or through Actions and upload these files manually in the release UI: -- das fertige `.tar.xz`-Archiv -- `package-info.env` -- `release-notes.md` +- `dist/Elsword-GE-11-1.tar.zst` +- `dist/Elsword-GE-11-1+dxvk-git.tar.zst` +- `dist/Elsword-GE-11-1-dxvk-git.tar.zst` +- `dist/SHA256SUMS.txt` +- `dist/*.sha256` +- `dist/release-notes.md` -Die Gitea Action nutzt diese Dateien direkt für das Release. +## Push to Gitea + +```bash +git init +git add . +git commit -m "Initial Elsword-GE build project" +git remote add origin ssh://git@DEIN-GITEA-SERVER/USER/elsword-ge.git +git push -u origin main +``` + +## Create a release by tag + +```bash +git tag v11-1 +git push origin v11-1 +``` + +## Gitea attachment size note + +Built Proton archives are usually much larger than 50 MB. Your Gitea instance may need larger release attachment limits. + +Example configuration hint only; exact keys can vary by Gitea version: + +```ini +[repository.upload] +FILE_MAX_SIZE = 2048 +MAX_FILES = 20 + +[attachment] +MAX_SIZE = 2048 +MAX_FILES = 20 + +[actions] +ENABLED = true +``` + +## Security and anti-cheat note + +This project intentionally avoids: + +- GameGuard bypasses +- anti-cheat bypasses +- GameGuard patches +- GameGuard hooks + +The goal is only a clean Proton-GE build with complete DXVK payloads and conservative defaults for optional extras. diff --git a/build.sh b/build.sh deleted file mode 100755 index 0d78d49..0000000 --- a/build.sh +++ /dev/null @@ -1,570 +0,0 @@ -#!/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 diff --git a/dist/.gitkeep b/dist/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/dist/.gitkeep @@ -0,0 +1 @@ + diff --git a/flake.nix b/flake.nix index eb1f389..6e01fbd 100644 --- a/flake.nix +++ b/flake.nix @@ -1,9 +1,9 @@ { - description = "Build versioned Elsword Proton-GE tools with DXVK and sniper runtime"; + description = "Gitea-ready custom Proton-GE 11 build project for Elsword"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - outputs = { self, nixpkgs }: + outputs = { nixpkgs, ... }: let system = "x86_64-linux"; pkgs = import nixpkgs { inherit system; }; @@ -11,13 +11,29 @@ { devShells.${system}.default = pkgs.mkShell { packages = with pkgs; [ + bash + binutils + cmake + coreutils curl + findutils + gawk git + gnugrep + gnused + gnutar glslang + jq meson ninja + patchelf + perl pkg-config python3 + wget + which + xz + zstd wineWow64Packages.stable pkgsCross.mingw32.stdenv.cc pkgsCross.mingwW64.stdenv.cc diff --git a/patches/proton-defaults-elsword.patch b/patches/proton-defaults-elsword.patch new file mode 100644 index 0000000..21a1fb8 --- /dev/null +++ b/patches/proton-defaults-elsword.patch @@ -0,0 +1,36 @@ +--- a/proton ++++ b/proton +@@ + import os ++ ++os.environ.setdefault("PROTON_DISCORD_BRIDGE", "0") ++os.environ.setdefault("PROTON_USE_OPTISCALER", "0") ++os.environ.setdefault("PROTON_USE_D7VK", "0") ++os.environ.setdefault("MANGOHUD", "0") ++os.environ.setdefault("ENABLE_VKBASALT", "0") +*** Add File: /home/magges/git/Elsword-GE/profiles/elsword-launch-options.md +# Elsword Launch Profiles + +## Safe / GameGuard Test + +```bash +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% +``` + +## Performance / Low-Latency Test + +```bash +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% +``` + +## Notes + +- Disable the Steam Overlay for Elsword if possible. +- Do not start Elsword through `gamemoderun`. +- Keep MangoHud disabled. +- Keep vkBasalt disabled. +- Keep the Discord bridge disabled. +- Keep OptiScaler disabled. +- Keep D7VK disabled. +- Test `LOW_LATENCY_LAYER=1` on its own before mixing other changes in. +- If GameGuard kicks or closes the game, return to the Safe profile first. diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..be4a4e7 --- /dev/null +++ b/scripts/build.sh @@ -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" < "$toolchain_dir/x86_64-w64-mingw32-g++" < "$toolchain_dir/i686-w64-mingw32-gcc" < "$toolchain_dir/i686-w64-mingw32-g++" < "$toolchain_dir/build-win64.txt" < "$toolchain_dir/build-win32.txt" <&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" <&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 "$@" diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 0000000..08308e2 --- /dev/null +++ b/scripts/common.sh @@ -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" < "$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")" < 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" <&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" diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100755 index 0000000..63d5f81 --- /dev/null +++ b/scripts/package.sh @@ -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 "$@" diff --git a/scripts/validate-build.sh b/scripts/validate-build.sh new file mode 100755 index 0000000..814d98e --- /dev/null +++ b/scripts/validate-build.sh @@ -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