Installer stalker-anomaly-anthology-21-final

game:
  arch: win64
  exe: $GAMEDIR/prefix/drive_c/ANTHOLOGY/Anomaly-1.5.3-Anthology 2.1/AnomalyLauncher.exe
  prefix: $GAMEDIR/prefix
  working_dir: $GAMEDIR/prefix/drive_c/ANTHOLOGY/Anomaly-1.5.3-Anthology 2.1
installer:
- mkdir: $GAMEDIR/install-media
- write_file:
    content: "import hashlib\nimport json\nimport shutil\nimport sys\nimport time\n\
      from pathlib import Path\nfrom urllib.parse import urlencode\nfrom urllib.request\
      \ import Request, urlopen\n\n# Official 2.1 Final public folder, listed on AP-PRO.ru.\n\
      SHARE = \"https://disk.yandex.ru/d/zBo4EybWJyuplA\"\nAPI = \"https://cloud-api.yandex.net/v1/disk/public/resources\"\
      \nUSER_AGENT = \"Lutris-Anomaly-Anthology/0.2\"\nREQUIRED = (\n    \"ANTHOLOGY2_1_Final_setup.exe\"\
      ,\n    \"Anthology_Game.bin\",\n    \"Anthology_Modpack.bin\",\n)\nCHUNK = 8\
      \ * 1024 * 1024\n\n\ndef api(endpoint=\"\", **params):\n    url = API + endpoint\
      \ + \"?\" + urlencode(params)\n    req = Request(url, headers={\"User-Agent\"\
      : USER_AGENT})\n    with urlopen(req, timeout=75) as response:\n        return\
      \ json.load(response)\n\n\ndef list_official_files():\n    # The public share\
      \ can be a folder with nested subfolders.\n    found = {}\n    queue = [\"\"\
      ]\n    while queue and len(found) < len(REQUIRED):\n        folder = queue.pop(0)\n\
      \        offset = 0\n        while True:\n            parameters = {\"public_key\"\
      : SHARE, \"limit\": 100, \"offset\": offset}\n            if folder:\n     \
      \           parameters[\"path\"] = folder\n            data = api(**parameters)\n\
      \            entries = data.get(\"_embedded\", {}).get(\"items\", [])\n    \
      \        for item in entries:\n                path = folder + \"/\" + item[\"\
      name\"]\n                if item.get(\"type\") == \"dir\":\n               \
      \     queue.append(path)\n                elif item.get(\"type\") == \"file\"\
      \ and item[\"name\"] in REQUIRED:\n                    if item[\"name\"] in\
      \ found:\n                        raise RuntimeError(\"Duplicate required file:\
      \ \" + item[\"name\"])\n                    size = item.get(\"size\")\n    \
      \                if not isinstance(size, int) or size <= 0:\n              \
      \          raise RuntimeError(\"Invalid file size: \" + item[\"name\"])\n  \
      \                  found[item[\"name\"]] = (path, size, item.get(\"md5\"))\n\
      \            offset += len(entries)\n            if not entries or offset >=\
      \ data.get(\"_embedded\", {}).get(\"total\", offset):\n                break\n\
      \    missing = set(REQUIRED) - found.keys()\n    if missing:\n        raise\
      \ RuntimeError(\"Official share does not contain expected files: \" + \", \"\
      .join(sorted(missing)))\n    return found\n\n\ndef md5_file(path):\n    h =\
      \ hashlib.md5()\n    with path.open(\"rb\") as stream:\n        for chunk in\
      \ iter(lambda: stream.read(CHUNK), b\"\"):\n            h.update(chunk)\n  \
      \  return h.hexdigest()\n\n\ndef file_ok(path, expected_size, expected_md5):\n\
      \    if not path.is_file() or path.stat().st_size != expected_size:\n      \
      \  return False\n    return not expected_md5 or md5_file(path).lower() == expected_md5.lower()\n\
      \n\ndef fetch_one(dest, name, path, size, expected_md5):\n    final = dest /\
      \ name\n    partial = dest / (name + \".part\")\n    if file_ok(final, size,\
      \ expected_md5):\n        print(\"Verified existing \" + name, flush=True)\n\
      \        return\n    if final.exists():\n        final.unlink()\n    if partial.exists()\
      \ and partial.stat().st_size > size:\n        partial.unlink()\n\n    print(\"\
      Downloading \" + name, flush=True)\n    for attempt in range(8):\n        downloaded\
      \ = partial.stat().st_size if partial.exists() else 0\n        if downloaded\
      \ == size:\n            break\n        try:\n            link = api(\"/download\"\
      , public_key=SHARE, path=path)[\"href\"]\n            headers = {\"User-Agent\"\
      : USER_AGENT}\n            if downloaded:\n                headers[\"Range\"\
      ] = \"bytes=\" + str(downloaded) + \"-\"\n            with urlopen(Request(link,\
      \ headers=headers), timeout=180) as response:\n                if downloaded:\n\
      \                    if response.status != 206 or not response.headers.get(\"\
      Content-Range\", \"\").startswith(\"bytes \" + str(downloaded) + \"-\"):\n \
      \                       # The server did not honor our partial download. Restart\
      \ safely.\n                        partial.unlink(missing_ok=True)\n       \
      \                 raise IOError(\"Host does not support resuming this request\"\
      )\n                next_report = downloaded + 512 * 1024 * 1024\n          \
      \      with partial.open(\"ab\" if downloaded else \"wb\") as output:\n    \
      \                while True:\n                        chunk = response.read(CHUNK)\n\
      \                        if not chunk:\n                            break\n\
      \                        output.write(chunk)\n                        downloaded\
      \ += len(chunk)\n                        if downloaded >= next_report:\n   \
      \                         print(\"  \" + name + \": \" + f\"{100 * downloaded\
      \ / size:.1f}%\", flush=True)\n                            next_report = downloaded\
      \ + 512 * 1024 * 1024\n            if partial.stat().st_size != size:\n    \
      \            raise IOError(\"Transfer incomplete\")\n            break\n   \
      \     except (OSError, ValueError, KeyError) as error:\n            if attempt\
      \ == 7:\n                raise RuntimeError(\"Unable to download \" + name +\
      \ \" from the official Yandex share\") from error\n            print(\"  Download\
      \ interrupted: \" + str(error) + \"; retrying\", flush=True)\n            time.sleep(min(2\
      \ ** attempt, 30))\n\n    if not file_ok(partial, size, expected_md5):\n   \
      \     raise RuntimeError(\"File integrity check failed: \" + name)\n    partial.replace(final)\n\
      \    print(\"Finished and verified \" + name, flush=True)\n\n\ndef main():\n\
      \    dest = Path(sys.argv[1]).resolve()\n    dest.mkdir(parents=True, exist_ok=True)\n\
      \    print(\"Checking official Anomaly Anthology 2.1 Final download folder\"\
      , flush=True)\n    files = list_official_files()\n    remaining = 0\n    for\
      \ name in REQUIRED:\n        _, size, checksum = files[name]\n        final\
      \ = dest / name\n        partial = dest / (name + \".part\")\n        if file_ok(final,\
      \ size, checksum):\n            continue\n        remaining += size - min(partial.stat().st_size\
      \ if partial.exists() else 0, size)\n\n    # Both downloaded installation media\
      \ and unpacked game reside under $GAMEDIR.\n    # The full game and modpack\
      \ require about 113 GB after extraction.\n    required_space = remaining + 113_000_000_000\n\
      \    available = shutil.disk_usage(dest).free\n    if available < required_space:\n\
      \        deficit = (required_space - available + 999_999_999) // 1_000_000_000\n\
      \        raise RuntimeError(f\"Not enough free disk space. Need about {deficit}\
      \ GB more for the full installation\")\n\n    for name in REQUIRED:\n      \
      \  path, size, checksum = files[name]\n        fetch_one(dest, name, path, size,\
      \ checksum)\n    print(\"All three installation files are ready.\", flush=True)\n\
      \n\nif __name__ == \"__main__\":\n    main()\n"
    file: $GAMEDIR/download-anthology.py
