Skip to content

Instantly share code, notes, and snippets.

@tsal
Forked from marceldev89/a3update.py
Created March 6, 2023 15:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tsal/108c05cb3a061a871148f7e343b72f57 to your computer and use it in GitHub Desktop.
Save tsal/108c05cb3a061a871148f7e343b72f57 to your computer and use it in GitHub Desktop.
Arma 3 Linux server and mod updater (workshop)
#!/usr/bin/python3
# MIT License
#
# Copyright (c) 2017 Marcel de Vries
#
# 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.
import os
import os.path
import re
import shutil
import time
from datetime import datetime
from urllib import request
#region Configuration
STEAM_CMD = "/home/steam/arma3/steam/steamcmd.sh"
STEAM_USER = ""
STEAM_PASS = ""
A3_SERVER_ID = "233780"
A3_SERVER_DIR = "/home/steam/arma3/install"
A3_WORKSHOP_ID = "107410"
A3_WORKSHOP_DIR = "{}/steamapps/workshop/content/{}".format(A3_SERVER_DIR, A3_WORKSHOP_ID)
A3_MODS_DIR = "/home/steam/arma3/mods"
MODS = {
"@cba_a3": "450814997",
"@ace3": "463939057",
"@alive": "620260972",
"@cup_terrains_core": "583496184",
"@cup_terrains_maps": "583544987",
"@cup_weapons": "497660133",
"@cup_units": "497661914",
"@cup_vehicles": "541888371"
}
PATTERN = re.compile(r"workshopAnnouncement.*?<p id=\"(\d+)\">", re.DOTALL)
WORKSHOP_CHANGELOG_URL = "https://steamcommunity.com/sharedfiles/filedetails/changelog"
#endregion
#region Functions
def log(msg):
print("")
print("{{0:=<{}}}".format(len(msg)).format(""))
print(msg);
print("{{0:=<{}}}".format(len(msg)).format(""))
def call_steamcmd(params):
os.system("{} {}".format(STEAM_CMD, params))
print("")
def update_server():
steam_cmd_params = " +login {} {}".format(STEAM_USER, STEAM_PASS)
steam_cmd_params += " +force_install_dir {}".format(A3_SERVER_DIR)
steam_cmd_params += " +app_update {} validate".format(A3_SERVER_ID)
steam_cmd_params += " +quit"
call_steamcmd(steam_cmd_params)
def mod_needs_update(mod_id, path):
if os.path.isdir(path):
response = request.urlopen("{}/{}".format(WORKSHOP_CHANGELOG_URL, mod_id)).read()
response = response.decode("utf-8")
match = PATTERN.search(response)
if match:
updated_at = datetime.fromtimestamp(int(match.group(1)))
created_at = datetime.fromtimestamp(os.path.getctime(path))
return (updated_at >= created_at)
return False
def update_mods():
for mod_name, mod_id in MODS.items():
path = "{}/{}".format(A3_WORKSHOP_DIR, mod_id)
# Check if mod needs to be updated
if os.path.isdir(path):
if mod_needs_update(mod_id, path):
# Delete existing folder so that we can verify whether the
# download succeeded
shutil.rmtree(path)
else:
print("No update required for \"{}\" ({})... SKIPPING".format(mod_name, mod_id))
continue
# Keep trying until the download actually succeeded
tries = 0
while os.path.isdir(path) == False and tries < 10:
log("Updating \"{}\" ({}) | {}".format(mod_name, mod_id, tries + 1))
steam_cmd_params = " +login {} {}".format(STEAM_USER, STEAM_PASS)
steam_cmd_params += " +force_install_dir {}".format(A3_SERVER_DIR)
steam_cmd_params += " +workshop_download_item {} {} validate".format(
A3_WORKSHOP_ID,
mod_id
)
steam_cmd_params += " +quit"
call_steamcmd(steam_cmd_params)
# Sleep for a bit so that we can kill the script if needed
time.sleep(5)
tries = tries + 1
if tries >= 10:
log("!! Updating {} failed after {} tries !!".format(mod_name, tries))
def lowercase_workshop_dir():
os.system("(cd {} && find . -depth -exec rename -v 's/(.*)\/([^\/]*)/$1\/\L$2/' {{}} \;)".format(A3_WORKSHOP_DIR))
def create_mod_symlinks():
for mod_name, mod_id in MODS.items():
link_path = "{}/{}".format(A3_MODS_DIR, mod_name)
real_path = "{}/{}".format(A3_WORKSHOP_DIR, mod_id)
if os.path.isdir(real_path):
if not os.path.islink(link_path):
os.symlink(real_path, link_path)
print("Creating symlink '{}'...".format(link_path))
else:
print("Mod '{}' does not exist! ({})".format(mod_name, real_path))
#endregion
log("Updating A3 server ({})".format(A3_SERVER_ID))
update_server()
log("Updating mods")
update_mods()
log("Converting uppercase files/folders to lowercase...")
lowercase_workshop_dir()
log("Creating symlinks...")
create_mod_symlinks()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment