Updating Template from Steam
This script will update the application in the template folder from Steam. Since the template is never running so it can be updated at any time throughout the day.
You can download the script straight from my github using the below commands or manually create the file yourself by copying the code from this forum (which may get outdated compared to github)
Method #1 - Obtain from GitHub:
Code: Select all
wget https://raw.githubusercontent.com/LHammonds/ark-bash/main/game-update-server.sh --output-document /opt/ark/scripts/game-update-server.sh
sudo chown arkserver:arkserver /opt/ark/scripts/game-update-server.sh
sudo chmod 0750 /opt/ark/scripts/game-update-server.sh
End of Method #1
Method #2 - Copy/Paste from this forum (but check GitHub for newer version):
Code: Select all
sudo touch /opt/ark/scripts/game-update-server.sh
sudo chown arkserver:arkserver /opt/ark/scripts/game-update-server.sh
sudo chmod 0750 /opt/ark/scripts/game-update-server.sh
Now edit the file:
Code: Select all
sudo vi /opt/ark/scripts/game-update-server.sh
Copy/paste the following into the file.
Code: Select all
#!/bin/bash
#############################################################
## Name : game-update-server.sh
## Version : 1.0
## Date : 2021-04-20
## Author : LHammonds
## Purpose : Update the server template from Steam.
## Compatibility : Verified on Ubuntu Server 20.04 LTS
## Requirements : Run as root or the specified install user.
## Run Frequency : Whenever needed. Template should never be running.
## Parameters : N/A
## Exit Codes :
## 0 = Success
## 1 = Invalid user
## ? = All other error codes are the result of steamcmd exit code.
######################## CHANGE LOG #########################
## DATE VER WHO WHAT WAS CHANGED
## ---------- --- --- ---------------------------------------
## 2021-04-20 1.0 LTH Created script.
#############################################################
## Import standard variables and functions. ##
source /etc/gamectl.conf
LogFile="${LogDir}/game-update-server.log"
SteamOut="${TempDir}/steam.out"
NoUpdate="already up to date"
UpgradeSuccess="fully installed"
#######################################
## PREREQUISITES ##
#######################################
## Verify required user access ##
if [ "${USER}" != "root" ] && [ "${USER}" != "${GameUser}" ]; then
printf "[ERROR] This script must be run as root or ${GameUser}.\n"
exit 1
fi
#######################################
## MAIN ##
#######################################
if [ -f ${SteamOut} ]; then
## Remove the temp file before we use it ##
rm ${SteamOut}
fi
printf "`date +%Y-%m-%d_%H:%M:%S` - Started template update.\n" | tee -a ${LogFile}
if [ "${USER}" == "root" ]; then
## Switch to the install user and update the instance ##
f_verbose "su --command='${SteamCMD} +login anonymous +force_install_dir ${TemplateDir} +app_update ${ServerID} +quit > ${SteamOut}' ${GameUser}"
su --command="${SteamCMD} +login anonymous +force_install_dir ${TemplateDir} +app_update ${ServerID} +quit > ${SteamOut}" ${GameUser}
ReturnCode=$?
elif [ "${USER}" == "${GameUser}" ]; then
## Already running as the install user, update the instance ##
f_verbose "${SteamCMD} +login anonymous +force_install_dir ${TemplateDir} +app_update ${ServerID} +quit > ${SteamOut}"
${SteamCMD} +login anonymous +force_install_dir ${TemplateDir} +app_update ${ServerID} +quit > ${SteamOut}
ReturnCode=$?
fi
f_verbose "[INFO] SteamCMD ReturnCode=${ReturnCode}"
if grep -Fq "${NoUpdate}" ${SteamOut}; then
## No update found ##
printf "[INFO] No update found.\n" | tee -a ${LogFile}
else
if grep -Fq "${UpgradeSuccess}" ${SteamOut}; then
## Upgrade peformed and was successful ##
printf "[INFO] Update performed and was successful.\n" | tee -a ${LogFile}
else
## Other issue (could be error, lack of space, timeout, etc.) ##
printf "[UNKNOWN] Unknown result...need exact wording.\n" | tee -a ${LogFile}
printf "[SAVE] Output text saved to ${GameRootDir}/bak/`date +%Y-%m-%d_%H-%M-%S`-steam.out\n" | tee -a ${LogFile}
cp ${SteamOut} ${BackupDir}/`date +%Y-%m-%d_%H-%M-%S`-steam.out
fi
fi
printf "`date +%Y-%m-%d_%H:%M:%S` - Completed template update.\n" | tee -a ${LogFile}
exit ${ReturnCode}
End of Method #2
Updating Workshop Mod Versions from Steam
This script will update the mods in the template folder from Steam Workshop. Since the template is never running so it can be updated at any time throughout the day.
This process is a bit more complicated because a simple download of the mods is not enough. The mods on the workshop are compressed and not usable as is and thus must be decompressed and made ready. You see the same behavior in the client. On the client side, you subscribe to workshop mods and steam downloads them but they have to be "installed" which is done automatically when you have the game started and sitting at the main menu. It shows a "installing mod" message at the bottom-right corner.
You can download the script straight from my github using the below commands or manually create the file yourself by copying the code from this forum (which may get outdated compared to github)
Method #1 - Obtain from GitHub:
Code: Select all
wget https://raw.githubusercontent.com/LHammonds/ark-bash/main/arkit.py --output-document /opt/ark/scripts/arkit.py
wget https://raw.githubusercontent.com/LHammonds/ark-bash/main/Ark_Mod_Downloader.py --output-document /opt/ark/scripts/Ark_Mod_Downloader.py
wget https://raw.githubusercontent.com/LHammonds/ark-bash/main/game-update-mods.sh --output-document /opt/ark/scripts/game-update-mods.sh
sudo chown arkserver:arkserver /opt/ark/scripts/*.py
sudo chown arkserver:arkserver /opt/ark/scripts/game-update-mods.sh
sudo chmod 0640 /opt/ark/scripts/*.py
sudo chmod 0750 /opt/ark/scripts/game-update-mods.sh
End of Method #1
Method #2 - Copy/Paste from this forum (but check GitHub for newer version):
Code: Select all
sudo touch /opt/ark/scripts/arkit.py
sudo touch /opt/ark/scripts/Ark_Mod_Downloader.py
sudo touch /opt/ark/scripts/game-update-mods.sh
sudo chown arkserver:arkserver /opt/ark/scripts/*.py
sudo chown arkserver:arkserver /opt/ark/scripts/game-update-mods.sh
sudo chmod 0640 /opt/ark/scripts/*.py
sudo chmod 0750 /opt/ark/scripts/game-update-mods.sh
Now edit the file:
Copy/paste the following into the file.
Code: Select all
'''
ARK: Survival Evolved Toolkit
Only supports Python3, Python2 is end of life and outdated thus not supported.
Purpose:
Provide a Python toolkit for ARK. Originally designed to unpack the workshop archives.
Notice:
I use PEP 8 as per it was intended; if you want to PEP 8 me read it first instead of being foolish: "A Foolish Consistency is the Hobgoblin of Little Minds"
'''
import struct
import zlib
import sys
import logging
__author__ = "James E"
__contact__ = "https://github.com/project-umbrella/arkit.py"
__copyright__ = "Copyright 2015, Project Umbrella"
__version__ = "0.0.0.1"
__status__ = "Prototype"
__date__ = "16 October 2015"
__license__ = "GPL v3.0 https://github.com/project-umbrella/arkit.py/blob/master/LICENSE"
logging.basicConfig(stream=sys.stderr, level=logging.CRITICAL)
class UnpackException(Exception):
pass
class SignatureUnpackException(UnpackException):
pass
class CorruptUnpackException(UnpackException):
pass
def unpack(src, dst):
'''
Unpacks ARK's Steam Workshop *.z archives.
Accepts two arguments:
src = Source File/Archive
dst = Destination File
Error Handling:
Currently logs errors via logging with an archive integrity as well as raising a custom exception. Also logs some debug and info messages.
All file system errors are handled by python core.
Process:
1. Open the source file.
2. Read header information from archive:
- 00 (8 bytes) signature (6 bytes) and format ver (2 bytes)
- 08 (8 byes) unpacked/uncompressed chunk size
- 10 (8 bytes) packed/compressed full size
- 18 (8 bytes) unpacked/uncompressed size
- 20 (8 bytes) first chunk packed/compressed size
- 26 (8 bytes) first chunk unpacked/uncompressed size
- 20 and 26 repeat until the total of all the unpacked/uncompressed chunk sizes matches the unpacked/uncompressed full size.
2. Read all the archive data and verify integrity (there should only be one partial chunk, and each chunk should match the archives header).
3. Write the file.
Development Note:
- Not thoroughly tested for errors. There may be instances where this method may fail either to extract a valid archive or detect a corrupt archive.
- Prevent overwriting files unless requested to do so.
- Create a batch method.
'''
with open(src, 'rb') as f:
sigver = struct.unpack('q', f.read(8))[0]
unpacked_chunk = f.read(8)
packed = f.read(8)
unpacked = f.read(8)
size_unpacked_chunk = struct.unpack('q', unpacked_chunk)[0]
size_packed = struct.unpack('q', packed)[0]
size_unpacked = struct.unpack('q', unpacked)[0]
#Verify the integrity of the Archive Header
if sigver == 2653586369:
if isinstance(size_unpacked_chunk, int) and isinstance(size_packed , int) and isinstance(size_unpacked , int):
logging.info("Archive is valid.")
logging.debug("Archive header size information. Unpacked Chunk: {}({}) Full Packed: {}({}) Full Unpacked: {}({})".format(size_unpacked_chunk, unpacked_chunk, size_packed, packed, size_unpacked, unpacked))
#Obtain the Archive Compression Index
compression_index = []
size_indexed = 0
while size_indexed < size_unpacked:
raw_compressed = f.read(8)
raw_uncompressed = f.read(8)
compressed = struct.unpack('q', raw_compressed)[0]
uncompressed = struct.unpack('q', raw_uncompressed)[0]
compression_index.append((compressed, uncompressed))
size_indexed += uncompressed
logging.debug("{}: {}/{} ({}/{}) - {} - {}".format(len(compression_index), size_indexed, size_unpacked, compressed, uncompressed, raw_compressed, raw_uncompressed))
if size_unpacked != size_indexed:
msg = "Header-Index mismatch. Header indicates it should only have {} bytes when uncompressed but the index indicates {} bytes.".format(size_unpacked, size_indexed)
logging.critical(msg)
raise CorruptUnpackException(msg)
#Read the actual archive data
data = b''
read_data = 0
for compressed, uncompressed in compression_index:
compressed_data = f.read(compressed)
uncompressed_data = zlib.decompress(compressed_data)
#Verify the size of the data is consistent with the archives index
if len(uncompressed_data) == uncompressed:
data += uncompressed_data
read_data += 1
#Verify there is only one partial chunk
if len(uncompressed_data) != size_unpacked_chunk and read_data != len(compression_index):
msg = "Index contains more than one partial chunk: was {} when the full chunk size is {}, chunk {}/{}".format(len(uncompressed_data), size_unpacked_chunk, read_data, len(compression_index))
logging.critical(msg)
raise CorruptUnpackException(msg)
else:
msg = "Uncompressed chunk size is not the same as in the index: was {} but should be {}.".format(len(uncompressed_data), uncompressed)
logging.critical(msg)
raise CorruptUnpackException(msg)
else:
msg = "Data types in the headers should be int's. Size Types: unpacked_chunk({}), packed({}), unpacked({})".format(sigver, type(size_unpacked_chunk), type(size_packed), type(size_unpacked))
logging.critical(msg)
raise CorruptUnpackException(msg)
else:
msg = "The signature and format version is incorrect. Signature was {} should be 2653586369.".format(sigver)
logging.critical(msg)
raise SignatureUnpackException(msg)
#Write the extracted data to disk
with open(dst, 'wb') as f:
f.write(data)
logging.info("Archive has been extracted.")
Code: Select all
sudo vi /opt/ark/scripts/Ark_Mod_Downloader.py
Copy/paste the following into the file.
Code: Select all
#############################################################
## Name : Ark_Mod_Download.py
## Version : 1.1
## Date : 2021-04-20
## Author : barrycarey (Matthew Carey)
## Purpose : Download and extract Ark mods from Steam workshop.
## Compatibility : Verified on Ubuntu Server 20.04 LTS
## Requirements : Python3, arkit.py
## Run Frequency : As needed.
## Parameters :
## --modids - Space separated list of Mod IDs to download
## --workingdir (Optional) - Home directory of your ARK server
## --steamcmd (Optional) - Path to SteamCMD. If not provided the tool will
## download SteamCMD to the CWD
## --update (Optional) - Update all current mods installed on the server
## --namefile (Optional) - Creates "Modname.name" file in the mod folder.
## Example:
## python3 /opt/ark/Ark_Mod_Downloader.py --modids "1609138312"
## --workingdir "/opt/ark/template" --steamcmd "/usr/games" --namefile
## Exit Codes :
## 0 = Success.
## 1 = SteamCMD not found.
## 2 = Ark not found in path.
## 3 = Failed to extract SteamCMD archive.
## 4 = Mod ID not provided nor request to update all mods.
######################## CHANGE LOG #########################
## DATE VER WHO WHAT WAS CHANGED
## ---------- --- --- ---------------------------------------
## 2017-10-18 1.0 bc Created script.
## 2021-04-20 1.1 LTH Modified to work on Linux and include exit codes.
#############################################################
import arkit
import sys
import os
import argparse
import shutil
import subprocess
from collections import OrderedDict
import struct
import urllib.request
import zipfile
class ArkModDownloader():
def __init__(self, steamcmd, modids, working_dir, mod_update, modname, preserve=False):
# If not working directory provided, check if CWD has an ARK server.
self.working_dir = working_dir
if not working_dir:
self.working_dir_check()
self.steamcmd = steamcmd # Path to SteamCMD exe
if not self.steamcmd_check():
print("SteamCMD Not Found And We Were Unable To Download It")
sys.exit(1)
self.modname = modname
self.installed_mods = [] # List to hold installed mods
self.map_names = [] # Stores map names from mod.info
self.meta_data = OrderedDict([]) # Stores key value from modmeta.info
self.temp_mod_path = os.path.join(self.working_dir, r"steamapps/workshop/content/346110")
self.preserve = preserve
self.prep_steamcmd()
if mod_update:
print("[+] Mod Update Is Selected. Updating Your Existing Mods")
self.update_mods()
# If any issues happen in download and extract chain this returns false
if modids:
for mod in modids:
if self.download_mod(mod):
if self.move_mod(mod):
print("[+] Mod {} Installation Finished".format(str(mod)))
else:
print("[+] There was as problem downloading mod {}. See above errors".format(str(mod)))
def create_mod_name_txt(self, mod_folder, modid):
print(os.path.join(mod_folder, self.map_names[0] + " - " + modid + ".txt"))
with open(os.path.join(mod_folder, self.map_names[0] + ".txt"), "w+") as f:
f.write(modid)
def working_dir_check(self):
print("[!] No working directory provided. Checking Current Directory")
print("[!] " + os.getcwd())
if os.path.isdir(os.path.join(os.getcwd(), "ShooterGame/Content")):
print("[+] Current Directory Has Ark Server. Using The Current Directory")
self.working_dir = os.getcwd()
else:
print("[x] Current Directory Does Not Contain An ARK Server. Aborting")
sys.exit(2)
def steamcmd_check(self):
"""
If SteamCMD path is provided verify that exe exists.
If no path provided check TCAdmin path working dir. If not located try to download SteamCMD.
:return: Bool
"""
# Check provided directory
if self.steamcmd:
print("[+] Checking Provided Path For SteamCMD")
if os.path.isfile(os.path.join(self.steamcmd, "steamcmd")):
self.steamcmd = os.path.join(self.steamcmd, "steamcmd")
print("[+] SteamCMD Found At Provided Path")
return True
# Check TCAdmin Directory
print("[+] SteamCMD Location Not Provided. Checking Common Locations")
if os.path.isfile(r"C:\Program Files\TCAdmin2\Monitor\Tools\SteamCmd\steamcmd"):
print("[+] SteamCMD Located In TCAdmin Directory")
self.steamcmd = r"C:\Program Files\TCAdmin2\Monitor\Tools\SteamCmd\steamcmd"
return True
# Check working directory
if os.path.isfile(os.path.join(self.working_dir, "SteamCMD/steamcmd")):
print("[+] Located SteamCMD")
self.steamcmd = os.path.join(self.working_dir, "SteamCMD/steamcmd")
return True
print("[+} SteamCMD Not Found In Common Locations. Attempting To Download")
try:
with urllib.request.urlopen("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip") as response:
if not os.path.isdir(os.path.join(self.working_dir, "SteamCMD")):
os.mkdir(os.path.join(self.working_dir, "SteamCMD"))
steam_cmd_zip = os.path.join(self.working_dir, "steamcmd.zip")
with open(steam_cmd_zip, "w+b") as output:
output.write(response.read())
zip_file = zipfile.ZipFile(steam_cmd_zip)
try:
zip_file.extractall(os.path.join(self.working_dir, "SteamCMD"))
except zipfile.BadZipfile as e:
print("[x] Failed To Extract steamcmd.zip. Aborting")
print("[x] Error: " + e)
sys.exit(3)
except urllib.request.HTTPError as e:
print("[x] Failed To Download SteamCMD. Aborting")
print("[x] ERROR: " + e)
return False
self.steamcmd = os.path.join(self.working_dir, r"SteamCMD/steamcmd")
return True
def prep_steamcmd(self):
"""
Delete steamapp folder to prevent Steam from remembering it has downloaded this mod before
This is mainly for game hosts. Example, hosts using TCAdmin have one SteamCMD folder. If mod was downloaded
by another customer SteamCMD will think it already exists and not download again.
:return:
"""
if self.preserve:
return
steamapps = os.path.join(os.path.dirname(self.steamcmd), "steamapps")
if os.path.isdir(steamapps):
print("[+] Removing Steamapps Folder")
try:
shutil.rmtree(steamapps)
except OSError:
"""
If run on a TCAdmin server using TCAdmin's SteamCMD this may prevent mod from downloading if another
user has downloaded the same mod. This is due to SteamCMD's cache. It will think this mod has is
already installed and up to date.
"""
print("[x] Failed To Remove Steamapps Folder. This is normally okay.")
print("[x] If this is a TCAdmin Server and using the TCAdmin SteamCMD it may prevent mod from downloading")
def update_mods(self):
self.build_list_of_mods()
if self.installed_mods:
for mod in self.installed_mods:
print("[+] Updating Mod " + mod)
if not self.download_mod(mod):
print("[x] Error Updating Mod " + mod)
else:
print("[+] No Installed Mods Found. Skipping Update")
def build_list_of_mods(self):
"""
Build a list of all installed mods by grabbing all directory names from the mod folder
:return:
"""
if not os.path.isdir(os.path.join(self.working_dir, "ShooterGame/Content/Mods")):
return
for curdir, dirs, files in os.walk(os.path.join(self.working_dir, "ShooterGame/Content/Mods")):
for d in dirs:
self.installed_mods.append(d)
break
def download_mod(self, modid):
"""
Launch SteamCMD to download ModID
/usr/games/steamcmd +login anonymous +force_install_dir /ArkGenesis +app_update 376030 +workshop_download_item 346110 849985437 +quit
:return:
"""
print("[+] Starting Download of Mod " + str(modid))
args = []
args.append(self.steamcmd)
args.append("+login anonymous")
args.append("+force_install_dir ")
args.append(self.working_dir)
args.append("+workshop_download_item")
args.append("346110")
args.append(modid)
args.append("+quit")
#wd = os.getcwd()
#os.chdir(self.working_dir)
res = subprocess.run(args, shell=False)
#os.chdir(wd)
print("cmd used: "+str(res.args))
return True if self.extract_mod(modid) else False
def extract_mod(self, modid):
"""
Extract the .z files using the arkit lib.
If any file fails to download this whole script will abort
:return: None
"""
print("[+] Extracting .z Files.")
try:
for curdir, subdirs, files in os.walk(os.path.join(self.temp_mod_path, modid, "WindowsNoEditor")):
for file in files:
name, ext = os.path.splitext(file)
if ext == ".z":
src = os.path.join(curdir, file)
dst = os.path.join(curdir, name)
uncompressed = os.path.join(curdir, file + ".uncompressed_size")
arkit.unpack(src, dst)
#print("[+] Extracted " + file)
os.remove(src)
if os.path.isfile(uncompressed):
os.remove(uncompressed)
except (arkit.UnpackException, arkit.SignatureUnpackException, arkit.CorruptUnpackException) as e:
print("[x] Unpacking .z files failed, aborting mod install")
return False
if self.create_mod_file(modid):
if self.move_mod(modid):
return True
else:
return False
def move_mod(self, modid):
"""
Move mod from SteamCMD download location to the ARK server.
It will delete an existing mod with the same ID
:return:
"""
ark_mod_folder = os.path.join(self.working_dir, "ShooterGame/Content/Mods")
output_dir = os.path.join(ark_mod_folder, str(modid))
source_dir = os.path.join(self.temp_mod_path, modid, "WindowsNoEditor")
# TODO Need to handle exceptions here
if not os.path.isdir(ark_mod_folder):
print("[+] Creating Directory: " + ark_mod_folder)
os.mkdir(ark_mod_folder)
if os.path.isdir(output_dir):
shutil.rmtree(output_dir)
print("[+] Moving Mod Files To: " + output_dir)
shutil.copytree(source_dir, output_dir)
print("[+] Moving Mod File To: " + output_dir + ".mod")
shutil.move(output_dir+"/.mod", output_dir+".mod")
if self.modname:
print("Creating Mod Name File")
self.create_mod_name_txt(ark_mod_folder, modid)
return True
def create_mod_file(self, modid):
"""
Create the .mod file.
This code is an adaptation of the code from Ark Server Launcher. All credit goes to Face Wound on Steam
:return:
"""
if not self.parse_base_info(modid) or not self.parse_meta_data(modid):
return False
print("[+] Writing .mod File")
with open(os.path.join(self.temp_mod_path, modid, r"WindowsNoEditor/.mod"), "w+b") as f:
modid = int(modid)
#f.write(struct.pack('ixxxx', modid)) # Needs 4 pad bits
f.write(struct.pack('Ixxxx', modid)) # Needs 4 pad bits
self.write_ue4_string("ModName", f)
self.write_ue4_string("", f)
map_count = len(self.map_names)
f.write(struct.pack("i", map_count))
for m in self.map_names:
self.write_ue4_string(m, f)
# Not sure of the reason for this
num2 = 4280483635
f.write(struct.pack('I', num2))
num3 = 2
f.write(struct.pack('i', num3))
if "ModType" in self.meta_data:
mod_type = b'1'
else:
mod_type = b'0'
# TODO The packing on this char might need to be changed
f.write(struct.pack('p', mod_type))
meta_length = len(self.meta_data)
f.write(struct.pack('i', meta_length))
for k, v in self.meta_data.items():
self.write_ue4_string(k, f)
self.write_ue4_string(v, f)
return True
def read_ue4_string(self, file):
count = struct.unpack('i', file.read(4))[0]
flag = False
if count < 0:
flag = True
count -= 1
if flag or count <= 0:
return ""
return file.read(count)[:-1].decode()
def write_ue4_string(self, string_to_write, file):
string_length = len(string_to_write) + 1
file.write(struct.pack('i', string_length))
barray = bytearray(string_to_write, "utf-8")
file.write(barray)
file.write(struct.pack('p', b'0'))
def parse_meta_data(self, modid):
"""
Parse the modmeta.info files and extract the key value pairs need to for the .mod file.
How To Parse modmeta.info:
1. Read 4 bytes to tell how many key value pairs are in the file
2. Read next 4 bytes tell us how many bytes to read ahead to get the key
3. Read ahead by the number of bytes retrieved from step 2
4. Read next 4 bytes to tell how many bytes to read ahead to get value
5. Read ahead by the number of bytes retrieved from step 4
6. Start at step 2 again
:return: Dict
"""
print("[+] Collecting Mod Meta Data From modmeta.info")
print("[+] Located The Following Meta Data:")
mod_meta = os.path.join(self.temp_mod_path, modid, r"WindowsNoEditor/modmeta.info")
if not os.path.isfile(mod_meta):
print("[x] Failed To Locate modmeta.info. Cannot continue without it. Aborting")
return False
with open(mod_meta, "rb") as f:
total_pairs = struct.unpack('i', f.read(4))[0]
for i in range(total_pairs):
key, value = "", ""
key_bytes = struct.unpack('i', f.read(4))[0]
key_flag = False
if key_bytes < 0:
key_flag = True
key_bytes -= 1
if not key_flag and key_bytes > 0:
raw = f.read(key_bytes)
key = raw[:-1].decode()
value_bytes = struct.unpack('i', f.read(4))[0]
value_flag = False
if value_bytes < 0:
value_flag = True
value_bytes -= 1
if not value_flag and value_bytes > 0:
raw = f.read(value_bytes)
value = raw[:-1].decode()
# TODO This is a potential issue if there is a key but no value
if key and value:
print("[!] " + key + ":" + value)
self.meta_data[key] = value
return True
def parse_base_info(self, modid):
print("[+] Collecting Mod Details From mod.info")
mod_info = os.path.join(self.temp_mod_path, modid, r"WindowsNoEditor/mod.info")
if not os.path.isfile(mod_info):
print("[x] Failed to locate mod.info at " + mod_info + ". Cannot Continue. Aborting")
return False
with open(mod_info, "rb") as f:
self.read_ue4_string(f)
map_count = struct.unpack('i', f.read(4))[0]
for i in range(map_count):
cur_map = self.read_ue4_string(f)
if cur_map:
self.map_names.append(cur_map)
return True
def main():
parser = argparse.ArgumentParser(description="A utility to download ARK Mods via SteamCMD")
parser.add_argument("--workingdir", default=None, dest="workingdir", help="Game server home directory. Current Directory is used if this is not provided")
parser.add_argument("--modids", nargs="+", default=None, dest="modids", help="ID of Mod To Download")
parser.add_argument("--steamcmd", default=None, dest="steamcmd", help="Path to SteamCMD")
parser.add_argument("--update", default=None, action="store_true", dest="mod_update", help="Update Existing Mods. ")
parser.add_argument("--preserve", default=None, action="store_true", dest="preserve", help="Don't Delete StreamCMD Content Between Runs")
parser.add_argument("--namefile", default=None, action="store_true", dest="modname", help="Create a .name File With Mods Text Name")
args = parser.parse_args()
if not args.modids and not args.mod_update:
print("[x] No Mod ID Provided and Update Not Selected. Aborting")
print("[?] Please provide a Mod ID to download or use --update to update your existing mods")
sys.exit(4)
ArkModDownloader(args.steamcmd,
args.modids,
args.workingdir,
args.mod_update,
args.modname,
args.preserve)
if __name__ == '__main__':
main()
Code: Select all
sudo vi /opt/ark/scripts/game-update-mods.sh
Copy/paste the following into the file.
Code: Select all
#!/bin/bash
#############################################################
## Name : game-update-mods.sh
## Version : 1.0
## Date : 2021-04-20
## Author : LHammonds
## Purpose : Update mods for the game template.
## Compatibility : Verified on Ubuntu Server 20.04 LTS
## Requirements : Run as root or the specified install user.
## Run Frequency : As needed.
## Parameters : None
## Exit Codes :
## 0 = Success
## 200 = ERROR Incorrect user
## 201 = ERROR Invalid template path
## ? = All other numbers are the sum of the total amount of errors.
######################## CHANGE LOG #########################
## DATE VER WHO WHAT WAS CHANGED
## ---------- --- --- ---------------------------------------
## 2021-04-20 1.0 LTH Created script.
#############################################################
## Import standard variables and functions. ##
source /etc/gamectl.conf
ErrorCount=0
LogFile="${LogDir}/game-update-mods.log"
StartTime="$(date +%s)"
#######################################
## PREREQUISITES ##
#######################################
## Verify required user access. ##
if [ "${USER}" != "root" ] && [ "${USER}" != "${GameUser}" ]; then
printf "[ERROR] This script must be run as root or ${GameUser}.\n"
exit 200
fi
## Validate Template Folder ##
if [ ! -d "${TemplateDir}" ]; then
printf "[ERROR] Invalid template path. ${TemplateDir} does not exist.\n"
exit 201
fi
#######################################
## MAIN ##
#######################################
printf "`date +%Y-%m-%d_%H:%M:%S` - Started mod update.\n" | tee -a ${LogFile}
if [ -d ${TempDir}/dumps ]; then
## Remove temporary folder used by ArkModDownloader ##
rm -rf ${TempDir}/dumps
fi
for ModID in ${GameModIds//,/ }
do
if [ -d ${TemplateDir}/steamapps/workshop/content/${GameID}/${ModID} ]; then
## Delete prior download folder ##
rm -rf ${TemplateDir}/steamapps/workshop/content/${GameID}/${ModID}
fi
if [ -f ${TemplateDir}/steamapps/workshop/content/${GameID}/${ModID}.mod ]; then
## Delete prior mod descriptor file ##
rm ${TemplateDir}/steamapps/workshop/content/${GameID}/${ModID}.mod
fi
if [ "${USER}" == "root" ]; then
## Switch to the install user and update the instance ##
f_verbose "su --command='${ArkModDLCMD} --modids '${ModID}' --workingdir '${TemplateDir}' --steamcmd '${SteamDir}' --namefile' ${GameUser}"
su --command="${ArkModDLCMD} --modids '${ModID}' --workingdir '${TemplateDir}' --steamcmd '${SteamDir}' --namefile" ${GameUser}
ReturnCode=$?
elif [ "${USER}" == "${GameUser}" ]; then
## Already running as the install user, update the instance ##
f_verbose "${ArkModDLCMD} --modids '${ModID}' --workingdir '${TemplateDir}' --steamcmd '${SteamDir}' --namefile ${GameUser}"
${ArkModDLCMD} --modids "${ModID}" --workingdir "${TemplateDir}" --steamcmd "${SteamDir}" --namefile ${GameUser}
ReturnCode=$?
fi
if [ "${ReturnCode}" == "0" ]; then
printf "[SUCCESS] Mod ${ModID} downloaded.\n" | tee -a ${LogFile}
chown --recursive ${GameUser}:${GameGroup} ${TemplateDir}/ShooterGame/Content/Mods/${ModID}*
find ${TemplateDir}/ShooterGame/Content/Mods/${ModID} -type d -exec chmod 0750 {} \;
find ${TemplateDir}/ShooterGame/Content/Mods/${ModID} -type f -exec chmod 0640 {} \;
else
printf "[ERROR] Mod ${ModID} failed with ReturnCode=${ReturnCode}\n" | tee -a ${LogFile}
((ErrorCount++))
fi
done
f_verbose "[INFO] ErrorCount=${ErrorCount}"
## Calculate total runtime ##
FinishTime="$(date +%s)"
ElapsedTime="$(expr ${FinishTime} - ${StartTime})"
Hours=$((${ElapsedTime} / 3600))
ElapsedTime=$((${ElapsedTime} - ${Hours} * 3600))
Minutes=$((${ElapsedTime} / 60))
Seconds=$((${ElapsedTime} - ${Minutes} * 60))
printf "[INFO] Total runtime: ${Hours} hour(s) ${Minutes} minute(s) ${Seconds} second(s)\n" | tee -a ${LogFile}
printf "`date +%Y-%m-%d_%H:%M:%S` - Completed mod update.\n" | tee -a ${LogFile}
exit ${ErrorCount}
End of Method #2
Synchronize One Instance with the Template
This script will copy the files in the template folder that have changed (e.g. updated) to a specified offline instance. It does not matter if the changes were from a server update or a mod update...it will synchronize the files to be the same. Because this only copies what has changed, it makes the process very quick. However, the game instance CANNOT be running and should be stopped prior to trying to synchronize the files.
You can download the script straight from my github using the below commands or manually create the file yourself by copying the code from this forum (which may get outdated compared to github)
Method #1 - Obtain from GitHub:
Code: Select all
wget https://raw.githubusercontent.com/LHammonds/ark-bash/main/game-sync-1.sh --output-document /opt/ark/scripts/game-sync-1.sh
sudo chown arkserver:arkserver /opt/ark/scripts/game-sync-1.sh
sudo chmod 0750 /opt/ark/scripts/game-sync-1.sh
End of Method #1
Method #2 - Copy/Paste from this forum (but check GitHub for newer version):
Code: Select all
sudo touch /opt/ark/scripts/game-sync-1.sh
sudo chown arkserver:arkserver /opt/ark/scripts/game-sync-1.sh
sudo chmod 0750 /opt/ark/scripts/game-sync-1.sh
Now edit the file:
Code: Select all
sudo vi /opt/ark/scripts/game-sync-1.sh
Copy/paste the following into the file.
Code: Select all
#!/bin/bash
#############################################################
## Name : game-sync-1.sh
## Version : 1.0
## Date : 2021-04-20
## Author : LHammonds
## Purpose : Synchronize template to a game instance.
## Compatibility : Verified on Ubuntu Server 20.04 LTS
## Requirements : Run as root or the specified install user.
## : Instance needs to be stopped.
## Run Frequency : As needed or before starting a server.
## Parameters : Game Instance (or none for interactive selection)
## Exit Codes :
## 0 = Success
## 1 = ERROR Invalid user
## 2 = ERROR Cannot sync online instance
## 3 = ERROR Invalid parameter
## 4 = ERROR No offline instances
######################## CHANGE LOG #########################
## DATE VER WHO WHAT WAS CHANGED
## ---------- --- --- ---------------------------------------
## 2021-04-20 1.0 LTH Created script.
#############################################################
## Import standard variables and functions. ##
source /etc/gamectl.conf
ErrorCount=0
LogFile="${LogDir}/game-sync-server.log"
#######################################
## FUNCTIONS ##
#######################################
function f_sync()
{
StartTime="$(date +%s)"
printf "`date +%Y-%m-%d_%H:%M:%S` - Sync ${1} started.\n" | tee -a ${LogFile}
if [ "${USER}" = "root" ]; then
## Switch to the install user and update the instance ##
f_verbose "su --command='rsync -a --exclude 'ShooterGame/Saved' --exclude 'steamapps/workshop' ${TemplateDir}/ ${GameRootDir}/${1}' ${GameUser}"
su --command="rsync -a --exclude 'ShooterGame/Saved' --exclude 'steamapps/workshop' ${TemplateDir}/ ${GameRootDir}/${1}" ${GameUser}
else
## Already running as install user, update the instance ##
f_verbose "rsync -a --exclude 'ShooterGame/Saved' --exclude 'steamapps/workshop' ${TemplateDir}/ ${GameRootDir}/${1}"
rsync -a --exclude 'ShooterGame/Saved' --exclude 'steamapps/workshop' ${TemplateDir}/ ${GameRootDir}/${1}
fi
ReturnCode=$?
if [ "${ReturnCode}" != "0" ]; then
printf "[ERROR] rsync ReturnCode=${ReturnCode}\n" | tee -a ${LogFile}
fi
## Calculate total runtime ##
FinishTime="$(date +%s)"
ElapsedTime="$(expr ${FinishTime} - ${StartTime})"
Hours=$((${ElapsedTime} / 3600))
ElapsedTime=$((${ElapsedTime} - ${Hours} * 3600))
Minutes=$((${ElapsedTime} / 60))
Seconds=$((${ElapsedTime} - ${Minutes} * 60))
printf "[INFO] Total runtime: ${Hours} hour(s) ${Minutes} minute(s) ${Seconds} second(s)\n" | tee -a ${LogFile}
printf "`date +%Y-%m-%d_%H:%M:%S` - Sync ${1} completed.\n" | tee -a ${LogFile}
}
#######################################
## PREREQUISITES ##
#######################################
## Verify required user access ##
if [ "${USER}" != "root" ] && [ "${USER}" != "${GameUser}" ]; then
printf "[ERROR] This script must be run as root or ${GameUser}.\n"
exit 1
fi
#######################################
## MAIN ##
#######################################
GameInstance=""
## Check command-line parameter for specified instance ##
case "$1" in
"") printf "[INFO] Game instance not specified as parameter. Going into pick list mode.\n";;
*) GameInstance=$1;;
esac
if [ "${GameInstance}" != "" ]; then
## Validate GameInstance ##
if [ -f "${GameRootDir}/${GameInstance}/ShooterGame/Binaries/Linux/ShooterGameServer" ]; then
## Verify instance is offline ##
${ScriptDir}/game-online.sh ${GameInstance} > /dev/null 2>&1
ReturnCode=$?
if [ "${ReturnCode}" == "0" ]; then
## Instance is offline, use parameter ##
f_sync ${GameInstance}
exit 0
else
printf "[ERROR] Cannot sync ${GameInstance} while online.\n"
exit 2
fi
else
printf "[ERROR] Invalid instance.\n"
exit 3
fi
fi
## Loop through all defined game instances and build list of what is online ##
arrList=()
for intIndex in "${!arrInstanceName[@]}"
do
## Verify instance is installed ##
if [ -f "${GameRootDir}/${arrInstanceName[${intIndex}]}/ShooterGame/Binaries/Linux/ShooterGameServer" ]; then
## Verify instance is offline ##
${ScriptDir}/game-online.sh ${arrInstanceName[${intIndex}]} > /dev/null 2>&1
ReturnCode=$?
if [ "${ReturnCode}" == "0" ]; then
## Instance is inactive ##
arrList+=(${arrInstanceName[${intIndex}]})
fi
fi
done
if [ ${#arrList[@]} -eq 0 ]; then
## If no instances are offline, the exit ##
printf "[INFO] No instances are offline. Cannot continue sync.\n"
exit 4
else
## Loop thru offline instances, present options for user to select ##
printf "Select which offline instance you want synchronized from the template:\n"
for intIndex in "${!arrList[@]}"
do
printf " ${intIndex}) ${arrList[intIndex]}\n"
done
printf " x) Exit\n"
read -n 1 -p "Your choice: " char_answer;
printf "\n"
## This will break if there are more than 10 instances ##
case ${char_answer} in
0) f_sync ${arrList[0]};;
1) f_sync ${arrList[1]};;
2) f_sync ${arrList[2]};;
3) f_sync ${arrList[3]};;
4) f_sync ${arrList[4]};;
5) f_sync ${arrList[5]};;
6) f_sync ${arrList[6]};;
7) f_sync ${arrList[7]};;
8) f_sync ${arrList[8]};;
9) f_sync ${arrList[9]};;
x|X) printf "Exit\n";;
*) printf "Exit\n";;
esac
fi
exit 0
End of Method #2
Synchronize All Instances with the Template
This script will copy the files in the template folder that have changed (e.g. updated) to all offline instances. It does not matter if the changes were from a server update or a mod update...it will synchronize the files to be the same. Because this only copies what has changed, it makes the process very quick. However, the game instance CANNOT be running and should be stopped prior to trying to synchronize the files.
You can download the script straight from my github using the below commands or manually create the file yourself by copying the code from this forum (which may get outdated compared to github)
Method #1 - Obtain from GitHub:
Code: Select all
wget https://raw.githubusercontent.com/LHammonds/ark-bash/main/game-sync-all.sh --output-document /opt/ark/scripts/game-sync-all.sh
sudo chown arkserver:arkserver /opt/ark/scripts/game-sync-all.sh
sudo chmod 0750 /opt/ark/scripts/game-sync-all.sh
End of Method #1
Method #2 - Copy/Paste from this forum (but check GitHub for newer version):
Code: Select all
sudo touch /opt/ark/scripts/game-sync-all.sh
sudo chown arkserver:arkserver /opt/ark/scripts/game-sync-all.sh
sudo chmod 0750 /opt/ark/scripts/game-sync-all.sh
Now edit the file:
Code: Select all
sudo vi /opt/ark/scripts/game-sync-all.sh
Copy/paste the following into the file.
Code: Select all
#!/bin/bash
#############################################################
## Name : game-sync-all.sh
## Version : 1.0
## Date : 2021-04-20
## Author : LHammonds
## Purpose : Synchronize template to all game instances.
## Compatibility : Verified on Ubuntu Server 20.04 LTS
## Requirements : Run as root or the specified install user.
## : Instance needs to be stopped.
## Run Frequency : As needed or before starting a server.
## Parameters : Game Instance
## Exit Codes :
## 0 = Success
## 1 = ERROR Invalid user
## 2 = ERROR No offline instances
######################## CHANGE LOG #########################
## DATE VER WHO WHAT WAS CHANGED
## ---------- --- --- ---------------------------------------
## 2021-04-20 1.0 LTH Created script.
#############################################################
## Import standard variables and functions. ##
source /etc/gamectl.conf
ErrorCount=0
LogFile="${LogDir}/game-sync-server.log"
#######################################
## FUNCTIONS ##
#######################################
function f_sync()
{
StartTime="$(date +%s)"
printf "`date +%Y-%m-%d_%H:%M:%S` - Sync ${1} started.\n" | tee -a ${LogFile}
if [ "${USER}" = "root" ]; then
## Switch to the install user and update the instance ##
f_verbose "su --command='rsync -a --exclude 'ShooterGame/Saved' --exclude 'steamapps/workshop' ${TemplateDir}/ ${GameRootDir}/${1}' ${GameUser}"
su --command="rsync -a --exclude 'ShooterGame/Saved' --exclude 'steamapps/workshop' ${TemplateDir}/ ${GameRootDir}/${1}" ${GameUser}
else
## Already running as install user, update the instance ##
f_verbose "rsync -a --exclude 'ShooterGame/Saved' --exclude 'steamapps/workshop' ${TemplateDir}/ ${GameRootDir}/${1}"
rsync -a --exclude 'ShooterGame/Saved' --exclude 'steamapps/workshop' ${TemplateDir}/ ${GameRootDir}/${1}
fi
ReturnCode=$?
if [ "${ReturnCode}" != "0" ]; then
printf "[ERROR] rsync ReturnCode=${ReturnCode}\n" | tee -a ${LogFile}
fi
## Calculate total runtime ##
FinishTime="$(date +%s)"
ElapsedTime="$(expr ${FinishTime} - ${StartTime})"
Hours=$((${ElapsedTime} / 3600))
ElapsedTime=$((${ElapsedTime} - ${Hours} * 3600))
Minutes=$((${ElapsedTime} / 60))
Seconds=$((${ElapsedTime} - ${Minutes} * 60))
printf "[INFO] Total runtime: ${Hours} hour(s) ${Minutes} minute(s) ${Seconds} second(s)\n" | tee -a ${LogFile}
printf "`date +%Y-%m-%d_%H:%M:%S` - Sync ${1} completed.\n" | tee -a ${LogFile}
}
#######################################
## PREREQUISITES ##
#######################################
## Verify required user access ##
if [ "${USER}" != "root" ] && [ "${USER}" != "${GameUser}" ]; then
printf "[ERROR] This script must be run as root or ${GameUser}.\n"
exit 1
fi
#######################################
## MAIN ##
#######################################
## Loop through all defined game instances and build list of what is online ##
arrList=()
for intIndex in "${!arrInstanceName[@]}"
do
## Verify instance is installed ##
if [ -f "${GameRootDir}/${arrInstanceName[${intIndex}]}/ShooterGame/Binaries/Linux/ShooterGameServer" ]; then
## Verify instance is offline ##
${ScriptDir}/game-online.sh ${arrInstanceName[${intIndex}]} > /dev/null 2>&1
ReturnCode=$?
if [ "${ReturnCode}" == "0" ]; then
## Instance is inactive ##
arrList+=(${arrInstanceName[${intIndex}]})
fi
fi
done
if [ ${#arrList[@]} -eq 0 ]; then
## If no instances are offline, the exit ##
printf "[INFO] No instances are offline. Cannot continue sync.\n"
exit 2
else
## Loop thru offline instances, run sync ##
for intIndex in "${!arrList[@]}"
do
## Sync instance ##
f_sync ${arrList[intIndex]}
done
fi
exit 0
End of Method #2