- execute:
    command: python3 "$GAMEDIR/download-anthology.py" "$GAMEDIR/install-media"
    disable_runtime: true
    working_dir: $GAMEDIR
- task:
    arch: win64
    name: create_prefix
    prefix: $GAMEDIR/prefix
- task:
    app: vcrun2022
    arch: win64
    description: Installing Microsoft Visual C++ 2015-2022 runtime
    name: winetricks
    prefix: $GAMEDIR/prefix
- task:
    arch: win64
    description: Install Anthology into C:\ANTHOLOGY
    executable: $GAMEDIR/install-media/ANTHOLOGY2_1_Final_setup.exe
    name: wineexec
    prefix: $GAMEDIR/prefix
    working_dir: $GAMEDIR/install-media
wine:
  dxvk: true
description: Automatically downloads and installs S.T.A.L.K.E.R. Anomaly Anthology
  2.1 Final, including the standalone game and optional modpack, using the official
  release files hosted on Yandex Disk. Creates an isolated 64-bit Wine prefix and
  installs Microsoft Visual C++ 2022.
game_slug: stalker-anomaly-anthology
gogslug: ''
humblestoreid: ''
installer_slug: stalker-anomaly-anthology-21-final
name: S.T.A.L.K.E.R. Anomaly Anthology
notes: "Requires approximately 180\u2013200 GB of free disk space during installation.\
  \ The installer downloads the three official release files using Python 3 and the\
  \ Yandex Disk public API. No external download manager or torrent client is required.\r\
  \n\r\nDuring the Windows setup wizard, select C:\\ANTHOLOGY as the installation\
  \ destination. The Lutris script creates a 64-bit Wine prefix and installs vcrun2022\
  \ to resolve a known Wine concrt140.dll compatibility issue.\r\n\r\nDirectX 11 with\
  \ DXVK is recommended. To use the modpack, select \"Play with modpack\" in the Anthology\
  \ launcher, then launch Anomaly (DX11) or Anomaly (DX11-AVX) through Mod Organizer\
  \ 2.\r\n\r\nTested on CachyOS Linux with Proton-CachyOS and an NVIDIA RTX 4070 SUPER.\
  \ Compatibility with other Wine runners and Linux distributions has not yet been\
  \ verified."
