From ec187e899429dc175c42bb97dc04f29c38e08f7e Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 01:14:16 -0600 Subject: [PATCH 01/28] lowercase workshop --- workshop.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/workshop.py b/workshop.py index 9894335..7498d8e 100644 --- a/workshop.py +++ b/workshop.py @@ -38,8 +38,39 @@ def preset(mod_file): for _, match in enumerate(matches, start=1): mods.append(match.group(1)) moddir = WORKSHOP + match.group(1) - moddirs.append(moddir) + moddirs.append("/arma3/workshop/" + moddir) download(mods) for moddir in moddirs: keys.copy(moddir) + lowercase_symlinks() return moddirs + +def lowercase_symlinks(): + src = "/arma3/steamapps/workshop/content/107410" + dst = "/arma3/workshop" + + if os.path.exists(dst): + for root, dirs, files in os.walk(dst, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(dst) + + for root, dirs, files in os.walk(src): + rel_root = os.path.relpath(root, src) + rel_root_lower = rel_root.lower() if rel_root != "." else "" + + dst_root = os.path.join(dst, rel_root_lower) + os.makedirs(dst_root, exist_ok=True) + + for f in files: + src_file = os.path.join(root, f) + dst_file = os.path.join(dst_root, f.lower()) + + if not os.path.exists(dst_file): + os.symlink(src_file, dst_file) + + for d in dirs: + dst_dir = os.path.join(dst_root, d.lower()) + os.makedirs(dst_dir, exist_ok=True) From 48d218512e8557403b395a4776850a6a25bb4bb3 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 01:16:35 -0600 Subject: [PATCH 02/28] lowercase workshop --- workshop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshop.py b/workshop.py index 7498d8e..d0ea848 100644 --- a/workshop.py +++ b/workshop.py @@ -38,7 +38,7 @@ def preset(mod_file): for _, match in enumerate(matches, start=1): mods.append(match.group(1)) moddir = WORKSHOP + match.group(1) - moddirs.append("/arma3/workshop/" + moddir) + moddirs.append("/arma3/workshop/" + match.group(1)) download(mods) for moddir in moddirs: keys.copy(moddir) From b8332aa73a13fcc27f57b5c18b289b9500715864 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 01:17:43 -0600 Subject: [PATCH 03/28] shutil --- workshop.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/workshop.py b/workshop.py index d0ea848..1becf4c 100644 --- a/workshop.py +++ b/workshop.py @@ -2,6 +2,7 @@ import re import subprocess import urllib.request +import shutil import keys @@ -50,12 +51,7 @@ def lowercase_symlinks(): dst = "/arma3/workshop" if os.path.exists(dst): - for root, dirs, files in os.walk(dst, topdown=False): - for name in files: - os.remove(os.path.join(root, name)) - for name in dirs: - os.rmdir(os.path.join(root, name)) - os.rmdir(dst) + shutil.rmtree(dst) for root, dirs, files in os.walk(src): rel_root = os.path.relpath(root, src) From fd16944b652e3d6563b8e73618774077fa1c281a Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 01:39:22 -0600 Subject: [PATCH 04/28] relative --- workshop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshop.py b/workshop.py index 1becf4c..15b927e 100644 --- a/workshop.py +++ b/workshop.py @@ -39,7 +39,7 @@ def preset(mod_file): for _, match in enumerate(matches, start=1): mods.append(match.group(1)) moddir = WORKSHOP + match.group(1) - moddirs.append("/arma3/workshop/" + match.group(1)) + moddirs.append("workshop/" + match.group(1)) download(mods) for moddir in moddirs: keys.copy(moddir) From 8d4a7f7cad5dba74d3e7133d811fa5cd2dd8435e Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 01:46:35 -0600 Subject: [PATCH 05/28] remove unused variable --- workshop.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/workshop.py b/workshop.py index 15b927e..441cbd6 100644 --- a/workshop.py +++ b/workshop.py @@ -6,7 +6,6 @@ import keys -WORKSHOP = "steamapps/workshop/content/107410/" USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36" # noqa: E501 @@ -38,7 +37,6 @@ def preset(mod_file): matches = re.finditer(regex, html, re.MULTILINE) for _, match in enumerate(matches, start=1): mods.append(match.group(1)) - moddir = WORKSHOP + match.group(1) moddirs.append("workshop/" + match.group(1)) download(mods) for moddir in moddirs: From eb9646f7cbc81e6902664606c531736faf2092f2 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 01:51:04 -0600 Subject: [PATCH 06/28] add validate, run it twice :| --- workshop.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/workshop.py b/workshop.py index 441cbd6..3f54a22 100644 --- a/workshop.py +++ b/workshop.py @@ -14,7 +14,10 @@ def download(mods): steamcmd.extend(["+force_install_dir", "/arma3"]) steamcmd.extend(["+login", os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]]) for id in mods: - steamcmd.extend(["+workshop_download_item", "107410", id]) + # A really bad way to avoid timeout issues :| + steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) + steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) + steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) steamcmd.extend(["+quit"]) subprocess.call(steamcmd) From 24522cc8eb085ecdec0bee3351656d56212bbbe5 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 11:13:16 -0600 Subject: [PATCH 07/28] retry --- workshop.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/workshop.py b/workshop.py index 3f54a22..9aa34b5 100644 --- a/workshop.py +++ b/workshop.py @@ -14,12 +14,18 @@ def download(mods): steamcmd.extend(["+force_install_dir", "/arma3"]) steamcmd.extend(["+login", os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]]) for id in mods: - # A really bad way to avoid timeout issues :| - steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) - steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) - steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) - steamcmd.extend(["+quit"]) - subprocess.call(steamcmd) + mod_steamcmd = steamcmd.copy() + mod_steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) + mod_steamcmd.extend(["+quit"]) + max_retries = 8 + for attempt in range(max_retries): + result = subprocess.call(mod_steamcmd) + if result == 0: + break # Success, move to next mod + elif attempt < max_retries - 1: + print(f"Download failed for mod {id} (attempt {attempt + 1}/{max_retries}), retrying...") + else: + print(f"Download failed for mod {id} after {max_retries} attempts") def preset(mod_file): From 5afef3a564eb82f6c0f42e9f58cedc7676b28f69 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 19:05:21 -0600 Subject: [PATCH 08/28] more attempts to recheck --- workshop.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/workshop.py b/workshop.py index 9aa34b5..ea5a66b 100644 --- a/workshop.py +++ b/workshop.py @@ -8,25 +8,35 @@ USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36" # noqa: E501 - def download(mods): steamcmd = ["/steamcmd/steamcmd.sh"] steamcmd.extend(["+force_install_dir", "/arma3"]) steamcmd.extend(["+login", os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]]) for id in mods: + print(f"Downloading mod {id}...") mod_steamcmd = steamcmd.copy() mod_steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) mod_steamcmd.extend(["+quit"]) max_retries = 8 for attempt in range(max_retries): - result = subprocess.call(mod_steamcmd) - if result == 0: + result = subprocess.run(mod_steamcmd, capture_output=True, text=True) + output = result.stdout + result.stderr + + timeout = "timeout downloading item" in output.lower() + + # Check for success (return code 0 and no timeout errors) + if result.returncode == 0 and not timeout: break # Success, move to next mod elif attempt < max_retries - 1: - print(f"Download failed for mod {id} (attempt {attempt + 1}/{max_retries}), retrying...") + if timeout: + print(f"Download timed out for mod {id} (attempt {attempt + 1}/{max_retries}), retrying...") + else: + print(f"Download failed for mod {id} (attempt {attempt + 1}/{max_retries}), retrying...") else: - print(f"Download failed for mod {id} after {max_retries} attempts") - + if timeout: + print(f"Download timed out for mod {id} after {max_retries} attempts") + else: + print(f"Download failed for mod {id} after {max_retries} attempts") def preset(mod_file): if mod_file.startswith("http"): @@ -77,3 +87,7 @@ def lowercase_symlinks(): for d in dirs: dst_dir = os.path.join(dst_root, d.lower()) os.makedirs(dst_dir, exist_ok=True) + +if __name__ == "__main__": + # Test download of ACE3 mod + download(["463939057"]) From 948b91fc755741963d9cb4c34619e295c4ee1711 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:25:06 -0600 Subject: [PATCH 09/28] no more steamcmd --- Dockerfile | 13 ++-- README.md | 16 ++--- api.py | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++++ launch.py | 53 ++++++--------- workshop.py | 65 +------------------ 5 files changed, 217 insertions(+), 114 deletions(-) create mode 100644 api.py diff --git a/Dockerfile b/Dockerfile index 5a33825..e1f8a94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,12 +23,10 @@ RUN apt-get update \ apt-get autoremove -y \ && \ rm -rf /var/lib/apt/lists/* \ - && \ - mkdir -p /steamcmd \ - && \ - wget -qO- 'https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz' | tar zxf - -C /steamcmd -ENV ARMA_BINARY=./arma3server +RUN pip3 install -U --break-system-packages --force "git+https://github.com/brettmayson/valvepythonsteam" + +ENV ARMA_BINARY=./arma3server_x64 ENV ARMA_CONFIG=main.cfg ENV ARMA_PARAMS= ENV ARMA_PROFILE=main @@ -38,10 +36,8 @@ ENV ARMA_CDLC= ENV HEADLESS_CLIENTS=0 ENV HEADLESS_CLIENTS_PROFILE="\$profile-hc-\$i" ENV PORT=2302 -ENV STEAM_BRANCH=public -ENV STEAM_BRANCH_PASSWORD= -ENV STEAM_ADDITIONAL_DEPOT= ENV MODS_LOCAL=true +ENV CLEAR_KEYS=true ENV MODS_PRESET= ENV SKIP_INSTALL=false @@ -53,7 +49,6 @@ EXPOSE 2306/udp WORKDIR /arma3 -VOLUME /steamcmd VOLUME /arma3/addons VOLUME /arma3/enoch VOLUME /arma3/expansion diff --git a/README.md b/README.md index c70144e..0ef6849 100644 --- a/README.md +++ b/README.md @@ -54,15 +54,13 @@ Profiles are saved in `/arma3/configs/profiles` | `-v /arma3/mods` | Mods that will be loaded by clients | | `-v /arma3/servermods` | Mods that will only be loaded by the server | | `-e PORT` | Port used by the server, (uses PORT to PORT+3) | 2302 | -| `-e ARMA_BINARY` | Arma 3 server binary to use, `./arma3server_x64` for x64 | `./arma3server` | +| `-e ARMA_BINARY` | Arma 3 server binary to use | `./arma3server` | | `-e ARMA_CONFIG` | Config file to load from `/arma3/configs` | `main.cfg` | | `-e ARMA_PARAMS` | Additional Arma CLI parameters | | `-e ARMA_PROFILE` | Profile name, stored in `/arma3/configs/profiles` | `main` | | `-e ARMA_WORLD` | World to load on startup | `empty` | | `-e ARMA_LIMITFPS` | Maximum FPS | `1000` | -| `-e ARMA_CDLC` | cDLCs to load | -| `-e STEAM_BRANCH` | Steam branch used by steamcmd | `public` | -| `-e STEAM_BRANCH_PASSWORD` | Steam branch password used by steamcmd | +| `-e ARMA_CDLC` | cDLCs to load, separated by semicolons | - | | `-e STEAM_USER` | Steam username used to login to steamcmd | | `-e STEAM_PASSWORD` | Steam password | | `-e HEADLESS_CLIENTS` | Launch n number of headless clients | `0` | @@ -70,7 +68,7 @@ Profiles are saved in `/arma3/configs/profiles` | `-e MODS_LOCAL` | Should the mods folder be loaded | `true` | | `-e MODS_PRESET` | An Arma 3 Launcher preset to load | | `-e SKIP_INSTALL` | Skip Arma 3 installation | `false` | -| `-e CLEAR_KEYS` | Clear the keys directory every launch (keys will still be copied from mods) | `false` | +| `-e CLEAR_KEYS` | Clear the keys directory every launch (keys will still be copied from mods) | `true` | The Steam account does not need to own Arma 3, but must have Steam Guard disabled. @@ -82,10 +80,10 @@ To use a Creator DLC the `STEAM_BRANCH` must be set to `creatordlc` | Name | Flag | | ---- | ---- | -| [CSLA Iron Curtain](https://store.steampowered.com/app/1294440/Arma_3_Creator_DLC_CSLA_Iron_Curtain/) | CSLA | -| [Global Mobilization - Cold War Germany](https://store.steampowered.com/app/1042220/Arma_3_Creator_DLC_Global_Mobilization__Cold_War_Germany/) | GM | +| [CSLA Iron Curtain](https://store.steampowered.com/app/1294440/Arma_3_Creator_DLC_CSLA_Iron_Curtain/) | csla | +| [Global Mobilization - Cold War Germany](https://store.steampowered.com/app/1042220/Arma_3_Creator_DLC_Global_Mobilization__Cold_War_Germany/) | gm | | [S.O.G. Prairie Fire](https://store.steampowered.com/app/1227700/Arma_3_Creator_DLC_SOG_Prairie_Fire) | vn | -| [Western Sahara](https://store.steampowered.com/app/1681170/Arma_3_Creator_DLC_Western_Sahara/) | WS | +| [Western Sahara](https://store.steampowered.com/app/1681170/Arma_3_Creator_DLC_Western_Sahara/) | ws | | [Spearhead 1944](https://store.steampowered.com/app/1175380/Arma_3_Creator_DLC_Spearhead_1944/) | spe | | [Reaction Forces](https://store.steampowered.com/app/2647760/Arma_3_Creator_DLC_Reaction_Forces/) | rf | | [Expeditionary Forces](https://store.steampowered.com/app/2647830/Arma_3_Creator_DLC_Expeditionary_Forces/) | ef | @@ -110,7 +108,7 @@ Bohemia-updated list of codes here: CACHE_EXPIRY_SECONDS: + print("Manifest cache expired, will refetch...") + return None + + return cache_data.get('manifests', []) + except (json.JSONDecodeError, FileNotFoundError): + print("Cache file corrupted or missing, will refetch...") + return None + +def save_manifests_to_cache(manifests): + """Save manifest data to cache with timestamp""" + os.makedirs(CACHE_DIR, exist_ok=True) + + manifest_data = [] + for manifest in manifests: + manifest_data.append({ + 'name': manifest.name, + 'gid': manifest.gid, + 'depot_id': manifest.depot_id + }) + + cache_data = { + 'timestamp': time.time(), + 'manifests': manifest_data + } + + with open(MANIFEST_CACHE_FILE, 'w') as f: + json.dump(cache_data, f, indent=2) + + print(f"Manifest data cached to {MANIFEST_CACHE_FILE}") + +def download_depot(client, depot_id): + cdn_client = CDNClient(client) + + cached_manifests = load_cached_manifests() + + if cached_manifests: + manifests = cached_manifests + print("Got manifests from cache for ARMA3 server app ID:", ARMA3_SERVER_APP_ID) + else: + print("Fetching fresh manifests from Steam...") + manifests_obj = cdn_client.get_manifests(ARMA3_SERVER_APP_ID, branch="creatordlc") + + save_manifests_to_cache(manifests_obj) + + manifests = [] + for manifest in manifests_obj: + manifests.append({ + 'name': manifest.name, + 'gid': manifest.gid, + 'depot_id': manifest.depot_id + }) + print("Got manifests for ARMA3 server app ID:", ARMA3_SERVER_APP_ID) + + for manifest in manifests: + print(f"[{manifest['name']}] ID: {manifest['gid']}, Depot ID: {manifest['depot_id']}") + + target_manifest = next((m for m in manifests if m['depot_id'] == depot_id), None) + if not target_manifest: + print(f"No manifest found for depot ID {depot_id}") + return + + print(f"Manifest ID: {target_manifest['gid']}, Depot ID: {target_manifest['depot_id']}") + + files_generator = cdn_client.iter_files(ARMA3_SERVER_APP_ID, branch="creatordlc", filter_func=lambda d_id, depot_info: d_id == target_manifest['depot_id']) + files = list(files_generator) + files = [f for f in files if f.is_file] + print(f"Found {len(files)} files to download") + download_files(client, cdn_client, files, destination="./") + +def download_workshop(client, workshop_id): + cdn_client = CDNClient(client) + workshop_manifest = cdn_client.get_manifest_for_workshop_item(workshop_id) + files_generator = workshop_manifest.iter_files() + files = list(files_generator) + files = [f for f in files if f.is_file] + print(f"Found {len(files)} files to download") + download_files(client, cdn_client, files, destination=f"workshop/{workshop_id}/") + +def download_files(client, cdn_client, files, destination): + print(f"Verifying {len(files)} files...") + files_to_download = [] + + for i, file in enumerate(files): + file.local = os.path.join(destination, file.filename) + + if os.path.exists(file.local): + with open(file.local, 'rb') as f: + existing_hash = hashlib.sha1(f.read()).hexdigest() + expected_hash = file.sha_content.hex() if isinstance(file.sha_content, bytes) else file.sha_content + if existing_hash == expected_hash: + continue + files_to_download.append(file) + + print(f"Need to download {len(files_to_download)} files...") + + for i, file in enumerate(files_to_download): + print(f"Downloading {i+1}/{len(files_to_download)}: {file.filename}") + _download_single_file(file) + + print("All files downloaded successfully.") + +def _download_single_file(file): + if file.local and os.path.dirname(file.local) != "": + os.makedirs(os.path.dirname(file.local), exist_ok=True) + + chunk_size = 1024 * 1024 # 1MB chunks + downloaded = 0 + failed = False + + with open(file.local, 'wb') as f: + while downloaded < file.size: + remaining = file.size - downloaded + read_size = min(chunk_size, remaining) + + chunk = file.read(read_size) + if not chunk: + break + + f.write(chunk) + downloaded += len(chunk) + + if downloaded % (10 * 1024 * 1024) == 0: + percent = (downloaded / file.size) * 100 + print(f" Progress: {percent:.1f}% ({downloaded}/{file.size} bytes)") + + if failed: + print(f"Failed to download {file.filename}") + else: + print(f"✓ Downloaded {file.filename} ({file.size} bytes)") + +if __name__ == "__main__": + import os + import sys + + if len(sys.argv) != 3: + print("Usage: python api.py ") + sys.exit(1) + + username = sys.argv[1] + password = sys.argv[2] + + client = login(username, password) + if client: + download_depot(client, 233785) # Western Sahara + download_workshop(client, 463939057) # Example workshop ID for ACE3 mod diff --git a/launch.py b/launch.py index 57f2b7b..5b708bb 100644 --- a/launch.py +++ b/launch.py @@ -4,6 +4,7 @@ import subprocess from string import Template +import api import local import workshop @@ -15,7 +16,6 @@ def mod_param(name, mods): def env_defined(key): return key in os.environ and len(os.environ[key]) > 0 - CONFIG_FILE = os.environ["ARMA_CONFIG"] KEYS = "/arma3/keys" @@ -26,46 +26,31 @@ def env_defined(key): os.remove(KEYS) os.makedirs(KEYS) +client = None if os.environ["SKIP_INSTALL"] in ["", "false"]: - # Install Arma - - steamcmd = ["/steamcmd/steamcmd.sh"] - steamcmd.extend(["+force_install_dir", "/arma3"]) - steamcmd.extend(["+login", os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]]) - steamcmd.extend(["+app_update", "233780"]) - if env_defined("STEAM_BRANCH"): - steamcmd.extend(["-beta", os.environ["STEAM_BRANCH"]]) - if env_defined("STEAM_BRANCH_PASSWORD"): - steamcmd.extend(["-betapassword", os.environ["STEAM_BRANCH_PASSWORD"]]) - steamcmd.extend(["validate"]) - if env_defined("STEAM_ADDITIONAL_DEPOT"): - for depot in os.environ["STEAM_ADDITIONAL_DEPOT"].split("|"): - depot_parts = depot.split(",") - steamcmd.extend( - ["+login", os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]] - ) - steamcmd.extend( - ["+download_depot", "233780", depot_parts[0], depot_parts[1]] - ) - steamcmd.extend(["+quit"]) - subprocess.call(steamcmd) - -if env_defined("STEAM_ADDITIONAL_DEPOT"): - for depot in os.environ["STEAM_ADDITIONAL_DEPOT"].split("|"): - depot_parts = depot.split(",") - depot_dir = ( - f"/steamcmd/linux32/steamapps/content/app_233780/depot_{depot_parts[0]}/" - ) - for file in os.listdir(depot_dir): - shutil.copytree(depot_dir + file, "/arma3/", dirs_exist_ok=True) - print(f"Moved {file} to /arma3") + client = login(username, password) + if not client: + print("Failed to login to Steam, exiting...") + exit(1) + download_depot(client, 233781) # Default Content + download_depot(client, 233783) # Linux Server + if os.environ["ARMA_BINARY"] == "arma3serverprofiling_x64": + download_depot(client, 233785) # Arma 3 Profiling + + for cdlc in os.environ["ARMA_CDLC"].split(";"): + if cdlc: + cdlc = cdlc.lower() + print("Downloading CDLC:", cdlc) + download_depot(client, api.CDLC_IDS[cdlc]) # Mods mods = [] if os.environ["MODS_PRESET"] != "": - mods.extend(workshop.preset(os.environ["MODS_PRESET"])) + if not client: + client = login(os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]) + mods.extend(workshop.preset(os.environ["MODS_PRESET"], client)) if os.environ["MODS_LOCAL"] == "true" and os.path.exists("mods"): mods.extend(local.mods("mods")) diff --git a/workshop.py b/workshop.py index ea5a66b..bfb46f3 100644 --- a/workshop.py +++ b/workshop.py @@ -5,40 +5,11 @@ import shutil import keys +import api USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36" # noqa: E501 -def download(mods): - steamcmd = ["/steamcmd/steamcmd.sh"] - steamcmd.extend(["+force_install_dir", "/arma3"]) - steamcmd.extend(["+login", os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]]) - for id in mods: - print(f"Downloading mod {id}...") - mod_steamcmd = steamcmd.copy() - mod_steamcmd.extend(["+workshop_download_item", "107410", id, "validate"]) - mod_steamcmd.extend(["+quit"]) - max_retries = 8 - for attempt in range(max_retries): - result = subprocess.run(mod_steamcmd, capture_output=True, text=True) - output = result.stdout + result.stderr - - timeout = "timeout downloading item" in output.lower() - - # Check for success (return code 0 and no timeout errors) - if result.returncode == 0 and not timeout: - break # Success, move to next mod - elif attempt < max_retries - 1: - if timeout: - print(f"Download timed out for mod {id} (attempt {attempt + 1}/{max_retries}), retrying...") - else: - print(f"Download failed for mod {id} (attempt {attempt + 1}/{max_retries}), retrying...") - else: - if timeout: - print(f"Download timed out for mod {id} after {max_retries} attempts") - else: - print(f"Download failed for mod {id} after {max_retries} attempts") - -def preset(mod_file): +def preset(mod_file, client): if mod_file.startswith("http"): req = urllib.request.Request( mod_file, @@ -56,38 +27,8 @@ def preset(mod_file): matches = re.finditer(regex, html, re.MULTILINE) for _, match in enumerate(matches, start=1): mods.append(match.group(1)) + api.download_workshop(client, match.group(1)) moddirs.append("workshop/" + match.group(1)) - download(mods) for moddir in moddirs: keys.copy(moddir) - lowercase_symlinks() return moddirs - -def lowercase_symlinks(): - src = "/arma3/steamapps/workshop/content/107410" - dst = "/arma3/workshop" - - if os.path.exists(dst): - shutil.rmtree(dst) - - for root, dirs, files in os.walk(src): - rel_root = os.path.relpath(root, src) - rel_root_lower = rel_root.lower() if rel_root != "." else "" - - dst_root = os.path.join(dst, rel_root_lower) - os.makedirs(dst_root, exist_ok=True) - - for f in files: - src_file = os.path.join(root, f) - dst_file = os.path.join(dst_root, f.lower()) - - if not os.path.exists(dst_file): - os.symlink(src_file, dst_file) - - for d in dirs: - dst_dir = os.path.join(dst_root, d.lower()) - os.makedirs(dst_dir, exist_ok=True) - -if __name__ == "__main__": - # Test download of ACE3 mod - download(["463939057"]) From 9c7348641d25213dc9387e58bafc485531ca9da0 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:26:24 -0600 Subject: [PATCH 10/28] install pip --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index e1f8a94..fda2b16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN apt-get update \ && \ apt-get install -y --no-install-recommends --no-install-suggests \ python3 \ + python3-pip \ lib32stdc++6 \ lib32gcc-s1 \ libcurl4 \ From dca6a25d23632914024e821e284de5b99cafd7a7 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:26:58 -0600 Subject: [PATCH 11/28] fix first run --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fda2b16..49a8e0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ RUN apt-get update \ && \ apt-get autoremove -y \ && \ - rm -rf /var/lib/apt/lists/* \ + rm -rf /var/lib/apt/lists/* RUN pip3 install -U --break-system-packages --force "git+https://github.com/brettmayson/valvepythonsteam" From 417d57edfa7bf2711d76a2e73913098ec8908fec Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:27:45 -0600 Subject: [PATCH 12/28] change pip thing --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 49a8e0f..4b88ac8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN apt-get update \ && \ rm -rf /var/lib/apt/lists/* -RUN pip3 install -U --break-system-packages --force "git+https://github.com/brettmayson/valvepythonsteam" +RUN pip3 install -U "git+https://github.com/brettmayson/valvepythonsteam" ENV ARMA_BINARY=./arma3server_x64 ENV ARMA_CONFIG=main.cfg From a0e5bbfb8f0d40aed3c19098b846ed4ee1e8b918 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:29:09 -0600 Subject: [PATCH 13/28] install git --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 4b88ac8..3fdea0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ RUN apt-get update \ ca-certificates \ curl \ libstdc++6 \ + git \ && \ apt-get remove --purge -y \ && \ From ff9f951c3ed0d6df1b5f1b3f43e6cf07ca9ecf0d Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:33:50 -0600 Subject: [PATCH 14/28] install clinet libs --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3fdea0c..f0717b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ RUN apt-get update \ && \ rm -rf /var/lib/apt/lists/* -RUN pip3 install -U "git+https://github.com/brettmayson/valvepythonsteam" +RUN pip3 install -U "git+https://github.com/brettmayson/valvepythonsteam[client]" ENV ARMA_BINARY=./arma3server_x64 ENV ARMA_CONFIG=main.cfg From 60a44263be55728e3181565fd29541b44731e7a9 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:35:35 -0600 Subject: [PATCH 15/28] install clinet libs --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f0717b3..fa656b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ RUN apt-get update \ && \ rm -rf /var/lib/apt/lists/* -RUN pip3 install -U "git+https://github.com/brettmayson/valvepythonsteam[client]" +RUN pip3 install -U "git+https://github.com/brettmayson/valvepythonsteam#egg=steam[client]" ENV ARMA_BINARY=./arma3server_x64 ENV ARMA_CONFIG=main.cfg From 2fde2b2262fb46405dd073ac26fa24dc214759e3 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:37:15 -0600 Subject: [PATCH 16/28] fix login call --- launch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launch.py b/launch.py index 5b708bb..84b4c1b 100644 --- a/launch.py +++ b/launch.py @@ -28,7 +28,7 @@ def env_defined(key): client = None if os.environ["SKIP_INSTALL"] in ["", "false"]: - client = login(username, password) + client = api.login(username, password) if not client: print("Failed to login to Steam, exiting...") exit(1) @@ -49,7 +49,7 @@ def env_defined(key): if os.environ["MODS_PRESET"] != "": if not client: - client = login(os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]) + client = api.login(os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]) mods.extend(workshop.preset(os.environ["MODS_PRESET"], client)) if os.environ["MODS_LOCAL"] == "true" and os.path.exists("mods"): From 22f0361d647c3276c06514c215dcd421e6762fee Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:38:46 -0600 Subject: [PATCH 17/28] fix login call --- launch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch.py b/launch.py index 84b4c1b..6d928a5 100644 --- a/launch.py +++ b/launch.py @@ -28,7 +28,7 @@ def env_defined(key): client = None if os.environ["SKIP_INSTALL"] in ["", "false"]: - client = api.login(username, password) + client = api.login(os.environ["STEAM_USER"], os.environ["STEAM_PASSWORD"]) if not client: print("Failed to login to Steam, exiting...") exit(1) From c35b0990417f1735d0b576b9674c9365cc8bb246 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:40:18 -0600 Subject: [PATCH 18/28] fix login call --- launch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launch.py b/launch.py index 6d928a5..ba538ed 100644 --- a/launch.py +++ b/launch.py @@ -32,16 +32,16 @@ def env_defined(key): if not client: print("Failed to login to Steam, exiting...") exit(1) - download_depot(client, 233781) # Default Content - download_depot(client, 233783) # Linux Server + api.download_depot(client, 233781) # Default Content + api.download_depot(client, 233783) # Linux Server if os.environ["ARMA_BINARY"] == "arma3serverprofiling_x64": - download_depot(client, 233785) # Arma 3 Profiling + api.download_depot(client, 233785) # Arma 3 Profiling for cdlc in os.environ["ARMA_CDLC"].split(";"): if cdlc: cdlc = cdlc.lower() print("Downloading CDLC:", cdlc) - download_depot(client, api.CDLC_IDS[cdlc]) + api.download_depot(client, api.CDLC_IDS[cdlc]) # Mods From aa7e631cc2135818e4678283e32ad58a26b55e44 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:57:56 -0600 Subject: [PATCH 19/28] workshop uses int --- Dockerfile | 4 ++++ launch.py | 2 +- workshop.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index fa656b2..d74d5ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,6 +58,10 @@ VOLUME /arma3/jets VOLUME /arma3/heli VOLUME /arma3/orange VOLUME /arma3/argo +VOLUME /arma3/mark +VOLUME /arma3/kart +VOLUME /arma3/aow +VOLUME /arma3/curator STOPSIGNAL SIGINT diff --git a/launch.py b/launch.py index ba538ed..52c249d 100644 --- a/launch.py +++ b/launch.py @@ -8,11 +8,11 @@ import local import workshop +print("Starting Arma 3 Server...") def mod_param(name, mods): return ' -{}="{}" '.format(name, ";".join(mods)) - def env_defined(key): return key in os.environ and len(os.environ[key]) > 0 diff --git a/workshop.py b/workshop.py index bfb46f3..6c692af 100644 --- a/workshop.py +++ b/workshop.py @@ -27,7 +27,7 @@ def preset(mod_file, client): matches = re.finditer(regex, html, re.MULTILINE) for _, match in enumerate(matches, start=1): mods.append(match.group(1)) - api.download_workshop(client, match.group(1)) + api.download_workshop(client, int(match.group(1))) moddirs.append("workshop/" + match.group(1)) for moddir in moddirs: keys.copy(moddir) From 70585444a9ea4a0e5ad319d74acb4ca9410c3b38 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 21:58:49 -0600 Subject: [PATCH 20/28] change prints --- api.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/api.py b/api.py index 4c9cc52..3fce38f 100644 --- a/api.py +++ b/api.py @@ -88,17 +88,13 @@ def download_depot(client, depot_id): 'gid': manifest.gid, 'depot_id': manifest.depot_id }) - print("Got manifests for ARMA3 server app ID:", ARMA3_SERVER_APP_ID) - - for manifest in manifests: - print(f"[{manifest['name']}] ID: {manifest['gid']}, Depot ID: {manifest['depot_id']}") target_manifest = next((m for m in manifests if m['depot_id'] == depot_id), None) if not target_manifest: print(f"No manifest found for depot ID {depot_id}") return - print(f"Manifest ID: {target_manifest['gid']}, Depot ID: {target_manifest['depot_id']}") + print(f"Downloading Manifest ID: {target_manifest['gid']}, Depot ID: {target_manifest['depot_id']}") files_generator = cdn_client.iter_files(ARMA3_SERVER_APP_ID, branch="creatordlc", filter_func=lambda d_id, depot_info: d_id == target_manifest['depot_id']) files = list(files_generator) From 60a94170abebf48734c05d0c0f82fd50a0f24bfc Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 22:08:47 -0600 Subject: [PATCH 21/28] PYTHONUNBUFFERED --- Dockerfile | 3 +++ api.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d74d5ed..7eed659 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,8 @@ RUN apt-get update \ RUN pip3 install -U "git+https://github.com/brettmayson/valvepythonsteam#egg=steam[client]" +ENV PYTHONUNBUFFERED=1 + ENV ARMA_BINARY=./arma3server_x64 ENV ARMA_CONFIG=main.cfg ENV ARMA_PARAMS= @@ -62,6 +64,7 @@ VOLUME /arma3/mark VOLUME /arma3/kart VOLUME /arma3/aow VOLUME /arma3/curator +VOLUME /arma3/tacops STOPSIGNAL SIGINT diff --git a/api.py b/api.py index 3fce38f..1f29c2f 100644 --- a/api.py +++ b/api.py @@ -129,12 +129,12 @@ def download_files(client, cdn_client, files, destination): print(f"Need to download {len(files_to_download)} files...") for i, file in enumerate(files_to_download): - print(f"Downloading {i+1}/{len(files_to_download)}: {file.filename}") _download_single_file(file) print("All files downloaded successfully.") def _download_single_file(file): + print(f"Downloading {i+1}/{len(files_to_download)}: {file.filename} ({file.size} bytes)") if file.local and os.path.dirname(file.local) != "": os.makedirs(os.path.dirname(file.local), exist_ok=True) @@ -161,7 +161,7 @@ def _download_single_file(file): if failed: print(f"Failed to download {file.filename}") else: - print(f"✓ Downloaded {file.filename} ({file.size} bytes)") + print(f"✓ Downloaded {file.filename}") if __name__ == "__main__": import os From 754beaa58a0dd964b4408ec0ad523158795578a5 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 22:10:51 -0600 Subject: [PATCH 22/28] fix download --- api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.py b/api.py index 1f29c2f..68820e5 100644 --- a/api.py +++ b/api.py @@ -129,12 +129,12 @@ def download_files(client, cdn_client, files, destination): print(f"Need to download {len(files_to_download)} files...") for i, file in enumerate(files_to_download): + print(f"Downloading {i+1}/{len(files_to_download)}: {file.filename} ({file.size} bytes)") _download_single_file(file) print("All files downloaded successfully.") def _download_single_file(file): - print(f"Downloading {i+1}/{len(files_to_download)}: {file.filename} ({file.size} bytes)") if file.local and os.path.dirname(file.local) != "": os.makedirs(os.path.dirname(file.local), exist_ok=True) From 1f7e1207ad5b061a143d991a2130544b9516e8b4 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 22:15:46 -0600 Subject: [PATCH 23/28] more volumes, lowercase files --- Dockerfile | 2 ++ api.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7eed659..3d3b520 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,6 +65,8 @@ VOLUME /arma3/kart VOLUME /arma3/aow VOLUME /arma3/curator VOLUME /arma3/tacops +VOLUME /arma3/tank +VOLUME /arma3/dta STOPSIGNAL SIGINT diff --git a/api.py b/api.py index 68820e5..67e52c0 100644 --- a/api.py +++ b/api.py @@ -100,7 +100,7 @@ def download_depot(client, depot_id): files = list(files_generator) files = [f for f in files if f.is_file] print(f"Found {len(files)} files to download") - download_files(client, cdn_client, files, destination="./") + download_files(client, cdn_client, files, destination="") def download_workshop(client, workshop_id): cdn_client = CDNClient(client) @@ -116,7 +116,7 @@ def download_files(client, cdn_client, files, destination): files_to_download = [] for i, file in enumerate(files): - file.local = os.path.join(destination, file.filename) + file.local = os.path.join(destination, file.filename).lower() if os.path.exists(file.local): with open(file.local, 'rb') as f: From db0ebb2c2d070b7f9f8503b9a8252dc7dcfc47ea Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 22:26:41 -0600 Subject: [PATCH 24/28] change paths to remove all the volumes --- Dockerfile | 15 +-------------- README.md | 25 +++++++++++++------------ api.py | 4 ++-- docker-compose.yml | 16 ++++------------ keys.py | 2 +- launch.py | 9 +++++---- 6 files changed, 26 insertions(+), 45 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3d3b520..b3472cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,20 +53,7 @@ EXPOSE 2306/udp WORKDIR /arma3 -VOLUME /arma3/addons -VOLUME /arma3/enoch -VOLUME /arma3/expansion -VOLUME /arma3/jets -VOLUME /arma3/heli -VOLUME /arma3/orange -VOLUME /arma3/argo -VOLUME /arma3/mark -VOLUME /arma3/kart -VOLUME /arma3/aow -VOLUME /arma3/curator -VOLUME /arma3/tacops -VOLUME /arma3/tank -VOLUME /arma3/dta +VOLUME /arma3/server STOPSIGNAL SIGINT diff --git a/README.md b/README.md index 0ef6849..dfcf779 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ An Arma 3 Dedicated Server. Updates to the latest version every time it is resta -p 2304:2304/udp \ -p 2305:2305/udp \ -p 2306:2306/udp \ - -v path/to/missions:/arma3/mpmissions \ - -v path/to/configs:/arma3/configs \ - -v path/to/mods:/arma3/mods \ - -v path/to/servermods:/arma3/servermods \ + -v path/to/missions:/arma3/server/mpmissions \ + -v path/to/configs:/arma3/server/configs \ + -v path/to/mods:/arma3/server/mods \ + -v path/to/servermods:/arma3/server/servermods \ -e ARMA_CONFIG=main.cfg \ -e STEAM_USER=myusername \ -e STEAM_PASSWORD=mypassword \ @@ -42,22 +42,23 @@ Use `docker-compose up -d` to start the server, detached. See [Docker-compose](https://docs.docker.com/compose/install/#install-compose) for an installation guide. -Profiles are saved in `/arma3/configs/profiles` +Profiles are saved in `/arma3/server/configs/profiles` ## Parameters | Parameter | Function | Default | | ------------- |-------------- | - | | `-p 2302-2306` | Ports required by Arma 3 | -| `-v /arma3/mpmission` | Folder with MP Missions | -| `-v /arma3/configs` | Folder containing config files | -| `-v /arma3/mods` | Mods that will be loaded by clients | -| `-v /arma3/servermods` | Mods that will only be loaded by the server | +| `-v /arma3/server/mpmission` | Folder with MP Missions | +| `-v /arma3/server/configs` | Folder containing config files | +| `-v /arma3/server/mods` | Mods that will be loaded by clients | +| `-v /arma3/server/servermods` | Mods that will only be loaded by the server | +| `-v /arma3/server` | Folder containing the server files | | `-e PORT` | Port used by the server, (uses PORT to PORT+3) | 2302 | | `-e ARMA_BINARY` | Arma 3 server binary to use | `./arma3server` | -| `-e ARMA_CONFIG` | Config file to load from `/arma3/configs` | `main.cfg` | +| `-e ARMA_CONFIG` | Config file to load from `/arma3/server/configs` | `main.cfg` | | `-e ARMA_PARAMS` | Additional Arma CLI parameters | -| `-e ARMA_PROFILE` | Profile name, stored in `/arma3/configs/profiles` | `main` | +| `-e ARMA_PROFILE` | Profile name, stored in `/arma3/server/configs/profiles` | `main` | | `-e ARMA_WORLD` | World to load on startup | `empty` | | `-e ARMA_LIMITFPS` | Maximum FPS | `1000` | | `-e ARMA_CDLC` | cDLCs to load, separated by semicolons | - | @@ -108,7 +109,7 @@ Bohemia-updated list of codes here: 0: for key in keys: if not os.path.isdir(key): - shutil.copy2(key, "/arma3/keys") + shutil.copy2(key, "/arma3/server/keys") else: print("Missing keys:", moddir) diff --git a/launch.py b/launch.py index 52c249d..c3db9ae 100644 --- a/launch.py +++ b/launch.py @@ -17,7 +17,7 @@ def env_defined(key): return key in os.environ and len(os.environ[key]) > 0 CONFIG_FILE = os.environ["ARMA_CONFIG"] -KEYS = "/arma3/keys" +KEYS = "/arma3/server/keys" if env_defined("CLEAR_KEYS") and os.environ["CLEAR_KEYS"] == "true" and os.path.isdir(KEYS): shutil.rmtree(KEYS) @@ -71,7 +71,7 @@ def env_defined(key): print("Headless Clients:", clients) if clients != 0: - with open("/arma3/configs/{}".format(CONFIG_FILE)) as config: + with open("/arma3/server/configs/{}".format(CONFIG_FILE)) as config: data = config.read() regex = r"(.+?)(?:\s+)?=(?:\s+)?(.+?)(?:$|\/|;)" @@ -108,9 +108,9 @@ def env_defined(key): subprocess.Popen(hc_launch, shell=True) else: - launch += ' -config="/arma3/configs/{}"'.format(CONFIG_FILE) + launch += ' -config="/arma3/server/configs/{}"'.format(CONFIG_FILE) -launch += ' -port={} -name="{}" -profiles="/arma3/configs/profiles"'.format( +launch += ' -port={} -name="{}" -profiles="/arma3/server/configs/profiles"'.format( os.environ["PORT"], os.environ["ARMA_PROFILE"] ) @@ -118,4 +118,5 @@ def env_defined(key): launch += mod_param("serverMod", local.mods("servermods")) print("LAUNCHING ARMA SERVER WITH", launch, flush=True) +os.chdir("/arma3/server") os.system(launch) From 87b3673978174da867453083ff6ee182abadf51a Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sun, 17 Aug 2025 23:54:05 -0600 Subject: [PATCH 25/28] install zstandard --- Dockerfile | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b3472cd..e5c3c7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ RUN apt-get update \ && \ rm -rf /var/lib/apt/lists/* -RUN pip3 install -U "git+https://github.com/brettmayson/valvepythonsteam#egg=steam[client]" +RUN pip3 install -U zstandard "git+https://github.com/brettmayson/valvepythonsteam#egg=steam[client]" ENV PYTHONUNBUFFERED=1 diff --git a/README.md b/README.md index dfcf779..3409c85 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Bohemia-updated list of codes here: Date: Mon, 18 Aug 2025 13:13:51 -0600 Subject: [PATCH 26/28] exeuctable --- api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.py b/api.py index fcba84f..b9b0d2d 100644 --- a/api.py +++ b/api.py @@ -123,6 +123,8 @@ def download_files(client, cdn_client, files, destination): existing_hash = hashlib.sha1(f.read()).hexdigest() expected_hash = file.sha_content.hex() if isinstance(file.sha_content, bytes) else file.sha_content if existing_hash == expected_hash: + if file.is_executable: + os.chmod(file.local, 0o755) continue files_to_download.append(file) @@ -161,6 +163,8 @@ def _download_single_file(file): if failed: print(f"Failed to download {file.filename}") else: + if file.is_executable: + os.chmod(file.local, 0o755) print(f"✓ Downloaded {file.filename}") if __name__ == "__main__": From 8aaa4deca874d05ac95a6c997d32f28cde07fa16 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Mon, 18 Aug 2025 13:21:07 -0600 Subject: [PATCH 27/28] fix keys --- workshop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshop.py b/workshop.py index 6c692af..1c0e4f5 100644 --- a/workshop.py +++ b/workshop.py @@ -28,7 +28,7 @@ def preset(mod_file, client): for _, match in enumerate(matches, start=1): mods.append(match.group(1)) api.download_workshop(client, int(match.group(1))) - moddirs.append("workshop/" + match.group(1)) + moddirs.append("server/workshop/" + match.group(1)) for moddir in moddirs: keys.copy(moddir) return moddirs From 1c43b1be487a1eda2e87761b9016a43921562c2c Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Mon, 18 Aug 2025 13:51:43 -0600 Subject: [PATCH 28/28] really fix --- workshop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workshop.py b/workshop.py index 1c0e4f5..be63092 100644 --- a/workshop.py +++ b/workshop.py @@ -28,7 +28,7 @@ def preset(mod_file, client): for _, match in enumerate(matches, start=1): mods.append(match.group(1)) api.download_workshop(client, int(match.group(1))) - moddirs.append("server/workshop/" + match.group(1)) + moddirs.append("workshop/" + match.group(1)) for moddir in moddirs: - keys.copy(moddir) + keys.copy("server/"+moddir) return moddirs