runner: wine
script:
  game:
    arch: win64
    exe: $GAMEDIR/prefix/drive_c/ANTHOLOGY/Anomaly-1.5.3-Anthology 2.1/AnomalyLauncher.exe
    prefix: $GAMEDIR/prefix
    working_dir: $GAMEDIR/prefix/drive_c/ANTHOLOGY/Anomaly-1.5.3-Anthology 2.1
  installer:
  - mkdir: $GAMEDIR/install-media
  - write_file:
      content: "import hashlib\nimport json\nimport shutil\nimport sys\nimport time\n\
        from pathlib import Path\nfrom urllib.parse import urlencode\nfrom urllib.request\
        \ import Request, urlopen\n\n# Official 2.1 Final public folder, listed on\
        \ AP-PRO.ru.\nSHARE = \"https://disk.yandex.ru/d/zBo4EybWJyuplA\"\nAPI = \"\
        https://cloud-api.yandex.net/v1/disk/public/resources\"\nUSER_AGENT = \"Lutris-Anomaly-Anthology/0.2\"\
        \nREQUIRED = (\n    \"ANTHOLOGY2_1_Final_setup.exe\",\n    \"Anthology_Game.bin\"\
        ,\n    \"Anthology_Modpack.bin\",\n)\nCHUNK = 8 * 1024 * 1024\n\n\ndef api(endpoint=\"\
        \", **params):\n    url = API + endpoint + \"?\" + urlencode(params)\n   \
        \ req = Request(url, headers={\"User-Agent\": USER_AGENT})\n    with urlopen(req,\
        \ timeout=75) as response:\n        return json.load(response)\n\n\ndef list_official_files():\n\
        \    # The public share can be a folder with nested subfolders.\n    found\
        \ = {}\n    queue = [\"\"]\n    while queue and len(found) < len(REQUIRED):\n\
        \        folder = queue.pop(0)\n        offset = 0\n        while True:\n\
        \            parameters = {\"public_key\": SHARE, \"limit\": 100, \"offset\"\
        : offset}\n            if folder:\n                parameters[\"path\"] =\
        \ folder\n            data = api(**parameters)\n            entries = data.get(\"\
        _embedded\", {}).get(\"items\", [])\n            for item in entries:\n  \
        \              path = folder + \"/\" + item[\"name\"]\n                if\
        \ item.get(\"type\") == \"dir\":\n                    queue.append(path)\n\
        \                elif item.get(\"type\") == \"file\" and item[\"name\"] in\
        \ REQUIRED:\n                    if item[\"name\"] in found:\n           \
        \             raise RuntimeError(\"Duplicate required file: \" + item[\"name\"\
        ])\n                    size = item.get(\"size\")\n                    if\
        \ not isinstance(size, int) or size <= 0:\n                        raise RuntimeError(\"\
        Invalid file size: \" + item[\"name\"])\n                    found[item[\"\
        name\"]] = (path, size, item.get(\"md5\"))\n            offset += len(entries)\n\
        \            if not entries or offset >= data.get(\"_embedded\", {}).get(\"\
        total\", offset):\n                break\n    missing = set(REQUIRED) - found.keys()\n\
        \    if missing:\n        raise RuntimeError(\"Official share does not contain\
        \ expected files: \" + \", \".join(sorted(missing)))\n    return found\n\n\
        \ndef md5_file(path):\n    h = hashlib.md5()\n    with path.open(\"rb\") as\
        \ stream:\n        for chunk in iter(lambda: stream.read(CHUNK), b\"\"):\n\
        \            h.update(chunk)\n    return h.hexdigest()\n\n\ndef file_ok(path,\
        \ expected_size, expected_md5):\n    if not path.is_file() or path.stat().st_size\
        \ != expected_size:\n        return False\n    return not expected_md5 or\
        \ md5_file(path).lower() == expected_md5.lower()\n\n\ndef fetch_one(dest,\
        \ name, path, size, expected_md5):\n    final = dest / name\n    partial =\
        \ dest / (name + \".part\")\n    if file_ok(final, size, expected_md5):\n\
        \        print(\"Verified existing \" + name, flush=True)\n        return\n\
        \    if final.exists():\n        final.unlink()\n    if partial.exists() and\
        \ partial.stat().st_size > size:\n        partial.unlink()\n\n    print(\"\
        Downloading \" + name, flush=True)\n    for attempt in range(8):\n       \
        \ downloaded = partial.stat().st_size if partial.exists() else 0\n       \
        \ if downloaded == size:\n            break\n        try:\n            link\
        \ = api(\"/download\", public_key=SHARE, path=path)[\"href\"]\n          \
        \  headers = {\"User-Agent\": USER_AGENT}\n            if downloaded:\n  \
        \              headers[\"Range\"] = \"bytes=\" + str(downloaded) + \"-\"\n\
        \            with urlopen(Request(link, headers=headers), timeout=180) as\
        \ response:\n                if downloaded:\n                    if response.status\
        \ != 206 or not response.headers.get(\"Content-Range\", \"\").startswith(\"\
        bytes \" + str(downloaded) + \"-\"):\n                        # The server\
        \ did not honor our partial download. Restart safely.\n                  \
        \      partial.unlink(missing_ok=True)\n                        raise IOError(\"\
        Host does not support resuming this request\")\n                next_report\
        \ = downloaded + 512 * 1024 * 1024\n                with partial.open(\"ab\"\
        \ if downloaded else \"wb\") as output:\n                    while True:\n\
        \                        chunk = response.read(CHUNK)\n                  \
        \      if not chunk:\n                            break\n                \
        \        output.write(chunk)\n                        downloaded += len(chunk)\n\
        \                        if downloaded >= next_report:\n                 \
        \           print(\"  \" + name + \": \" + f\"{100 * downloaded / size:.1f}%\"\
        , flush=True)\n                            next_report = downloaded + 512\
        \ * 1024 * 1024\n            if partial.stat().st_size != size:\n        \
        \        raise IOError(\"Transfer incomplete\")\n            break\n     \
        \   except (OSError, ValueError, KeyError) as error:\n            if attempt\
        \ == 7:\n                raise RuntimeError(\"Unable to download \" + name\
        \ + \" from the official Yandex share\") from error\n            print(\"\
        \  Download interrupted: \" + str(error) + \"; retrying\", flush=True)\n \
        \           time.sleep(min(2 ** attempt, 30))\n\n    if not file_ok(partial,\
        \ size, expected_md5):\n        raise RuntimeError(\"File integrity check\
        \ failed: \" + name)\n    partial.replace(final)\n    print(\"Finished and\
        \ verified \" + name, flush=True)\n\n\ndef main():\n    dest = Path(sys.argv[1]).resolve()\n\
        \    dest.mkdir(parents=True, exist_ok=True)\n    print(\"Checking official\
        \ Anomaly Anthology 2.1 Final download folder\", flush=True)\n    files =\
        \ list_official_files()\n    remaining = 0\n    for name in REQUIRED:\n  \
        \      _, size, checksum = files[name]\n        final = dest / name\n    \
        \    partial = dest / (name + \".part\")\n        if file_ok(final, size,\
        \ checksum):\n            continue\n        remaining += size - min(partial.stat().st_size\
        \ if partial.exists() else 0, size)\n\n    # Both downloaded installation\
        \ media and unpacked game reside under $GAMEDIR.\n    # The full game and\
        \ modpack require about 113 GB after extraction.\n    required_space = remaining\
        \ + 113_000_000_000\n    available = shutil.disk_usage(dest).free\n    if\
        \ available < required_space:\n        deficit = (required_space - available\
        \ + 999_999_999) // 1_000_000_000\n        raise RuntimeError(f\"Not enough\
        \ free disk space. Need about {deficit} GB more for the full installation\"\
        )\n\n    for name in REQUIRED:\n        path, size, checksum = files[name]\n\
        \        fetch_one(dest, name, path, size, checksum)\n    print(\"All three\
        \ installation files are ready.\", flush=True)\n\n\nif __name__ == \"__main__\"\
        :\n    main()\n"
      file: $GAMEDIR/download-anthology.py
  - execute:
      command: python3 "$GAMEDIR/download-anthology.py" "$GAMEDIR/install-media"
      disable_runtime: true
      working_dir: $GAMEDIR
  - task:
      arch: win64
      name: create_prefix
      prefix: $GAMEDIR/prefix
  - task:
      app: vcrun2022
      arch: win64
      description: Installing Microsoft Visual C++ 2015-2022 runtime
      name: winetricks
      prefix: $GAMEDIR/prefix
  - task:
      arch: win64
      description: Install Anthology into C:\ANTHOLOGY
      executable: $GAMEDIR/install-media/ANTHOLOGY2_1_Final_setup.exe
      name: wineexec
      prefix: $GAMEDIR/prefix
      working_dir: $GAMEDIR/install-media
  wine:
    dxvk: true
slug: stalker-anomaly-anthology-21-final
steamid: null
version: 2.1 Final
year: 2023
{
  "game_slug": "stalker-anomaly-anthology",
  "version": "2.1 Final",
  "description": "Automatically downloads and installs S.T.A.L.K.E.R. Anomaly Anthology 2.1 Final, including the standalone game and optional modpack, using the official release files hosted on Yandex Disk. Creates an isolated 64-bit Wine prefix and installs Microsoft Visual C++ 2022.",
  "notes": "Requires approximately 180\u2013200 GB of free disk space during installation. The installer downloads the three official release files using Python 3 and the Yandex Disk public API. No external download manager or torrent client is required.\r\n\r\nDuring the Windows setup wizard, select C:\\ANTHOLOGY as the installation destination. The Lutris script creates a 64-bit Wine prefix and installs vcrun2022 to resolve a known Wine concrt140.dll compatibility issue.\r\n\r\nDirectX 11 with DXVK is recommended. To use the modpack, select \"Play with modpack\" in the Anthology launcher, then launch Anomaly (DX11) or Anomaly (DX11-AVX) through Mod Organizer 2.\r\n\r\nTested on CachyOS Linux with Proton-CachyOS and an NVIDIA RTX 4070 SUPER. Compatibility with other Wine runners and Linux distributions has not yet been verified.",
  "name": "S.T.A.L.K.E.R. Anomaly Anthology",
  "year": 2023,
  "steamid": null,
  "gogslug": "",
  "humblestoreid": "",
  "runner": "wine",
  "slug": "stalker-anomaly-anthology-21-final",
  "installer_slug": "stalker-anomaly-anthology-21-final",
  "script": {
    "game": {
      "arch": "win64",
      "exe": "$GAMEDIR/prefix/drive_c/ANTHOLOGY/Anomaly-1.5.3-Anthology 2.1/AnomalyLauncher.exe",
      "prefix": "$GAMEDIR/prefix",
      "working_dir": "$GAMEDIR/prefix/drive_c/ANTHOLOGY/Anomaly-1.5.3-Anthology 2.1"
    },
    "installer": [
      {
        "mkdir": "$GAMEDIR/install-media"
      },
      {
        "write_file": {
          "content": "import hashlib\nimport json\nimport shutil\nimport sys\nimport time\nfrom pathlib import Path\nfrom urllib.parse import urlencode\nfrom urllib.request import Request, urlopen\n\n# Official 2.1 Final public folder, listed on AP-PRO.ru.\nSHARE = \"https://disk.yandex.ru/d/zBo4EybWJyuplA\"\nAPI = \"https://cloud-api.yandex.net/v1/disk/public/resources\"\nUSER_AGENT = \"Lutris-Anomaly-Anthology/0.2\"\nREQUIRED = (\n    \"ANTHOLOGY2_1_Final_setup.exe\",\n    \"Anthology_Game.bin\",\n    \"Anthology_Modpack.bin\",\n)\nCHUNK = 8 * 1024 * 1024\n\n\ndef api(endpoint=\"\", **params):\n    url = API + endpoint + \"?\" + urlencode(params)\n    req = Request(url, headers={\"User-Agent\": USER_AGENT})\n    with urlopen(req, timeout=75) as response:\n        return json.load(response)\n\n\ndef list_official_files():\n    # The public share can be a folder with nested subfolders.\n    found = {}\n    queue = [\"\"]\n    while queue and len(found) < len(REQUIRED):\n        folder = queue.pop(0)\n        offset = 0\n        while True:\n            parameters = {\"public_key\": SHARE, \"limit\": 100, \"offset\": offset}\n            if folder:\n                parameters[\"path\"] = folder\n            data = api(**parameters)\n            entries = data.get(\"_embedded\", {}).get(\"items\", [])\n            for item in entries:\n                path = folder + \"/\" + item[\"name\"]\n                if item.get(\"type\") == \"dir\":\n                    queue.append(path)\n                elif item.get(\"type\") == \"file\" and item[\"name\"] in REQUIRED:\n                    if item[\"name\"] in found:\n                        raise RuntimeError(\"Duplicate required file: \" + item[\"name\"])\n                    size = item.get(\"size\")\n                    if not isinstance(size, int) or size <= 0:\n                        raise RuntimeError(\"Invalid file size: \" + item[\"name\"])\n                    found[item[\"name\"]] = (path, size, item.get(\"md5\"))\n            offset += len(entries)\n            if not entries or offset >= data.get(\"_embedded\", {}).get(\"total\", offset):\n                break\n    missing = set(REQUIRED) - found.keys()\n    if missing:\n        raise RuntimeError(\"Official share does not contain expected files: \" + \", \".join(sorted(missing)))\n    return found\n\n\ndef md5_file(path):\n    h = hashlib.md5()\n    with path.open(\"rb\") as stream:\n        for chunk in iter(lambda: stream.read(CHUNK), b\"\"):\n            h.update(chunk)\n    return h.hexdigest()\n\n\ndef file_ok(path, expected_size, expected_md5):\n    if not path.is_file() or path.stat().st_size != expected_size:\n        return False\n    return not expected_md5 or md5_file(path).lower() == expected_md5.lower()\n\n\ndef fetch_one(dest, name, path, size, expected_md5):\n    final = dest / name\n    partial = dest / (name + \".part\")\n    if file_ok(final, size, expected_md5):\n        print(\"Verified existing \" + name, flush=True)\n        return\n    if final.exists():\n        final.unlink()\n    if partial.exists() and partial.stat().st_size > size:\n        partial.unlink()\n\n    print(\"Downloading \" + name, flush=True)\n    for attempt in range(8):\n        downloaded = partial.stat().st_size if partial.exists() else 0\n        if downloaded == size:\n            break\n        try:\n            link = api(\"/download\", public_key=SHARE, path=path)[\"href\"]\n            headers = {\"User-Agent\": USER_AGENT}\n            if downloaded:\n                headers[\"Range\"] = \"bytes=\" + str(downloaded) + \"-\"\n            with urlopen(Request(link, headers=headers), timeout=180) as response:\n                if downloaded:\n                    if response.status != 206 or not response.headers.get(\"Content-Range\", \"\").startswith(\"bytes \" + str(downloaded) + \"-\"):\n                        # The server did not honor our partial download. Restart safely.\n                        partial.unlink(missing_ok=True)\n                        raise IOError(\"Host does not support resuming this request\")\n                next_report = downloaded + 512 * 1024 * 1024\n                with partial.open(\"ab\" if downloaded else \"wb\") as output:\n                    while True:\n                        chunk = response.read(CHUNK)\n                        if not chunk:\n                            break\n                        output.write(chunk)\n                        downloaded += len(chunk)\n                        if downloaded >= next_report:\n                            print(\"  \" + name + \": \" + f\"{100 * downloaded / size:.1f}%\", flush=True)\n                            next_report = downloaded + 512 * 1024 * 1024\n            if partial.stat().st_size != size:\n                raise IOError(\"Transfer incomplete\")\n            break\n        except (OSError, ValueError, KeyError) as error:\n            if attempt == 7:\n                raise RuntimeError(\"Unable to download \" + name + \" from the official Yandex share\") from error\n            print(\"  Download interrupted: \" + str(error) + \"; retrying\", flush=True)\n            time.sleep(min(2 ** attempt, 30))\n\n    if not file_ok(partial, size, expected_md5):\n        raise RuntimeError(\"File integrity check failed: \" + name)\n    partial.replace(final)\n    print(\"Finished and verified \" + name, flush=True)\n\n\ndef main():\n    dest = Path(sys.argv[1]).resolve()\n    dest.mkdir(parents=True, exist_ok=True)\n    print(\"Checking official Anomaly Anthology 2.1 Final download folder\", flush=True)\n    files = list_official_files()\n    remaining = 0\n    for name in REQUIRED:\n        _, size, checksum = files[name]\n        final = dest / name\n        partial = dest / (name + \".part\")\n        if file_ok(final, size, checksum):\n            continue\n        remaining += size - min(partial.stat().st_size if partial.exists() else 0, size)\n\n    # Both downloaded installation media and unpacked game reside under $GAMEDIR.\n    # The full game and modpack require about 113 GB after extraction.\n    required_space = remaining + 113_000_000_000\n    available = shutil.disk_usage(dest).free\n    if available < required_space:\n        deficit = (required_space - available + 999_999_999) // 1_000_000_000\n        raise RuntimeError(f\"Not enough free disk space. Need about {deficit} GB more for the full installation\")\n\n    for name in REQUIRED:\n        path, size, checksum = files[name]\n        fetch_one(dest, name, path, size, checksum)\n    print(\"All three installation files are ready.\", flush=True)\n\n\nif __name__ == \"__main__\":\n    main()\n",
          "file": "$GAMEDIR/download-anthology.py"
        }
      },
      {
        "execute": {
          "command": "python3 \"$GAMEDIR/download-anthology.py\" \"$GAMEDIR/install-media\"",
          "disable_runtime": true,
          "working_dir": "$GAMEDIR"
        }
      },
      {
        "task": {
          "arch": "win64",
          "name": "create_prefix",
          "prefix": "$GAMEDIR/prefix"
        }
      },
      {
        "task": {
          "app": "vcrun2022",
          "arch": "win64",
          "description": "Installing Microsoft Visual C++ 2015-2022 runtime",
          "name": "winetricks",
          "prefix": "$GAMEDIR/prefix"
        }
      },
      {
        "task": {
          "arch": "win64",
          "description": "Install Anthology into C:\\ANTHOLOGY",
          "executable": "$GAMEDIR/install-media/ANTHOLOGY2_1_Final_setup.exe",
          "name": "wineexec",
          "prefix": "$GAMEDIR/prefix",
          "working_dir": "$GAMEDIR/install-media"
        }
      }
    ],
    "wine": {
      "dxvk": true
    }
  }
}
Back to game