Difference between revisions of "Workshop Browser"

 
(99 intermediate revisions by 2 users not shown)
Line 1: Line 1:
The Steam Workshop browser allows your users to download and install mods and maps from Steam with a single click.
+
The Steam Workshop browser allows your users to download and install mods and maps from Steam with a single click. Each game handles Workshop mods differently. You must configure scripts that move the mod files to the correct location after it has been downloaded.
  
 +
 +
[[File:Workshop_Browser.png|800px]]
  
 
== Configure the Workshop Browser ==
 
== Configure the Workshop Browser ==
Line 29: Line 31:
 
: '''FileNameNoPath''' - The filename without any paths. If the content has more than 1 file this value is blank. For example: aim_ak47_training_csgo.bsp
 
: '''FileNameNoPath''' - The filename without any paths. If the content has more than 1 file this value is blank. For example: aim_ak47_training_csgo.bsp
 
: '''FileNameSavePath''' - The full path where the single file is downloaded. For example ServiceRoot/steamapps/Workshop/content/STOREID/FILEID/mymaps/aim_ak47_training_csgo.bsp
 
: '''FileNameSavePath''' - The full path where the single file is downloaded. For example ServiceRoot/steamapps/Workshop/content/STOREID/FILEID/mymaps/aim_ak47_training_csgo.bsp
: '''TagsArray''' - A string array that contains the content's tags. (added in 2.0.130.1)
+
: '''TagsArray''' - A string array that contains the content's tags.
: '''Tags''' - A list of the content's tags separated by comma. (added in 2.0.130.1)
+
: '''Tags''' - A list of the content's tags separated by comma.
: '''FileTitle''' - The content's name. (added in 2.0.130.1)
+
: '''FileTitle''' - The content's name.
  
  
Line 39: Line 41:
 
: [[ThisServer]], [[ThisGame]], [[ThisUser]], [[ThisService]]
 
: [[ThisServer]], [[ThisGame]], [[ThisUser]], [[ThisService]]
 
: '''Available Variables:'''
 
: '''Available Variables:'''
: '''FileId''' - The id of the Workshop content that was installed.
+
: '''FileId''' - The id of the Workshop content that was uninstalled.
 
: '''FileIds''' - A list of currently installed file ids formatted and separated according to the game's configuration. It does not include the file id that is being uninstalled.
 
: '''FileIds''' - A list of currently installed file ids formatted and separated according to the game's configuration. It does not include the file id that is being uninstalled.
 
: '''FileIdsArray''' - An uint32 array of currently installed file ids. It does not include the file id that is being uninstalled.
 
: '''FileIdsArray''' - An uint32 array of currently installed file ids. It does not include the file id that is being uninstalled.
Line 47: Line 49:
 
: '''FileNameNoPath''' - The filename without any paths. If the content has more than 1 file this value is blank. For example: aim_ak47_training_csgo.bsp
 
: '''FileNameNoPath''' - The filename without any paths. If the content has more than 1 file this value is blank. For example: aim_ak47_training_csgo.bsp
 
: '''FileNameSavePath''' - The full path where the single file is downloaded. For example ServiceRoot/steamapps/Workshop/content/STOREID/FILEID/mymaps/aim_ak47_training_csgo.bsp
 
: '''FileNameSavePath''' - The full path where the single file is downloaded. For example ServiceRoot/steamapps/Workshop/content/STOREID/FILEID/mymaps/aim_ak47_training_csgo.bsp
: '''TagsArray''' - A string array that contains the content's tags. (added in 2.0.130.1)
+
: '''TagsArray''' - A string array that contains the content's tags.
: '''Tags''' - A list of the content's tags separated by comma. (added in 2.0.130.1)
+
: '''Tags''' - A list of the content's tags separated by comma.
: '''FileTitle''' - The content's name. (added in 2.0.130.1)
+
: '''FileTitle''' - The content's name.
 +
 
 +
 
 +
; After Workshop Content Updated (available in 2.0.131.3 and greater)
 +
: Occurs after the content has been updated. '''If the game's workshop update script is not configured the uninstall/install scripts will be executed.'''
 +
: '''Available objects:'''
 +
: [[ThisServer]], [[ThisGame]], [[ThisUser]], [[ThisService]]
 +
: '''Available Variables:'''
 +
: '''FileId''' - The id of the Workshop content that was installed.
 +
: '''FileIds''' - A list of currently installed file ids formatted and separated according to the game's configuration. It includes the file id that is being installed.
 +
: '''FileIdsArray''' - An uint32 array of currently installed file ids. It includes the file id that is being installed.
 +
: '''InstallPath''' - The folder where the content is downloaded. For example: ServiceRoot/steamapps/Workshop/content/STOREID/FILEID
 +
: '''FileUrl''' - Url to download the file. If the content has more than 1 file this value is blank.
 +
: '''FileName''' - The filename according to the Steam API. The value is relative to the install path. If the content has more than 1 file this value is blank. For example: mymaps/aim_ak47_training_csgo.bsp
 +
: '''FileNameNoPath''' - The filename without any paths. If the content has more than 1 file this value is blank. For example: aim_ak47_training_csgo.bsp
 +
: '''FileNameSavePath''' - The full path where the single file is downloaded. For example ServiceRoot/steamapps/Workshop/content/STOREID/FILEID/mymaps/aim_ak47_training_csgo.bsp
 +
: '''TagsArray''' - A string array that contains the content's tags.
 +
: '''Tags''' - A list of the content's tags separated by comma.
 +
: '''FileTitle''' - The content's name.
 +
 
 +
; Before Workshop Automatic Update (available in 2.0.144 and greater)
 +
: This event is executed when the service has a scheduled task for Workshop Update. Use it to send an update notification to the connected players. See the Ark scripts for an example.
 +
: '''Available objects:'''
 +
: [[ThisServer]], [[ThisGame]], [[ThisUser]], [[ThisService]]
  
 
== Sample Scripts ==
 
== Sample Scripts ==
Line 55: Line 80:
  
 
=== Ark ===
 
=== Ark ===
These scripts require Python 2.7 installed in the default location. Update the paths as needed.
+
* Updated 2020/08/24. Fix for file ids > 2147483647 after this was fixed in the game
 +
* Updated 2020/07/07. Fix for file ids > 2147483647
 +
 
 +
==== After Workshop Install ====
 +
''' <span style="color:red">The parallel code in the after installed script bellow causes Mono to crash. On Linux use this script instead:</span> [[Ark Workshop Scripts for Linux]] '''
 +
'''Operating System:''' Windows
 +
'''Description:''' Extract .z, copy to ShooterGame/Content/Mods and update GameUserSettings.ini
 +
'''Script Engine:''' IronPython
 +
'''Event:''' After Workshop Content Installed
 +
'''Ignore execution errors''' Unchecked
 +
'''Notes''' This script supports extraction of multiple files simultaneously. The default is 2. You can increase it by editing this line near the bottom of the script.
 +
'''options.MaxDegreeOfParallelism = 2'''
 +
<span style="color:red">For VPS the recommended value is 2. For dedicated servers the recommended value is 4. The higher the value the faster the script will execute but it will require more CPU and disk. Set it too high and it might take even longer.</span>
 +
 
 +
<source lang="python">import clr
 +
 
 +
 
 +
from System.IO import Directory, File, Path, SearchOption
 +
from System import Environment, PlatformID, String, Exception
 +
from System.Text.RegularExpressions import Regex, RegexOptions, Match
 +
 
 +
 
 +
 
 +
#Support for parallel extraction
 +
clr.AddReference('System.Core')
 +
from System.Collections.Generic import List
 +
from System import Action
 +
from System.Threading.Tasks import Parallel, ParallelOptions
 +
 
 +
extractedcount=0
 +
totalfilecount=0
 +
lastfileprogress=0
 +
########################################
 +
# https://github.com/TheCherry/ark-server-manager #
 +
########################################
 +
import struct
 +
import zlib
 +
import sys
 +
 
 +
def str_to_l(st):
 +
    return struct.unpack('q', st)[0]
 +
 
 +
def z_unpack(src, dst):
 +
    global extractedcount, totalfilecount, lastfileprogress
 +
    with open(src, 'rb') as f_src:
 +
        with open(dst, 'wb') as f_dst:
 +
            f_src.read(8)
 +
            size1 = str_to_l(f_src.read(8))
 +
            f_src.read(8)
 +
            size2 = str_to_l(f_src.read(8))
 +
            if(size1 == -1641380927):
 +
                size1 = 131072L
 +
            runs = (size2 + size1 - 1L) / size1
 +
            array = []
 +
            for i in range(runs):
 +
                array.append(f_src.read(8))
 +
                f_src.read(8)
 +
            for i in range(runs):
 +
                to_read = array[i]
 +
                compressed = f_src.read(str_to_l(to_read))
 +
                decompressed = zlib.decompress(compressed)
 +
                f_dst.write(decompressed)
 +
    Script.WriteToConsole("Extracted " + dst.Replace(ThisService.RootDirectory, ""))
 +
    File.Delete(src)
 +
    File.Delete(src + ".uncompressed_size")
 +
    extractedcount=extractedcount+1
 +
    progress=round((float(extractedcount)/totalfilecount)*100,0)
 +
    if progress > lastfileprogress + 4:
 +
      lastfileprogress=progress
 +
      ThisTaskStep.UpdateProgress(progress)
 +
   
 +
#######################################################################
 +
# https://github.com/barrycarey/Ark_Mod_Downloader/blob/master/Ark_Mod_Downloader.py #
 +
#######################################################################
 +
import os
 +
import struct
 +
from collections import OrderedDict
 +
map_names = []
 +
map_count=0
 +
temp_mod_path = os.path.join(ThisService.RootDirectory, "ShooterGame/Content/Mods")
 +
meta_data = OrderedDict([])
 +
 
 +
def parse_base_info(modid):
 +
        Script.WriteToConsole("[+] Collecting Mod Details From mod.info")
 +
 
 +
        mod_info = os.path.join(temp_mod_path, modid, "mod.info")
 +
 
 +
        if not os.path.isfile(mod_info):
 +
            raise Exception("[x] Failed to locate mod.info. Cannot Continue. Please try again.")
 +
            return False
 +
 
 +
        with open(mod_info, "rb") as f:
 +
            read_ue4_string(f)
 +
            map_count = struct.unpack('i', f.read(4))[0]
 +
 
 +
            for i in range(map_count):
 +
                cur_map = read_ue4_string(f)
 +
                if cur_map:
 +
                    map_names.append(cur_map)
 +
 
 +
        return True
 +
 
 +
def parse_meta_data(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(temp_mod_path, modid, "modmeta.info")
 +
        if not os.path.isfile(mod_meta):
 +
            raise Exception("[x] Failed To Locate modmeta.info. Cannot continue without it. Please try again.")
 +
            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:
 +
                    Script.WriteToConsole("[!] " + key + ":" + value)
 +
                    meta_data[key] = value
 +
 
 +
        return True
 +
 
 +
def create_mod_file(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 parse_base_info(modid) or not parse_meta_data(modid):
 +
            return False
 +
 
 +
        print("[+] Writing .mod File")
 +
        with open(os.path.join(temp_mod_path, modid + ".mod"), "w+b") as f:
 +
 
 +
            modid = int(modid)
 +
            if modid > 2147483647:
 +
              diff = modid-2147483647
 +
              modid = -2147483647 + diff - 2  
 +
            f.write(struct.pack('ixxxx', modid))  # Needs 4 pad bits
 +
            write_ue4_string("ModName", f)
 +
            write_ue4_string("", f)
 +
 
 +
            map_count = len(map_names)
 +
            f.write(struct.pack("i", map_count))
 +
 
 +
            for m in map_names:
 +
                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 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(meta_data)
 +
            f.write(struct.pack('i', meta_length))
 +
 
 +
            for k, v in meta_data.items():
 +
                write_ue4_string(k, f)
 +
                write_ue4_string(v, f)
 +
 
 +
        return True
 +
 
 +
def read_ue4_string(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(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'))
 +
 
 +
###########################################
 +
###########################################
 +
###########################################
 +
 
 +
# Only extract files the correct folder depending on operating system
 +
oseditor="WindowsNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxNoEditor"
 +
noeditor=Path.Combine(InstallPath, oseditor )
 +
 
 +
# Use other OS folder if it doesn't exist.
 +
if not Directory.Exists(noeditor) :
 +
  oseditor = "LinuxNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "WindowsNoEditor"
 +
  noeditor = Path.Combine(InstallPath, oseditor) 
 +
 
 +
# Extract and delete all .z files
 +
actions = List[Action]()
 +
for zfile in Directory.GetFiles(noeditor, "*.z", SearchOption.AllDirectories):
 +
file=Path.Combine(Path.GetDirectoryName(zfile), Path.GetFileNameWithoutExtension(zfile))
 +
action=Action(lambda a=zfile, b=file: z_unpack(a, b))
 +
actions.Add(action)
 +
 
 +
options=ParallelOptions()
 +
#Extract 2 files at a time.
 +
options.MaxDegreeOfParallelism = 2
 +
totalfilecount=actions.Count
 +
ThisTaskStep.WriteLog(String.Format("Extracting {0} files...", totalfilecount))
 +
ThisTaskStep.UpdateProgress(0)
 +
Parallel.Invoke(options, actions.ToArray())
 +
 
 +
# Move folder to correct location. Delete if it already exists.
 +
# Define modid before FileId is altered so we write the correct id to inifile
 +
modid = FileId
 +
if FileId > 2147483647:
 +
  diff = FileId-2147483647
 +
  FileId = -2147483647 + diff - 2
 +
 
 +
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", modid))
 +
if Directory.Exists(modfolder) :
 +
  Directory.Delete(modfolder, True)
 +
Directory.Move(Path.Combine(InstallPath, oseditor), modfolder)
 +
 
 +
# Update ini file
 +
serveros = "WindowsServer" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxServer"
 +
inifile = Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Saved/Config/{0}/GameUserSettings.ini", serveros))
 +
pattern="ActiveMods[ \t]*=[ \t]*(?<ActiveMods>[0-9, \t]*)"
 +
filecontents = File.ReadAllText(inifile)
 +
match = Regex.Match(filecontents, pattern, RegexOptions.IgnoreCase)
 +
if match.Success :
 +
  activemods = match.Groups["ActiveMods"].Value
 +
  if String.IsNullOrEmpty(activemods) or activemods.IndexOf(modid.ToString()) == -1 :
 +
    if activemods.Length > 0 :
 +
      activemods = activemods + ","
 +
      activemods = activemods + modid.ToString()
 +
      filecontents=filecontents.Replace(match.Groups["ActiveMods"].Value, activemods)
 +
    else :
 +
      activemods = modid.ToString()
 +
      filecontents = filecontents.Substring(0, match.Groups["ActiveMods"].Index) + activemods + filecontents.Substring(match.Groups["ActiveMods"].Index)
 +
    File.WriteAllText(inifile, filecontents)
 +
 
 +
#Create .mod
 +
parse_base_info(modid.ToString())
 +
parse_meta_data(modid.ToString())
 +
create_mod_file(modid.ToString())
 +
 
 +
# Delete folder
 +
if Directory.Exists(InstallPath) :
 +
  Directory.Delete(InstallPath, True)</source>
 +
 
 +
==== After Workshop Uninstall ====
  
 
  '''Operating System:''' Any
 
  '''Operating System:''' Any
  '''Description:''' Extract .z, copy to ShooterGame/Content/Mods and update GameUserSettings.ini
+
  '''Description:''' Delete mod from ShooterGame/Content/Mods and update GameUserSettings.ini
 +
'''Script Engine:''' IronPython
 +
'''Event:''' After Workshop Content Uninstalled
 +
'''Ignore execution errors''' Unchecked
 +
 
 +
<source lang="python">import clr
 +
 
 +
from System.IO import Path, Directory, File
 +
from System import Environment, PlatformID, String
 +
from System.Text.RegularExpressions import Regex, RegexOptions, Match
 +
 
 +
modid = FileId
 +
if FileId > 2147483647:
 +
  diff = FileId-2147483647
 +
  FileId = -2147483647 + diff - 2
 +
 
 +
# Delete folder
 +
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", modid))
 +
if Directory.Exists(modfolder) :
 +
  Directory.Delete(modfolder, True)
 +
 
 +
#Delete .mod
 +
modfile=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}.mod", modid))
 +
if File.Exists(modfile) :
 +
  File.Delete(modfile)
 +
 
 +
# Update ini file
 +
serverfolder = "WindowsServer" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxServer"
 +
inifile = Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Saved/Config/{0}/GameUserSettings.ini", serverfolder))
 +
pattern="ActiveMods[ \t]*=[ \t]*(?<ActiveMods>[0-9, \t]*)"
 +
filecontents = File.ReadAllText(inifile)
 +
match = Regex.Match(filecontents, pattern, RegexOptions.IgnoreCase)
 +
if match.Success :
 +
  activemods = match.Groups["ActiveMods"].Value
 +
  if activemods.IndexOf(modid.ToString()) != -1 :
 +
    activemods = activemods.Replace("," + modid.ToString(), String.Empty).Replace(modid.ToString() + ",", String.Empty).Replace(modid.ToString(), String.Empty)
 +
    filecontents=filecontents.Replace(match.Groups["ActiveMods"].Value, activemods)
 +
    File.WriteAllText(inifile, filecontents)</source>
 +
 
 +
==== After Workshop Update ====
 +
''' <span style="color:red">The parallel code in the after updated script bellow causes Mono to crash. On Linux use this script instead:</span> [[Ark Workshop Scripts for Linux]] '''
 +
 
 +
This script is the same as the install script except it does not update the .ini so it keeps the mod order.
 +
 
 +
<source lang="python">import clr
 +
 
 +
 
 +
from System.IO import Directory, File, Path, SearchOption
 +
from System import Environment, PlatformID, String, Exception
 +
from System.Text.RegularExpressions import Regex, RegexOptions, Match
 +
 
 +
 
 +
 
 +
#Support for parallel extraction
 +
clr.AddReference('System.Core')
 +
from System.Collections.Generic import List
 +
from System import Action
 +
from System.Threading.Tasks import Parallel, ParallelOptions
 +
 
 +
extractedcount=0
 +
totalfilecount=0
 +
lastfileprogress=0
 +
########################################
 +
# https://github.com/TheCherry/ark-server-manager #
 +
########################################
 +
import struct
 +
import zlib
 +
import sys
 +
 
 +
def str_to_l(st):
 +
    return struct.unpack('q', st)[0]
 +
 
 +
def z_unpack(src, dst):
 +
    global extractedcount, totalfilecount, lastfileprogress
 +
    with open(src, 'rb') as f_src:
 +
        with open(dst, 'wb') as f_dst:
 +
            f_src.read(8)
 +
            size1 = str_to_l(f_src.read(8))
 +
            f_src.read(8)
 +
            size2 = str_to_l(f_src.read(8))
 +
            if(size1 == -1641380927):
 +
                size1 = 131072L
 +
            runs = (size2 + size1 - 1L) / size1
 +
            array = []
 +
            for i in range(runs):
 +
                array.append(f_src.read(8))
 +
                f_src.read(8)
 +
            for i in range(runs):
 +
                to_read = array[i]
 +
                compressed = f_src.read(str_to_l(to_read))
 +
                decompressed = zlib.decompress(compressed)
 +
                f_dst.write(decompressed)
 +
    Script.WriteToConsole("Extracted " + dst.Replace(ThisService.RootDirectory, ""))
 +
    File.Delete(src)
 +
    File.Delete(src + ".uncompressed_size")
 +
    extractedcount=extractedcount+1
 +
    progress=round((float(extractedcount)/totalfilecount)*100,0)
 +
    if progress > lastfileprogress + 4:
 +
      lastfileprogress=progress
 +
      ThisTaskStep.UpdateProgress(progress)
 +
   
 +
#######################################################################
 +
# https://github.com/barrycarey/Ark_Mod_Downloader/blob/master/Ark_Mod_Downloader.py #
 +
#######################################################################
 +
import os
 +
import struct
 +
from collections import OrderedDict
 +
map_names = []
 +
map_count=0
 +
temp_mod_path = os.path.join(ThisService.RootDirectory, "ShooterGame/Content/Mods")
 +
meta_data = OrderedDict([])
 +
 
 +
def parse_base_info(modid):
 +
        Script.WriteToConsole("[+] Collecting Mod Details From mod.info")
 +
 
 +
        mod_info = os.path.join(temp_mod_path, modid, "mod.info")
 +
 
 +
        if not os.path.isfile(mod_info):
 +
            raise Exception("[x] Failed to locate mod.info. Cannot Continue. Please try again.")
 +
            return False
 +
 
 +
        with open(mod_info, "rb") as f:
 +
            read_ue4_string(f)
 +
            map_count = struct.unpack('i', f.read(4))[0]
 +
 
 +
            for i in range(map_count):
 +
                cur_map = read_ue4_string(f)
 +
                if cur_map:
 +
                    map_names.append(cur_map)
 +
 
 +
        return True
 +
 
 +
def parse_meta_data(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(temp_mod_path, modid, "modmeta.info")
 +
        if not os.path.isfile(mod_meta):
 +
            raise Exception("[x] Failed To Locate modmeta.info. Cannot continue without it. Please try again.")
 +
            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:
 +
                    Script.WriteToConsole("[!] " + key + ":" + value)
 +
                    meta_data[key] = value
 +
 
 +
        return True
 +
 
 +
def create_mod_file(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 parse_base_info(modid) or not parse_meta_data(modid):
 +
            return False
 +
 
 +
        print("[+] Writing .mod File")
 +
        with open(os.path.join(temp_mod_path, modid + ".mod"), "w+b") as f:
 +
 
 +
            modid = int(modid)
 +
            if modid > 2147483647:
 +
              diff = modid-2147483647
 +
              modid = -2147483647 + diff - 2  
 +
            f.write(struct.pack('ixxxx', modid))  # Needs 4 pad bits
 +
            write_ue4_string("ModName", f)
 +
            write_ue4_string("", f)
 +
 
 +
            map_count = len(map_names)
 +
            f.write(struct.pack("i", map_count))
 +
 
 +
            for m in map_names:
 +
                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 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(meta_data)
 +
            f.write(struct.pack('i', meta_length))
 +
 
 +
            for k, v in meta_data.items():
 +
                write_ue4_string(k, f)
 +
                write_ue4_string(v, f)
 +
 
 +
        return True
 +
 
 +
def read_ue4_string(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(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'))
 +
 
 +
###########################################
 +
###########################################
 +
###########################################
 +
 
 +
# Only extract files the correct folder depending on operating system
 +
oseditor="WindowsNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxNoEditor"
 +
noeditor=Path.Combine(InstallPath, oseditor )
 +
 
 +
# Use other OS folder if it doesn't exist.
 +
if not Directory.Exists(noeditor) :
 +
  oseditor = "LinuxNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "WindowsNoEditor"
 +
  noeditor = Path.Combine(InstallPath, oseditor) 
 +
 
 +
# Extract and delete all .z files
 +
actions = List[Action]()
 +
for zfile in Directory.GetFiles(noeditor, "*.z", SearchOption.AllDirectories):
 +
file=Path.Combine(Path.GetDirectoryName(zfile), Path.GetFileNameWithoutExtension(zfile))
 +
action=Action(lambda a=zfile, b=file: z_unpack(a, b))
 +
actions.Add(action)
 +
 
 +
options=ParallelOptions()
 +
#Extract 2 files at a time.
 +
options.MaxDegreeOfParallelism = 2
 +
totalfilecount=actions.Count
 +
ThisTaskStep.WriteLog(String.Format("Extracting {0} files...", totalfilecount))
 +
ThisTaskStep.UpdateProgress(0)
 +
Parallel.Invoke(options, actions.ToArray())
 +
 
 +
# Move folder to correct location. Delete if it already exists.
 +
# Define modid before FileId is altered so we write the correct id to inifile
 +
modid = FileId
 +
if FileId > 2147483647:
 +
  diff = FileId-2147483647
 +
  FileId = -2147483647 + diff - 2
 +
 
 +
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", modid))
 +
if Directory.Exists(modfolder) :
 +
  Directory.Delete(modfolder, True)
 +
Directory.Move(Path.Combine(InstallPath, oseditor), modfolder)
 +
 
 +
#Create .mod
 +
parse_base_info(modid.ToString())
 +
parse_meta_data(modid.ToString())
 +
create_mod_file(modid.ToString())
 +
 
 +
# Delete folder
 +
if Directory.Exists(InstallPath) :
 +
  Directory.Delete(InstallPath, True)</source>
 +
 
 +
==== Before Workshop Automatic Update ====
 +
This script sends a message to players 5 minutes before doing an automatic update.
 +
 
 +
'''Operating System:''' Any
 +
'''Description:''' Broadcast a message, wait 5 minutes, save world
 +
'''Script Engine:''' IronPython
 +
'''Event:''' Before Workshop Automatic Update
 +
'''Ignore execution errors''' Checked
 +
 
 +
<source lang="python">import clr
 +
clr.AddReference("TCAdmin.GameHosting.SDK")
 +
clr.AddReference("TCAdmin.Interfaces")
 +
 
 +
from System.IO import File, Path
 +
from System.Text.RegularExpressions import Regex, RegexOptions
 +
from System import Environment, PlatformID, String
 +
from System.Threading import Thread
 +
from TCAdmin.GameHosting.SDK import rconClient
 +
from TCAdmin.GameHosting.SDK import RCONGameType
 +
from TCAdmin.Interfaces.Server import ServiceStatus
 +
 
 +
if ThisService.Status.ServiceStatus != ServiceStatus.Running :
 +
  Script.WriteToConsole(String.Format("{0} is stopped. The script will not continue.", ThisService.ConnectionInfo))
 +
  Script.Exit()
 +
 
 +
serveros = "WindowsServer" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxServer"
 +
inifile = Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Saved/Config/{0}/GameUserSettings.ini", serveros))
 +
 
 +
pattern="ServerAdminPassword[ \t]*=[ \t]*[\"]?(?<ServerAdminPassword>([^\"\r\n])*)[\"]?"
 +
filecontents = File.ReadAllText(inifile)
 +
match = Regex.Match(filecontents, pattern, RegexOptions.IgnoreCase)
 +
 
 +
if match.Success :
 +
  rconpass = match.Groups["ServerAdminPassword"].Value
 +
  Script.WriteToConsole(String.Format("RCon password is: {0}", rconpass))
 +
 
 +
  rconclient=rconClient()
 +
  rconclient.GameType = RCONGameType.CounterStrikeSource
 +
  rconclient.Server = ThisService.IpAddress
 +
  rconclient.Port = ThisService.RConPort
 +
  Script.WriteToConsole("Sending update notification...")
 +
  rconclient.Send(None, None, rconpass, "broadcast \"Server will update mods in 5 minutes!\"")
 +
  Script.WriteToConsole(String.Format("RCon Response: {0}", rconclient.ReadResponse()))
 +
  Script.WriteToConsole("Waiting for 5 minutes...")
 +
  Thread.Sleep(300000)
 +
  Script.WriteToConsole("Saving world...")
 +
  rconclient.Send(None, None, rconpass, "saveworld")
 +
  Script.WriteToConsole(String.Format("RCon Response: {0}", rconclient.ReadResponse()))
 +
  Script.WriteToConsole("Done.")
 +
  </source>
 +
 
 +
=== Arma 3 ===
 +
* Go to the game's settings. Add a variable named '''Mods''' and another named '''ServerMods'''.
 +
* Go to the game's command lines. Add the variables to the game's command line: '''-mod=![Mods] -servermod=![ServerMods]'''
 +
* Go to the game's steam settings. Set Workshop File Id Format to '''![FileId]''' and Workshop File Id Separator to ''';'''
 +
 
 +
'''Operating System:''' Any
 +
'''Description:''' Configure mod
 
  '''Script Engine:''' IronPython
 
  '''Script Engine:''' IronPython
 
  '''Event:''' After Workshop Content Installed
 
  '''Event:''' After Workshop Content Installed
 
  '''Ignore execution errors''' Unchecked
 
  '''Ignore execution errors''' Unchecked
  
 +
<source lang="python">
 
  import clr
 
  import clr
  clr.AddReference("INIFileParser")
+
  from System import Array, String
 +
from System.IO import File, Path, Directory, SearchOption
 +
 
 +
servertag="Server"
 +
servermods=""
 +
mods=""
 
   
 
   
  from System.IO import Directory, File, Path, SearchOption
+
  if ThisService.Variables.HasValue("ServerMods") :
from System import Environment, PlatformID, String
+
  servermods=ThisService.Variables["ServerMods"]
from IniParser import FileIniDataParser
 
from IniParser.Model import IniData
 
 
   
 
   
  import sys
+
  if ThisService.Variables.HasValue("Mods") :
  if Environment.OSVersion.Platform == PlatformID.Win32NT :
+
  mods=ThisService.Variables["Mods"]
  sys.path.append("C:\\Python27\\Lib")
+
 
 +
  if Array.IndexOf(TagsArray, servertag) == -1 :
 +
  ThisService.Variables["Mods"]=String.Format("@{0}", FileId) + ";" + mods   
 
  else :
 
  else :
  sys.path.append("/usr/lib/python2.7")   
+
  ThisService.Variables["ServerMods"]=String.Format("@{0}", FileId) + ";" + servermods  
 
   
 
   
 +
# Move folder to correct location
 +
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", FileId))
 +
if Directory.Exists(modfolder) :
 +
  Directory.Delete(modfolder, True)
 +
Directory.Move(InstallPath, modfolder)
 
   
 
   
  ########################################
+
  # Move keys to root key folder
  # https://github.com/TheCherry/ark-server-manager #
+
  modkeys=Path.Combine(modfolder, "keys")
  ########################################
+
  rootkeys=Path.Combine(ThisService.RootDirectory, "keys")
  import struct
+
  Directory.CreateDirectory(rootkeys)
  import zlib
+
  if Directory.Exists(modkeys) :
import sys
+
  for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
 +
    keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
 +
    if File.Exists(keyfile) :
 +
      File.Delete(keyfile)
 +
    File.Move(file, keyfile)
 
   
 
   
  def str_to_l(st):
+
  # Move key to root key folder
     return struct.unpack('q', st)[0]
+
modkeys=Path.Combine(modfolder, "key")
 +
if Directory.Exists(modkeys) :
 +
  for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
 +
    keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
 +
    if File.Exists(keyfile) :
 +
      File.Delete(keyfile)
 +
     File.Move(file, keyfile)
 
   
 
   
  def z_unpack(src, dst):
+
  # Update command line
    with open(src, 'rb') as f_src:
+
ThisService.Save()
        with open(dst, 'wb') as f_dst:
+
ThisService.Configure()
            f_src.read(8)
+
</source>
            size1 = str_to_l(f_src.read(8))
+
 
            f_src.read(8)
+
'''Operating System:''' Any
            size2 = str_to_l(f_src.read(8))
+
'''Description:''' Configure mod
            if(size1 == -1641380927):
+
'''Script Engine:''' IronPython
                size1 = 131072L
+
'''Event:''' After Workshop Content Uninstalled
            runs = (size2 + size1 - 1L) / size1
+
'''Ignore execution errors''' Unchecked
            array = []
+
 
            for i in range(runs):
+
<source lang="python">
                array.append(f_src.read(8))
+
import clr
                f_src.read(8)
+
from System import Array, String
            for i in range(runs):
+
from System.IO import Path, Directory
                to_read = array[i]
 
                compressed = f_src.read(str_to_l(to_read))
 
                decompressed = zlib.decompress(compressed)
 
                f_dst.write(decompressed)               
 
 
   
 
   
  #######################################################################
+
  servermods=""
# https://github.com/barrycarey/Ark_Mod_Downloader/blob/master/Ark_Mod_Downloader.py #
+
  mods=""
#######################################################################
 
import os
 
import struct
 
from collections import OrderedDict
 
map_names = []
 
map_count=0
 
  temp_mod_path = os.path.join(ThisService.RootDirectory, "ShooterGame/Content/Mods")
 
meta_data = OrderedDict([])
 
 
   
 
   
  def parse_base_info(modid):
+
  if ThisService.Variables.HasValue("ServerMods") :
 +
  servermods=ThisService.Variables["ServerMods"]
 
   
 
   
        Script.WriteToConsole("[+] Collecting Mod Details From mod.info")
+
if ThisService.Variables.HasValue("Mods") :
 +
  mods=ThisService.Variables["Mods"]
 +
 
 +
ThisService.Variables["ServerMods"]=servermods.Replace(String.Format("@{0};", FileId), "")
 +
ThisService.Variables["Mods"]=mods.Replace(String.Format("@{0};", FileId), "")
 
   
 
   
        mod_info = os.path.join(temp_mod_path, modid, "mod.info")
+
# Delete mod folder
 +
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", FileId))
 +
if Directory.Exists(modfolder) :
 +
  Directory.Delete(modfolder, True)
 
   
 
   
        if not os.path.isfile(mod_info):
+
# Update command line
            Script.WriteToConsole("[x] Failed to locate mod.info. Cannot ContinueAborting")
+
ThisService.Save()
            return False
+
ThisService.Configure()
 +
</source>
 +
 
 +
'''Operating System:''' Any
 +
'''Description:''' Clear workshop mod variables
 +
'''Script Engine:''' IronPython
 +
'''Event:''' After Reinstalled
 +
'''Ignore execution errors''' Unchecked
 +
 
 +
<source lang="python">
 +
ThisService.Variables["ServerMods"]=""
 +
ThisService.Variables["Mods"]=""
 +
ThisService.Save()
 +
</source>
 +
 
 +
=== America's Army Proving Grounds ===
 +
'''Operating System:''' Any
 +
'''Description:''' Updates [SteamUGCManager.SteamUGCManager] in AASteamUGCManager.ini
 +
'''Script Engine:''' IronPython
 +
  '''Events:''' After Workshop Content Installed, After Workshop Content Uninstalled
 +
'''Ignore execution errors''' Unchecked
 +
 
 +
<source lang="python">
 +
import clr
 +
clr.AddReference("INIFileParser")
 +
 
 +
from IniParser.Model.Configuration import IniParserConfiguration
 +
from IniParser.Parser import IniDataParser
 +
from IniParser import FileIniDataParser
 +
from System.IO import Path, File
 +
from System import String
 
   
 
   
        with open(mod_info, "rb") as f:
+
#Remove all keys from [SteamUGCManager.SteamUGCManager]
            read_ue4_string(f)
+
inifile = Path.Combine(ThisService.RootDirectory, String.Format("AAGame/Config/ServerConfig/AASteamUGCManager.ini"))
            map_count = struct.unpack('i', f.read(4))[0]
+
iniconfig = IniParserConfiguration()
 +
iniconfig.AllowDuplicateKeys = True
 +
dataparser = IniDataParser(iniconfig)
 +
ini = FileIniDataParser(dataparser)
 +
data = ini.ReadFile(inifile)
 +
data["SteamUGCManager.SteamUGCManager"].RemoveAllKeys()
 +
ini.WriteFile(inifile, data)
 
   
 
   
            for i in range(map_count):
+
#Add mods under [SteamUGCManager.SteamUGCManager]
                cur_map = read_ue4_string(f)
+
i=0
                if cur_map:
+
items=""
                    map_names.append(cur_map)
+
while i < len(FileIdsArray):
 +
  items = items + String.Format("ServerSubscribedItems=(IdString={0})\n",FileIdsArray[i])
 +
  i += 1
 
   
 
   
        return True
+
contents=File.ReadAllText(inifile)
 +
contents=contents.Replace("[SteamUGCManager.SteamUGCManager]", "[SteamUGCManager.SteamUGCManager]\n" + items)
 +
File.WriteAllText(inifile, contents)
 +
</source>
 +
 
 +
=== Broke Protocol: Online City RPG  ===
 +
'''Operating System:''' Windows
 +
'''Description:''' Moves mod files.
 +
'''Script Engine:''' Batch
 +
'''Event:''' After Workshop Content Installed
 +
'''Ignore execution errors''' Unchecked
 +
 
 +
<source lang="batch">
 +
move /y %InstallPath% %ThisService_RootDirectory%\AssetBundles\%FileId%
 +
</source>
 +
 
 +
'''Operating System:''' Windows
 +
'''Description:''' Delete mod files.
 +
'''Script Engine:''' Batch
 +
'''Event:''' After Workshop Content Uninstalled
 +
'''Ignore execution errors''' Unchecked
 +
 
 +
<source lang="batch">
 +
rd /q /s %ThisService_RootDirectory%\AssetBundles\%FileId%
 +
</source>
 +
 
 +
=== Conan Exiles ===
 +
'''Operating System:''' Any
 +
'''Description:''' Moves mod files.
 +
'''Script Engine:''' IronPython
 +
'''Event:''' After Workshop Content Installed
 +
'''Ignore execution errors''' Unchecked
 +
These scripts have been updated to keep the order of modlist.txt
 +
 
 +
<source lang="python>
 +
import clr
 +
from System import String
 +
from System.IO import Directory, File, Path, SearchOption, DirectoryInfo
 
   
 
   
  def parse_meta_data(modid):
+
  import System
        """
+
clr.AddReference("System.Core")
        Parse the modmeta.info files and extract the key value pairs need to for the .mod file.
+
clr.ImportExtensions(System.Linq)
        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")
+
modpath = Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods")
        print("[+] Located The Following Meta Data:")
+
Directory.CreateDirectory(modpath)
 
   
 
   
        mod_meta = os.path.join(temp_mod_path, modid, "modmeta.info")
+
#Save a list of the mod's files so we can delete them when uninstalling the mod.
        if not os.path.isfile(mod_meta):
+
modpakfile = "";
            Script.WriteToConsole("[x] Failed To Locate modmeta.info. Cannot continue without it. Aborting")
+
modfilelist=Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods", String.Format("{0}.txt", FileId))
            return False
+
for file in Directory.GetFiles(InstallPath, "*", SearchOption.AllDirectories):
 +
  modfile = Path.Combine(modpath, Path.GetFileName(file))
 +
  if(File.Exists(modfile)) :
 +
    File.Delete(modfile)
 +
  File.Move(file, modfile)
 +
  modfilename=Path.GetFileName(file)
 +
  File.AppendAllText(modfilelist, String.Format("{0}\r", modfilename))
 +
  if Path.GetExtension(modfilename) == ".pak" :
 +
    modpakfile = modfilename
 
   
 
   
        with open(mod_meta, "rb") as f:
+
#Create modlist.txt
 +
modlisttxt=Path.Combine(modpath, "modlist.txt")
 +
if File.Exists(modlisttxt) :
 +
  lines = File.ReadAllLines(modlisttxt)
 +
  File.Delete(modlisttxt)
 +
  with File.AppendText(modlisttxt) as fs :
 +
    for line in lines :
 +
      fs.WriteLine(line)
 +
   
 +
with File.AppendText(modlisttxt) as fs :
 +
  fs.WriteLine(modpakfile)
 +
</source>
 +
 
 +
'''Operating System:''' Any
 +
'''Description:''' Delete mod files
 +
'''Script Engine:''' IronPython
 +
'''Event:''' After Workshop Content Uninstalled
 +
'''Ignore execution errors''' Unchecked
 +
 
 +
<source lang="python>
 +
import clr
 +
from System import String
 +
from System.IO import Directory, File, Path, SearchOption, DirectoryInfo
 
   
 
   
            total_pairs = struct.unpack('i', f.read(4))[0]
+
import System
 +
clr.AddReference("System.Core")
 +
clr.ImportExtensions(System.Linq)
 
   
 
   
            for i in range(total_pairs):
+
modpath = Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods")
 +
modfilelist=Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods", String.Format("{0}.txt", FileId))
 
   
 
   
                key, value = "", ""
+
modpakfile = "";
 +
if File.Exists(modfilelist) :
 +
  modfiles = File.ReadAllLines(modfilelist)
 +
  for modfile in modfiles :
 +
    if Path.GetExtension(modfile) == ".pak" :
 +
      modpakfile = modfile
 +
    modfile = Path.Combine(modpath, modfile)
 +
    if File.Exists(modfile) :
 +
      File.Delete(modfile)
 +
  File.Delete(modfilelist)
 +
 
 +
#Create modlist.txt
 +
modlisttxt=Path.Combine(modpath, "modlist.txt")
 +
if(File.Exists(modlisttxt)) :
 +
  lines = File.ReadAllLines(modlisttxt).Where(lambda l: not l.Contains(modpakfile)).ToArray()
 +
  File.Delete(modlisttxt)
 +
  with File.AppendText(modlisttxt) as fs :
 +
    for line in lines :
 +
      fs.WriteLine(line)
 +
</source>
 +
 
 +
'''Operating System:''' Any
 +
'''Description:''' Moves mod files.
 +
'''Script Engine:''' IronPython
 +
'''Event:''' After Workshop Content Updated
 +
'''Ignore execution errors''' Unchecked
 +
This is basically the install script but with the part that updates modlist.txt removed so we don't lose the mod order.
 +
 
 +
<source lang="python>
 +
import clr
 +
from System import String
 +
from System.IO import Directory, File, Path, SearchOption, DirectoryInfo
 
   
 
   
                key_bytes = struct.unpack('i', f.read(4))[0]
+
import System
                key_flag = False
+
clr.AddReference("System.Core")
                if key_bytes < 0:
+
clr.ImportExtensions(System.Linq)
                    key_flag = True
 
                    key_bytes -= 1
 
 
   
 
   
                if not key_flag and key_bytes > 0:
+
modpath = Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods")
 +
Directory.CreateDirectory(modpath)
 
   
 
   
                    raw = f.read(key_bytes)
+
#Save a list of the mod's files so we can delete them when uninstalling the mod.
                    key = raw[:-1].decode()
+
modpakfile = "";
 +
modfilelist=Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods", String.Format("{0}.txt", FileId))
 +
for file in Directory.GetFiles(InstallPath, "*", SearchOption.AllDirectories):
 +
  modfile = Path.Combine(modpath, Path.GetFileName(file))
 +
  if(File.Exists(modfile)) :
 +
    File.Delete(modfile)
 +
  File.Move(file, modfile)
 +
  modfilename=Path.GetFileName(file)
 +
  File.AppendAllText(modfilelist, String.Format("{0}\r", modfilename))
 +
  if Path.GetExtension(modfilename) == ".pak" :
 +
    modpakfile = modfilename
 +
</source>
 +
 
 +
=== DayZ ===
 +
'''Scripts thanks to [https://TRUgaming.com TRUgaming]'''
 +
* Create a variable named Mods
 +
* Add this to the command line: -mod=![Mods]
 +
 
 +
'''Operating System:''' Any
 +
'''Description:''' After Workshop Content Installed
 +
'''Script Engine:''' IronPython
 +
'''Event:''' AfterWorkshopInstall
 +
'''Ignore execution errors:''' Unchecked
 +
 
 +
<source lang="python>
 +
import clr
 +
from System import Array, String
 +
from System.IO import File, Path, Directory, SearchOption
 +
from System.Threading import Thread
 +
import re
 
   
 
   
                value_bytes = struct.unpack('i', f.read(4))[0]
+
# Setup variables
                value_flag = False
+
servertag="Server"
                if value_bytes < 0:
+
mods=""
                    value_flag = True
+
y=[]
                    value_bytes -= 1
+
ModName=""
 
   
 
   
                if not value_flag and value_bytes > 0:
+
# Get commandline list of mods
                    raw = f.read(value_bytes)
+
if ThisService.Variables.HasValue("Mods") :
                    value = raw[:-1].decode()
+
  mods=ThisService.Variables["Mods"]
 +
else:
 +
  raise Exception("Missing Mod information. Installation Failed!")
 
   
 
   
                # TODO This is a potential issue if there is a key but no value
+
# Check for FileTitle
                if key and value:
+
if len(FileTitle) != 0:
                    Script.WriteToConsole("[!] " + key + ":" + value)
+
# Check FileTitle for special character
                    meta_data[key] = value
+
  for i in range(len(FileTitle)):
 +
    if bool(re.search('[a-zA-Z0-9 \]\[_\'+-]', FileTitle[i])) :
 +
      y.append(FileTitle[i])
 +
    else:
 +
      y.append("")
 +
       
 +
    ModName= "".join(str(x) for x in y)
 +
    ModName= ModName.Replace("  "," ").Replace(" ","_")
 +
    ModName= ModName.strip()
 +
else: 
 +
    ModName = FileId
 +
 
 +
if (mods.find(';') != -1) :
 +
    ThisService.Variables["Mods"]=String.Format("@{0}", ModName) + ";" + mods 
 +
else : 
 +
  if len(mods) > 0 :
 +
      ThisService.Variables["Mods"]=String.Format("@{0}", ModName) + ";" + mods 
 +
  else :
 +
      ThisService.Variables["Mods"]=String.Format("@{0}", ModName) + mods 
 +
     
 +
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", ModName))
 +
 
 +
# If mod exists delete the folder
 +
if Directory.Exists(modfolder) :
 +
    Directory.Delete(modfolder, True)
 
   
 
   
        return True
+
# Move mod to root using renamed file name as folder name
 +
Directory.Move(InstallPath, modfolder)
 
   
 
   
  def create_mod_file(modid):
+
  # Setup keys path to to copy .bikey file root \keys folder
        """
+
modkeys=Path.Combine(modfolder, "keys")
        Create the .mod file.
+
rootkeys=Path.Combine(ThisService.RootDirectory, "keys")
        This code is an adaptation of the code from Ark Server Launcher. All credit goes to Face Wound on Steam
 
        :return:
 
        """
 
        if not parse_base_info(modid) or not parse_meta_data(modid):
 
            return False
 
 
   
 
   
        print("[+] Writing .mod File")
+
# Check for \keys folder in root
        with open(os.path.join(temp_mod_path, modid + ".mod"), "w+b") as f:
+
if not Directory.Exists(rootkeys) :
 +
  Directory.CreateDirectory(rootkeys)
 
   
 
   
            modid = int(modid)
+
# Check for mod \keys folder
            f.write(struct.pack('ixxxx', modid)) # Needs 4 pad bits
+
# Get list of all files on mods \keys folder
            write_ue4_string("ModName", f)
+
# Copy file(s) from mods \keys folder to root \keys folder
            write_ue4_string("", f)
+
if Directory.Exists(modkeys) :
 +
  for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
 +
    keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
 +
    if not File.Exists(keyfile) :
 +
      File.Copy(file, keyfile)
 
   
 
   
            map_count = len(map_names)
+
# Update command line
            f.write(struct.pack("i", map_count))
+
ThisService.Save()
 +
ThisService.Configure()
 +
</source>
 +
 
 +
'''Operating System:''' Any
 +
'''Description:''' After Workshop Content Uninstalled
 +
'''Script Engine:''' IronPython
 +
'''Event:''' AfterWorkshopUninstall
 +
'''Ignore execution errors:''' Unchecked
 +
 
 +
<source lang="python>
 +
import clr
 +
from System import Array, String
 +
from System.IO import Directory, File, Path, SearchOption, DirectoryInfo
 +
import re
 
   
 
   
            for m in map_names:
+
# Setup variables
                write_ue4_string(m, f)
+
mods=""
 +
mlist=""
 +
lastmod=""
 +
ModName=""
 +
y=[]
 
   
 
   
            # Not sure of the reason for this
+
# Check for Variable
            num2 = 4280483635
+
# Get commandline mods list
            f.write(struct.pack('I', num2))
+
if ThisService.Variables.HasValue("Mods") :
            num3 = 2
+
  mods=ThisService.Variables["Mods"]
            f.write(struct.pack('i', num3))
+
else:
 +
  raise Exception("Missing Mod information. Uninstall Failed!")
 
   
 
   
            if "ModType" in meta_data:
+
# Check if the mod being uninstalled is the last mod or is the only mod in the commandline
                mod_type = b'1'
+
# Convert commandline modlist string to List
            else:
+
mlist = mods.split('@')
                mod_type = b'0'
 
 
   
 
   
            # TODO The packing on this char might need to be changed
+
def is_last(alist,choice):
            f.write(struct.pack('p', mod_type))
+
  if choice == alist[-1]:
            meta_length = len(meta_data)
+
    return True
            f.write(struct.pack('i', meta_length))
+
  else:
 +
    return False
 +
 
 +
if len(FileTitle) != 0:
 +
  for i in range(len(FileTitle)):
 +
    if bool(re.search('[a-zA-Z0-9 \]\[_\'+-]', FileTitle[i])) :
 +
      y.append(FileTitle[i])
 +
    else:
 +
      y.append("")
 +
       
 +
    ModName= "".join(str(x) for x in y)
 +
    ModName= ModName.Replace("  "," ").Replace(" ","_")
 +
    ModName= ModName.strip()
 +
else:
 +
  ModName = FileId
 
   
 
   
            for k, v in meta_data.items():
+
# If mod to be uninstalled is the last mod set flag to true
                write_ue4_string(k, f)
+
if is_last(mlist,ModName) == True :
                write_ue4_string(v, f)
+
    lastmod = "y"
 +
else:
 +
    lastmod = "n"
 +
                   
 +
# Remove mod name from commandline variable
 +
# If the mod is the last mod in the list or the only mod in the list remove FileTitle and not FileTitle;,
 +
#    attempting to remove FileTitle; will fail and the mod will not be removed from the commandline
 +
# If mod removal result; in only one mod being left make sure there is not trailing semi-colon
 +
if (mods.find(';') != -1) :
 +
  if lastmod == "n" :
 +
# Multiple mods not last mod
 +
      ThisService.Variables["Mods"]=mods.Replace(String.Format("@{0};", ModName), "")
 +
  else :
 +
# Last Mod in list
 +
      ThisService.Variables["Mods"]=mods.Replace(String.Format(";@{0}", ModName), "")
 +
else :
 +
# Only Mod
 +
  ThisService.Variables["Mods"]=mods.Replace(String.Format("@{0}", ModName), "")
 +
 
 +
# Setup folders for removal
 +
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", ModName))
 +
modkeys=Path.Combine(modfolder, "keys")
 +
rootkeys=Path.Combine(ThisService.RootDirectory, "keys")
 
   
 
   
        return True
+
# Delete mod key files
 +
if Directory.Exists(modkeys) :
 +
  for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
 +
    keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
 +
    File.Delete(keyfile)
 
   
 
   
  def read_ue4_string(file):
+
  # Delete mod folder
        count = struct.unpack('i', file.read(4))[0]
+
if Directory.Exists(modfolder) :
        flag = False
+
  Directory.Delete(modfolder, True)
        if count < 0:
 
            flag = True
 
            count -= 1
 
 
   
 
   
        if flag or count <= 0:
+
# Becuase some devs develope more tyhan one mod and use the same .bikey file
            return ""
+
# Parse the mod folders and make sire the .bikey files are in the \keys folder
 +
# If not copy the /bikey file
 +
dirinfo = DirectoryInfo(ThisService.RootDirectory)
 +
for i in  dirinfo.GetDirectories("@*") :
 +
  modfolder=Path.Combine(ThisService.RootDirectory, i.Name)
 +
  modkeys=Path.Combine(modfolder, "keys")
 +
  if Directory.Exists(modkeys) :
 +
    for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
 +
      keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
 +
      if not File.Exists(keyfile) :
 +
        File.Copy(file, keyfile)
 
   
 
   
        return file.read(count)[:-1].decode()
+
# Update command line
   
+
ThisService.Save()
  def write_ue4_string(string_to_write, file):
+
ThisService.Configure()
        string_length = len(string_to_write) + 1
+
</source>
        file.write(struct.pack('i', string_length))
+
 
        barray = bytearray(string_to_write, "utf-8")
+
'''Operating System:''' Any
        file.write(barray)
+
  '''Description:''' After Workshop Content Updated
        file.write(struct.pack('p', b'0'))
+
  '''Script Engine:''' IronPython
+
'''Event:''' AfterWorkshopUpdate
###########################################
+
'''Ignore execution errors:''' Unchecked
###########################################
+
 
###########################################
+
<source lang="python>
+
import clr
# Only extract files the correct folder depending on operating system
+
from System import Array, String
oseditor="WindowsNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxNoEditor"
+
from System.IO import File, Path, Directory, SearchOption
noeditor=Path.Combine(InstallPath, oseditor )
+
from System.Threading import Thread
# Extract and delete all .z files
+
import re
for zfile in Directory.GetFiles(noeditor, "*.z", SearchOption.AllDirectories):
+
 
  file=Path.Combine(Path.GetDirectoryName(zfile), Path.GetFileNameWithoutExtension(zfile))
+
# Setup variables
  z_unpack(zfile, file)
+
servertag="Server"
  Script.WriteToConsole("Extracted " + file)
+
mods=""
  File.Delete(zfile)
+
y=[]
  File.Delete(zfile + ".uncompressed_size")
+
ModName=""
+
 
# Move folder to correct location
+
# Get commandline list of mods
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", FileId))
+
if ThisService.Variables.HasValue("Mods") :
Directory.Move(Path.Combine(InstallPath, oseditor), modfolder)
+
  mods=ThisService.Variables["Mods"]
+
else:
# Update ini file
+
  raise Exception("Missing Mod information. Installation Failed!")
serveros = "WindowsServer" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxServer"
+
 
inifile = Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Saved/Config/{0}/GameUserSettings.ini", serveros))
+
# Check for FileTitle
ini = FileIniDataParser()
+
if len(FileTitle) != 0:
data = ini.ReadFile(inifile)
+
# Check FileTitle for special character
data["ServerSettings"]["ActiveMods"] = FileIds
+
  for i in range(len(FileTitle)):
ini.WriteFile(inifile, data)
+
    if bool(re.search('[a-zA-Z0-9 \]\[_\'+-]', FileTitle[i])) :
+
      y.append(FileTitle[i])
#Create .mod
+
    else:
parse_base_info(FileId.ToString())
+
      y.append("")
parse_meta_data(FileId.ToString())
+
         
create_mod_file(FileId.ToString())
+
    ModName= "".join(str(x) for x in y)
 +
    ModName= ModName.Replace("  "," ").Replace(" ","_")
 +
    ModName= ModName.strip()
 +
else: 
 +
   ModName = FileId
 +
   
 +
       
 +
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", ModName))
 +
   
 +
# If mod exists delete the folder
 +
if Directory.Exists(modfolder) :
 +
   Directory.Delete(modfolder, True)
 +
 
 +
# Move mod to root using renamed file name as folder name
 +
Directory.Move(InstallPath, modfolder)
 +
 
 +
# Setup keys path to to copy .bikey file root \keys folder
 +
modkeys=Path.Combine(modfolder, "keys")
 +
rootkeys=Path.Combine(ThisService.RootDirectory, "keys")
 +
 
 +
# Check for \keys folder in root
 +
if not Directory.Exists(rootkeys) :
 +
  Directory.CreateDirectory(rootkeys)
 +
 
 +
# Check for mod \keys folder
 +
# Get list of all files on mods \keys folder
 +
# Copy file(s) from mods \keys folder to root \keys folder
 +
if Directory.Exists(modkeys) :
 +
  for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
 +
    keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
 +
    if not File.Exists(keyfile) :
 +
      File.Copy(file, keyfile)
 +
 
 +
# Update command line
 +
ThisService.Save()
 +
ThisService.Configure()
 +
</source>
  
 +
=== Garry's Mod ===
 +
* Updated 2021/05/23. Fixed errors when file already exists. Gave execute permissions to bin/gmad_linux.
 
  '''Operating System:''' Any
 
  '''Operating System:''' Any
  '''Description:''' Delete mod from ShooterGame/Content/Mods and update GameUserSettings.ini
+
  '''Description:''' After Workshop Content Installed
 
  '''Script Engine:''' IronPython
 
  '''Script Engine:''' IronPython
  '''Event:''' After Workshop Content Uninstalled
+
  '''Event:''' AfterWorkshopInstall
  '''Ignore execution errors''' Unchecked
+
  '''Ignore execution errors:''' Unchecked
 +
 
 +
<source lang="python">import clr
 +
clr.AddReference("TCAdmin.SDK")
 +
from System import Environment, PlatformID, String
 +
from System.IO import Directory, File, Path, SearchOption
 +
from System.Net import WebClient
 +
from System.Diagnostics import Process
 +
from TCAdmin.SDK.Misc import CompressionTools, Linux
 +
 
 +
addon=Path.Combine(ThisService.RootDirectory, "garrysmod", "addons", FileId.ToString())
 +
 
 +
urldownload=True;
 +
#File downloaded from Steam not from URL. Update variable values.
 +
if String.IsNullOrEmpty(FileUrl) :
 +
  for file in Directory.GetFiles(InstallPath) :
 +
    urldownload=False
 +
    FileName=Path.GetFileName(file)
 +
    FileNameNoPath=FileName
 +
    FileNameSavePath=file
 +
 
 +
#Rename .gm to .gma
 +
if FileNameSavePath.EndsWith(".gm") :
 +
  if File.Exists(FileNameSavePath + "a") :
 +
    File.Delete(FileNameSavePath + "a")
 +
  File.Move(FileNameSavePath, FileNameSavePath + "a")
 +
  FileNameSavePath=FileNameSavePath + "a"
 +
  FileName=FileName + "a"
 +
  FileNameNoPath=FileNameNoPath + "a"
 +
 
 +
#If not .gma just move to addons
 +
if not FileNameSavePath.EndsWith(".gma") :
 +
  Directory.CreateDirectory(addon)
 +
  if File.Exists(Path.Combine(addon, FileNameNoPath)) :
 +
    File.Delete(Path.Combine(addon, FileNameNoPath))
 +
  File.Move(FileNameSavePath, Path.Combine(addon, FileNameNoPath))
 +
  Script.Exit();
  
import clr
+
#Important: Only 7zip 9.20 is able to extract the file.
clr.AddReference("INIFileParser")
+
if Environment.OSVersion.Platform == PlatformID.Win32NT :
 +
  _7zaurl="https://www.7-zip.org/a/7za920.zip"
 +
  _7zazip=Path.Combine(TCAdminFolder, "Monitor", "Tools", "7za.zip")
 +
  _7zapath=Path.Combine(TCAdminFolder, "Monitor", "Tools", "7za")
 +
  _7zaexe = Path.Combine(_7zapath, "7za.exe")
 +
  gmad = Path.Combine(ThisService.RootDirectory, "bin", "gmad.exe")
 +
else :
 +
  _7zaurl="http://cdnsource.tcadmin.net/games/__files/p7zip_9.20_x86_linux_bin.tar.bz2"
 +
  _7zazip=Path.Combine(TCAdminFolder, "Monitor", "Tools", "7za.tar.bz2")
 +
  _7zapath=Path.Combine(TCAdminFolder, "Monitor", "Tools", "7za")
 +
  _7zaexe = Path.Combine(_7zapath, "p7zip_9.20", "bin", "7za")
 +
  gmad = Path.Combine(ThisService.RootDirectory, "bin", "gmad_linux")
 +
  Linux.MakeExecutable(gmad)
 
   
 
   
from System.IO import Path, Directory, File
+
#Download 7zip if needed.
from System import Environment, PlatformID, String
+
if not File.Exists(_7zaexe) :
from IniParser import FileIniDataParser
+
  Script.WriteToConsole("Downloading 7zip...")
from IniParser.Model import IniData
+
  wc = WebClient()
+
  wc.DownloadFile(_7zaurl, _7zazip);
# Delete folder
+
  c=CompressionTools()
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", FileId))
+
  c.Decompress(_7zazip, _7zapath)
if Directory.Exists(modfolder) :
+
  File.Delete(_7zazip)
  Directory.Delete(modfolder, True)
+
 
+
#Only files downloaded from URL need to be extracted
#Delete .mod
+
if urldownload :
modfile=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}.mod", FileId))
+
  p = Process()
if File.Exists(modfile) :
+
  p.StartInfo.FileName=_7zaexe
  File.Delete(modfile)
+
  p.StartInfo.Arguments=String.Format('e -y -o"{0}" "{1}"', addon, FileNameSavePath)
+
  p.StartInfo.WorkingDirectory=ThisService.WorkingDirectory
# Update ini file
+
  p.Start()
serverfolder = "WindowsServer" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxServer"
+
  p.WaitForExit()
inifile = Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Saved/Config/{0}/GameUserSettings.ini", serverfolder))
+
  extractedfile=Path.Combine(addon, FileNameNoPath).Replace(".gma", String.Empty)
ini = FileIniDataParser()
+
  realgma=Path.Combine(ThisService.RootDirectory, "garrysmod", "addons", String.Format("{0}.gma", FileId))
data = ini.ReadFile(inifile)
+
  if File.Exists(realgma) :  
data["ServerSettings"]["ActiveMods"] = FileIds
+
    File.Delete(realgma)
ini.WriteFile(inifile, data)
+
  File.Move(extractedfile, realgma)
 +
else :
 +
  realgma=Path.Combine(Path.Combine(ThisService.RootDirectory, "garrysmod", "addons"), FileId.ToString() + ".gma")
 +
  if File.Exists(realgma) :  
 +
    File.Delete(realgma)
 +
  File.Move(FileNameSavePath, realgma)
 +
 
 +
p = Process()
 +
p.StartInfo.FileName=gmad
 +
p.StartInfo.Arguments=String.Format('extract -file "{0}"', realgma)
 +
p.StartInfo.WorkingDirectory=addon
 +
p.Start()
 +
p.WaitForExit()
 +
 
 +
File.Delete(realgma)</source>
  
=== America's Army Proving Grounds ===
 
 
  '''Operating System:''' Any
 
  '''Operating System:''' Any
  '''Description:''' Updates [SteamUGCManager.SteamUGCManager] in AASteamUGCManager.ini
+
  '''Description:''' After Workshop Content Uninstalled
 
  '''Script Engine:''' IronPython
 
  '''Script Engine:''' IronPython
  '''Events:''' After Workshop Content Installed, After Workshop Content Uninstalled
+
  '''Event:''' AfterWorkshopUninstall
  '''Ignore execution errors''' Unchecked
+
  '''Ignore execution errors:''' Unchecked
  
 +
<source lang="python>
 
  import clr
 
  import clr
clr.AddReference("INIFileParser")
+
  from System.IO import Directory, Path
import sys
 
 
from IniParser.Parser import IniDataParser
 
from IniParser import FileIniDataParser
 
  from System.IO import Path, File
 
from System import String
 
 
#Remove all keys from [SteamUGCManager.SteamUGCManager]
 
inifile = Path.Combine(ThisService.RootDirectory, String.Format("AAGame/Config/ServerConfig/AASteamUGCManager.ini"))
 
dataparser = IniDataParser()
 
ini = FileIniDataParser(dataparser)
 
data = ini.ReadFile(inifile)
 
data["SteamUGCManager.SteamUGCManager"].RemoveAllKeys()
 
ini.WriteFile(inifile, data)
 
 
#Add mods under [SteamUGCManager.SteamUGCManager]
 
i=0
 
items=""
 
while i < len(FileIdsArray):
 
  items = items + String.Format("ServerSubscribedItems=(IdString={0})\n",FileIdsArray[i])
 
  i += 1
 
 
   
 
   
  contents=File.ReadAllText(inifile)
+
  addon = Path.Combine(ThisService.RootDirectory, "garrysmod", "addons", FileId.ToString())
contents=contents.Replace("[SteamUGCManager.SteamUGCManager]", "[SteamUGCManager.SteamUGCManager]\n" + items)
+
  if Directory.Exists(addon) :
  File.WriteAllText(inifile, contents)
+
  Directory.Delete(addon, True);
 
+
</source>
  
 
=== Space Engineers ===
 
=== Space Engineers ===
Line 368: Line 1,394:
 
  '''Ignore execution errors''' Unchecked
 
  '''Ignore execution errors''' Unchecked
  
  import clr;
+
<source lang="python>
 +
  import clr
 +
clr.AddReference("System.Xml")
 +
 +
from System import Exception
 
  from System import String
 
  from System import String
  from System.IO import File, Path
+
  from System.IO import Path
 +
from System.Xml import XmlDocument
 
   
 
   
  configpath=Path.Combine(ThisService.RootDirectory, "SpaceEngineers-dedicated.cfg")
+
  configpath=Path.Combine(ThisService.RootDirectory, "Sandbox.sbc")
  modlines=String.Format("<ModItem>\n<Name>{0}.sbm</Name>\n<PublishedFileId>{0}</PublishedFileId>\n</ModItem>", FileId)
+
  xmldoc = XmlDocument()
  if File.Exists(configpath):
+
xmldoc.Load(configpath)
  contents=File.ReadAllText(configpath)
+
  contents=contents.Replace("</Mods>", modlines + "\n</Mods>")
+
mods=xmldoc.SelectSingleNode("MyObjectBuilder_Checkpoint/Mods")
  File.WriteAllText(configpath, contents)
+
if mods is None:
 +
  raise Exception("Could not find Mods section")
 +
 
 +
#Add only if mod does not exist
 +
modinfo=mods.SelectSingleNode(String.Format("ModItem[PublishedFileId='{0}']", FileId))
 +
  if modinfo is None:
 +
    modinfo = xmldoc.CreateElement("ModItem")
 +
    modname = xmldoc.CreateElement("Name")
 +
    modfileid = xmldoc.CreateElement("PublishedFileId")
 +
    modname.InnerText=String.Format("{0}.sbm", FileId)
 +
    modfileid.InnerText=FileId.ToString()
 +
    modinfo.AppendChild(modname)
 +
    modinfo.AppendChild(modfileid)
 +
    mods.AppendChild(modinfo)
 +
    xmldoc.Save(configpath)
 +
</source>
  
 
  '''Operating System:''' Any
 
  '''Operating System:''' Any
Line 385: Line 1,431:
 
  '''Ignore execution errors''' Unchecked
 
  '''Ignore execution errors''' Unchecked
  
 +
<source lang="python>
 
  import clr
 
  import clr
 
  clr.AddReference("System.Xml")
 
  clr.AddReference("System.Xml")
Line 393: Line 1,440:
 
  from System.Xml import XmlDocument
 
  from System.Xml import XmlDocument
 
   
 
   
  configpath=Path.Combine(ThisService.RootDirectory, "SpaceEngineers-dedicated.cfg")
+
  configpath=Path.Combine(ThisService.RootDirectory, "Sandbox.sbc")
 
  xmldoc = XmlDocument()
 
  xmldoc = XmlDocument()
 
  xmldoc.Load(configpath)
 
  xmldoc.Load(configpath)
 
   
 
   
  mods=xmldoc.SelectSingleNode("MyConfigDedicated/Mods")
+
  mods=xmldoc.SelectSingleNode("MyObjectBuilder_Checkpoint/Mods")
 
  if mods is None:
 
  if mods is None:
 
   raise Exception("Could not find Mods section")
 
   raise Exception("Could not find Mods section")
 
+
 +
#Remove mod and save file only if the mod exists in the file
 
  modinfo=mods.SelectSingleNode(String.Format("ModItem[PublishedFileId='{0}']", FileId))
 
  modinfo=mods.SelectSingleNode(String.Format("ModItem[PublishedFileId='{0}']", FileId))
 
  if not modinfo is None:
 
  if not modinfo is None:
 
   mods.RemoveChild(modinfo)
 
   mods.RemoveChild(modinfo)
 
   xmldoc.Save(configpath)
 
   xmldoc.Save(configpath)
 +
</source>
 +
 +
== Known Issues ==
 +
; The remote server returned an error<nowiki>:</nowiki> (429) Too Many Requests.
 +
: This is a temporary error from the Steam api. Wait a few seconds and start the task again.

Latest revision as of 17:35, 28 May 2023

The Steam Workshop browser allows your users to download and install mods and maps from Steam with a single click. Each game handles Workshop mods differently. You must configure scripts that move the mod files to the correct location after it has been downloaded.


Workshop Browser.png

Configure the Workshop Browser

Go to Settings > Default Steam Settings. Enter your Steam API key.

Go to Settings > Games > select the game > Steam Settings. Set the following values:

  • Store Id - Set the game's id in the Steam store. For example: https://store.steampowered.com/app/4000/Garrys_Mod/
  • Enable Workshop Browser - Enables the Workshop browser.
  • Stop before installing Workshop content - Specify if you want to stop the service before installing content.
  • Skip Workshop download - If enabled the content is not downloaded automatically. Use this option if the game can download content automatically.
  • Workshop file id format - Format used on the file id. For example if the file id needs @ at the beginning use @![FileId]
  • Workshop file id separator - Specify the characters used to separate file ids. Use ![NewLine] to separate with a new line character.

Go to Settings > Games > select the game > Feature Permissions and enable Workshop browser for users, resellers and sub admins. (If you want to test this feature as admin don't enable these options)

Script Events

After Workshop Content Installed
Occurs after the content has been downloaded.
Available objects:
ThisServer, ThisGame, ThisUser, ThisService
Available Variables:
FileId - The id of the Workshop content that was installed.
FileIds - A list of currently installed file ids formatted and separated according to the game's configuration. It includes the file id that is being installed.
FileIdsArray - An uint32 array of currently installed file ids. It includes the file id that is being installed.
InstallPath - The folder where the content is downloaded. For example: ServiceRoot/steamapps/Workshop/content/STOREID/FILEID
FileUrl - Url to download the file. If the content has more than 1 file this value is blank.
FileName - The filename according to the Steam API. The value is relative to the install path. If the content has more than 1 file this value is blank. For example: mymaps/aim_ak47_training_csgo.bsp
FileNameNoPath - The filename without any paths. If the content has more than 1 file this value is blank. For example: aim_ak47_training_csgo.bsp
FileNameSavePath - The full path where the single file is downloaded. For example ServiceRoot/steamapps/Workshop/content/STOREID/FILEID/mymaps/aim_ak47_training_csgo.bsp
TagsArray - A string array that contains the content's tags.
Tags - A list of the content's tags separated by comma.
FileTitle - The content's name.


After Workshop Content Uninstalled
Occurs after the content has been uninstalled.
Available objects:
ThisServer, ThisGame, ThisUser, ThisService
Available Variables:
FileId - The id of the Workshop content that was uninstalled.
FileIds - A list of currently installed file ids formatted and separated according to the game's configuration. It does not include the file id that is being uninstalled.
FileIdsArray - An uint32 array of currently installed file ids. It does not include the file id that is being uninstalled.
InstallPath - The folder where the content is located. For example: ServiceRoot/steamapps/Workshop/content/STOREID/FILEID
FileUrl - Url to download the file. If the content has more than 1 file this value is blank.
FileName - The filename according to the Steam API. The value is relative to the install path. If the content has more than 1 file this value is blank. For example: mymaps/aim_ak47_training_csgo.bsp
FileNameNoPath - The filename without any paths. If the content has more than 1 file this value is blank. For example: aim_ak47_training_csgo.bsp
FileNameSavePath - The full path where the single file is downloaded. For example ServiceRoot/steamapps/Workshop/content/STOREID/FILEID/mymaps/aim_ak47_training_csgo.bsp
TagsArray - A string array that contains the content's tags.
Tags - A list of the content's tags separated by comma.
FileTitle - The content's name.


After Workshop Content Updated (available in 2.0.131.3 and greater)
Occurs after the content has been updated. If the game's workshop update script is not configured the uninstall/install scripts will be executed.
Available objects:
ThisServer, ThisGame, ThisUser, ThisService
Available Variables:
FileId - The id of the Workshop content that was installed.
FileIds - A list of currently installed file ids formatted and separated according to the game's configuration. It includes the file id that is being installed.
FileIdsArray - An uint32 array of currently installed file ids. It includes the file id that is being installed.
InstallPath - The folder where the content is downloaded. For example: ServiceRoot/steamapps/Workshop/content/STOREID/FILEID
FileUrl - Url to download the file. If the content has more than 1 file this value is blank.
FileName - The filename according to the Steam API. The value is relative to the install path. If the content has more than 1 file this value is blank. For example: mymaps/aim_ak47_training_csgo.bsp
FileNameNoPath - The filename without any paths. If the content has more than 1 file this value is blank. For example: aim_ak47_training_csgo.bsp
FileNameSavePath - The full path where the single file is downloaded. For example ServiceRoot/steamapps/Workshop/content/STOREID/FILEID/mymaps/aim_ak47_training_csgo.bsp
TagsArray - A string array that contains the content's tags.
Tags - A list of the content's tags separated by comma.
FileTitle - The content's name.
Before Workshop Automatic Update (available in 2.0.144 and greater)
This event is executed when the service has a scheduled task for Workshop Update. Use it to send an update notification to the connected players. See the Ark scripts for an example.
Available objects:
ThisServer, ThisGame, ThisUser, ThisService

Sample Scripts

The Workshop browser downloads the content to ServiceRoot/steamapps/Workshop/content/STOREID/FILEID. It is up to the administrator to configure a script that moves the files to the correct location and update the config file. Contact TCAdmin support if you need help creating a script.

Ark

  • Updated 2020/08/24. Fix for file ids > 2147483647 after this was fixed in the game
  • Updated 2020/07/07. Fix for file ids > 2147483647

After Workshop Install

The parallel code in the after installed script bellow causes Mono to crash. On Linux use this script instead: Ark Workshop Scripts for Linux

Operating System: Windows
Description: Extract .z, copy to ShooterGame/Content/Mods and update GameUserSettings.ini
Script Engine: IronPython
Event: After Workshop Content Installed
Ignore execution errors Unchecked
Notes This script supports extraction of multiple files simultaneously. The default is 2. You can increase it by editing this line near the bottom of the script.
options.MaxDegreeOfParallelism = 2
For VPS the recommended value is 2. For dedicated servers the recommended value is 4. The higher the value the faster the script will execute but it will require more CPU and disk. Set it too high and it might take even longer.
import clr


from System.IO import Directory, File, Path, SearchOption
from System import Environment, PlatformID, String, Exception
from System.Text.RegularExpressions import Regex, RegexOptions, Match

		   

#Support for parallel extraction
clr.AddReference('System.Core')
from System.Collections.Generic import List
from System import Action
from System.Threading.Tasks import Parallel, ParallelOptions

extractedcount=0
totalfilecount=0
lastfileprogress=0
########################################
# https://github.com/TheCherry/ark-server-manager #
########################################
import struct
import zlib
import sys

def str_to_l(st):
    return struct.unpack('q', st)[0]

def z_unpack(src, dst):
    global extractedcount, totalfilecount, lastfileprogress
    with open(src, 'rb') as f_src:
        with open(dst, 'wb') as f_dst:
            f_src.read(8)
            size1 = str_to_l(f_src.read(8))
            f_src.read(8)
            size2 = str_to_l(f_src.read(8))
            if(size1 == -1641380927):
                size1 = 131072L
            runs = (size2 + size1 - 1L) / size1
            array = []
            for i in range(runs):
                array.append(f_src.read(8))
                f_src.read(8)
            for i in range(runs):
                to_read = array[i]
                compressed = f_src.read(str_to_l(to_read))
                decompressed = zlib.decompress(compressed)
                f_dst.write(decompressed)
    Script.WriteToConsole("Extracted " + dst.Replace(ThisService.RootDirectory, ""))
    File.Delete(src)
    File.Delete(src + ".uncompressed_size")
    extractedcount=extractedcount+1
    progress=round((float(extractedcount)/totalfilecount)*100,0)
    if progress > lastfileprogress + 4:
      lastfileprogress=progress
      ThisTaskStep.UpdateProgress(progress)
     
#######################################################################
# https://github.com/barrycarey/Ark_Mod_Downloader/blob/master/Ark_Mod_Downloader.py #
#######################################################################
import os
import struct
from collections import OrderedDict
map_names = []
map_count=0
temp_mod_path = os.path.join(ThisService.RootDirectory, "ShooterGame/Content/Mods")
meta_data = OrderedDict([])

def parse_base_info(modid):
        Script.WriteToConsole("[+] Collecting Mod Details From mod.info")

        mod_info = os.path.join(temp_mod_path, modid, "mod.info")

        if not os.path.isfile(mod_info):
            raise Exception("[x] Failed to locate mod.info. Cannot Continue. Please try again.")
            return False

        with open(mod_info, "rb") as f:
            read_ue4_string(f)
            map_count = struct.unpack('i', f.read(4))[0]

            for i in range(map_count):
                cur_map = read_ue4_string(f)
                if cur_map:
                    map_names.append(cur_map)

        return True

def parse_meta_data(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(temp_mod_path, modid, "modmeta.info")
        if not os.path.isfile(mod_meta):
            raise Exception("[x] Failed To Locate modmeta.info. Cannot continue without it. Please try again.")
            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:
                    Script.WriteToConsole("[!] " + key + ":" + value)
                    meta_data[key] = value

        return True

def create_mod_file(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 parse_base_info(modid) or not parse_meta_data(modid):
            return False

        print("[+] Writing .mod File")
        with open(os.path.join(temp_mod_path, modid + ".mod"), "w+b") as f:

            modid = int(modid)
            if modid > 2147483647:
              diff = modid-2147483647
              modid = -2147483647 + diff - 2					  						
            f.write(struct.pack('ixxxx', modid))  # Needs 4 pad bits
            write_ue4_string("ModName", f)
            write_ue4_string("", f)

            map_count = len(map_names)
            f.write(struct.pack("i", map_count))

            for m in map_names:
                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 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(meta_data)
            f.write(struct.pack('i', meta_length))

            for k, v in meta_data.items():
                write_ue4_string(k, f)
                write_ue4_string(v, f)

        return True

def read_ue4_string(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(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'))

###########################################
###########################################
###########################################

# Only extract files the correct folder depending on operating system
oseditor="WindowsNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxNoEditor"
noeditor=Path.Combine(InstallPath, oseditor )

# Use other OS folder if it doesn't exist.
if not Directory.Exists(noeditor) :
  oseditor = "LinuxNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "WindowsNoEditor"
  noeditor = Path.Combine(InstallPath, oseditor)  

# Extract and delete all .z files
actions = List[Action]()
for zfile in Directory.GetFiles(noeditor, "*.z", SearchOption.AllDirectories):
 file=Path.Combine(Path.GetDirectoryName(zfile), Path.GetFileNameWithoutExtension(zfile))
 action=Action(lambda a=zfile, b=file: z_unpack(a, b))
 actions.Add(action)

options=ParallelOptions()
#Extract 2 files at a time.
options.MaxDegreeOfParallelism = 2
totalfilecount=actions.Count
ThisTaskStep.WriteLog(String.Format("Extracting {0} files...", totalfilecount))
ThisTaskStep.UpdateProgress(0)
Parallel.Invoke(options, actions.ToArray())

# Move folder to correct location. Delete if it already exists.
# Define modid before FileId is altered so we write the correct id to inifile
modid = FileId
if FileId > 2147483647:
  diff = FileId-2147483647
  FileId = -2147483647 + diff - 2

modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", modid))
if Directory.Exists(modfolder) :
  Directory.Delete(modfolder, True)
Directory.Move(Path.Combine(InstallPath, oseditor), modfolder)

# Update ini file
serveros = "WindowsServer" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxServer"
inifile = Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Saved/Config/{0}/GameUserSettings.ini", serveros))
pattern="ActiveMods[ \t]*=[ \t]*(?<ActiveMods>[0-9, \t]*)"
filecontents = File.ReadAllText(inifile)
match = Regex.Match(filecontents, pattern, RegexOptions.IgnoreCase)
if match.Success :
  activemods = match.Groups["ActiveMods"].Value
  if String.IsNullOrEmpty(activemods) or activemods.IndexOf(modid.ToString()) == -1 :
    if activemods.Length > 0 :
      activemods = activemods + ","
      activemods = activemods + modid.ToString()
      filecontents=filecontents.Replace(match.Groups["ActiveMods"].Value, activemods)
    else :
      activemods = modid.ToString()
      filecontents = filecontents.Substring(0, match.Groups["ActiveMods"].Index) + activemods + filecontents.Substring(match.Groups["ActiveMods"].Index)
    File.WriteAllText(inifile, filecontents)

#Create .mod
parse_base_info(modid.ToString())
parse_meta_data(modid.ToString())
create_mod_file(modid.ToString())

# Delete folder
if Directory.Exists(InstallPath) :
  Directory.Delete(InstallPath, True)

After Workshop Uninstall

Operating System: Any
Description: Delete mod from ShooterGame/Content/Mods and update GameUserSettings.ini
Script Engine: IronPython
Event: After Workshop Content Uninstalled
Ignore execution errors Unchecked
import clr

from System.IO import Path, Directory, File
from System import Environment, PlatformID, String
from System.Text.RegularExpressions import Regex, RegexOptions, Match

modid = FileId
if FileId > 2147483647:
  diff = FileId-2147483647
  FileId = -2147483647 + diff - 2

# Delete folder
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", modid))
if Directory.Exists(modfolder) :
  Directory.Delete(modfolder, True)

#Delete .mod
modfile=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}.mod", modid))
if File.Exists(modfile) :
  File.Delete(modfile)

# Update ini file
serverfolder = "WindowsServer" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxServer"
inifile = Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Saved/Config/{0}/GameUserSettings.ini", serverfolder))
pattern="ActiveMods[ \t]*=[ \t]*(?<ActiveMods>[0-9, \t]*)"
filecontents = File.ReadAllText(inifile)
match = Regex.Match(filecontents, pattern, RegexOptions.IgnoreCase)
if match.Success :
  activemods = match.Groups["ActiveMods"].Value
  if activemods.IndexOf(modid.ToString()) != -1 :
    activemods = activemods.Replace("," + modid.ToString(), String.Empty).Replace(modid.ToString() + ",", String.Empty).Replace(modid.ToString(), String.Empty)
    filecontents=filecontents.Replace(match.Groups["ActiveMods"].Value, activemods)
    File.WriteAllText(inifile, filecontents)

After Workshop Update

The parallel code in the after updated script bellow causes Mono to crash. On Linux use this script instead: Ark Workshop Scripts for Linux

This script is the same as the install script except it does not update the .ini so it keeps the mod order.

import clr


from System.IO import Directory, File, Path, SearchOption
from System import Environment, PlatformID, String, Exception
from System.Text.RegularExpressions import Regex, RegexOptions, Match

		   

#Support for parallel extraction
clr.AddReference('System.Core')
from System.Collections.Generic import List
from System import Action
from System.Threading.Tasks import Parallel, ParallelOptions

extractedcount=0
totalfilecount=0
lastfileprogress=0
########################################
# https://github.com/TheCherry/ark-server-manager #
########################################
import struct
import zlib
import sys

def str_to_l(st):
    return struct.unpack('q', st)[0]

def z_unpack(src, dst):
    global extractedcount, totalfilecount, lastfileprogress
    with open(src, 'rb') as f_src:
        with open(dst, 'wb') as f_dst:
            f_src.read(8)
            size1 = str_to_l(f_src.read(8))
            f_src.read(8)
            size2 = str_to_l(f_src.read(8))
            if(size1 == -1641380927):
                size1 = 131072L
            runs = (size2 + size1 - 1L) / size1
            array = []
            for i in range(runs):
                array.append(f_src.read(8))
                f_src.read(8)
            for i in range(runs):
                to_read = array[i]
                compressed = f_src.read(str_to_l(to_read))
                decompressed = zlib.decompress(compressed)
                f_dst.write(decompressed)
    Script.WriteToConsole("Extracted " + dst.Replace(ThisService.RootDirectory, ""))
    File.Delete(src)
    File.Delete(src + ".uncompressed_size")
    extractedcount=extractedcount+1
    progress=round((float(extractedcount)/totalfilecount)*100,0)
    if progress > lastfileprogress + 4:
      lastfileprogress=progress
      ThisTaskStep.UpdateProgress(progress)
     
#######################################################################
# https://github.com/barrycarey/Ark_Mod_Downloader/blob/master/Ark_Mod_Downloader.py #
#######################################################################
import os
import struct
from collections import OrderedDict
map_names = []
map_count=0
temp_mod_path = os.path.join(ThisService.RootDirectory, "ShooterGame/Content/Mods")
meta_data = OrderedDict([])

def parse_base_info(modid):
        Script.WriteToConsole("[+] Collecting Mod Details From mod.info")

        mod_info = os.path.join(temp_mod_path, modid, "mod.info")

        if not os.path.isfile(mod_info):
            raise Exception("[x] Failed to locate mod.info. Cannot Continue. Please try again.")
            return False

        with open(mod_info, "rb") as f:
            read_ue4_string(f)
            map_count = struct.unpack('i', f.read(4))[0]

            for i in range(map_count):
                cur_map = read_ue4_string(f)
                if cur_map:
                    map_names.append(cur_map)

        return True

def parse_meta_data(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(temp_mod_path, modid, "modmeta.info")
        if not os.path.isfile(mod_meta):
            raise Exception("[x] Failed To Locate modmeta.info. Cannot continue without it. Please try again.")
            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:
                    Script.WriteToConsole("[!] " + key + ":" + value)
                    meta_data[key] = value

        return True

def create_mod_file(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 parse_base_info(modid) or not parse_meta_data(modid):
            return False

        print("[+] Writing .mod File")
        with open(os.path.join(temp_mod_path, modid + ".mod"), "w+b") as f:

            modid = int(modid)
            if modid > 2147483647:
              diff = modid-2147483647
              modid = -2147483647 + diff - 2					  						
            f.write(struct.pack('ixxxx', modid))  # Needs 4 pad bits
            write_ue4_string("ModName", f)
            write_ue4_string("", f)

            map_count = len(map_names)
            f.write(struct.pack("i", map_count))

            for m in map_names:
                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 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(meta_data)
            f.write(struct.pack('i', meta_length))

            for k, v in meta_data.items():
                write_ue4_string(k, f)
                write_ue4_string(v, f)

        return True

def read_ue4_string(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(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'))

###########################################
###########################################
###########################################

# Only extract files the correct folder depending on operating system
oseditor="WindowsNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxNoEditor"
noeditor=Path.Combine(InstallPath, oseditor )

# Use other OS folder if it doesn't exist.
if not Directory.Exists(noeditor) :
  oseditor = "LinuxNoEditor" if Environment.OSVersion.Platform == PlatformID.Win32NT else "WindowsNoEditor"
  noeditor = Path.Combine(InstallPath, oseditor)  

# Extract and delete all .z files
actions = List[Action]()
for zfile in Directory.GetFiles(noeditor, "*.z", SearchOption.AllDirectories):
 file=Path.Combine(Path.GetDirectoryName(zfile), Path.GetFileNameWithoutExtension(zfile))
 action=Action(lambda a=zfile, b=file: z_unpack(a, b))
 actions.Add(action)

options=ParallelOptions()
#Extract 2 files at a time.
options.MaxDegreeOfParallelism = 2
totalfilecount=actions.Count
ThisTaskStep.WriteLog(String.Format("Extracting {0} files...", totalfilecount))
ThisTaskStep.UpdateProgress(0)
Parallel.Invoke(options, actions.ToArray())

# Move folder to correct location. Delete if it already exists.
# Define modid before FileId is altered so we write the correct id to inifile
modid = FileId
if FileId > 2147483647:
  diff = FileId-2147483647
  FileId = -2147483647 + diff - 2

modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", modid))
if Directory.Exists(modfolder) :
  Directory.Delete(modfolder, True)
Directory.Move(Path.Combine(InstallPath, oseditor), modfolder)

#Create .mod
parse_base_info(modid.ToString())
parse_meta_data(modid.ToString())
create_mod_file(modid.ToString())

# Delete folder
if Directory.Exists(InstallPath) :
  Directory.Delete(InstallPath, True)

Before Workshop Automatic Update

This script sends a message to players 5 minutes before doing an automatic update.

Operating System: Any
Description: Broadcast a message, wait 5 minutes, save world
Script Engine: IronPython
Event: Before Workshop Automatic Update
Ignore execution errors Checked
import clr
clr.AddReference("TCAdmin.GameHosting.SDK")
clr.AddReference("TCAdmin.Interfaces")

from System.IO import File, Path
from System.Text.RegularExpressions import Regex, RegexOptions
from System import Environment, PlatformID, String
from System.Threading import Thread
from TCAdmin.GameHosting.SDK import rconClient
from TCAdmin.GameHosting.SDK import RCONGameType
from TCAdmin.Interfaces.Server import ServiceStatus

if ThisService.Status.ServiceStatus != ServiceStatus.Running :
  Script.WriteToConsole(String.Format("{0} is stopped. The script will not continue.", ThisService.ConnectionInfo))
  Script.Exit()

serveros = "WindowsServer" if Environment.OSVersion.Platform == PlatformID.Win32NT else "LinuxServer"
inifile = Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Saved/Config/{0}/GameUserSettings.ini", serveros))

pattern="ServerAdminPassword[ \t]*=[ \t]*[\"]?(?<ServerAdminPassword>([^\"\r\n])*)[\"]?"
filecontents = File.ReadAllText(inifile)
match = Regex.Match(filecontents, pattern, RegexOptions.IgnoreCase)

if match.Success :
  rconpass = match.Groups["ServerAdminPassword"].Value
  Script.WriteToConsole(String.Format("RCon password is: {0}", rconpass))
  
  rconclient=rconClient()
  rconclient.GameType = RCONGameType.CounterStrikeSource
  rconclient.Server = ThisService.IpAddress
  rconclient.Port = ThisService.RConPort
  Script.WriteToConsole("Sending update notification...")
  rconclient.Send(None, None, rconpass, "broadcast \"Server will update mods in 5 minutes!\"")
  Script.WriteToConsole(String.Format("RCon Response: {0}", rconclient.ReadResponse()))
  Script.WriteToConsole("Waiting for 5 minutes...")
  Thread.Sleep(300000)
  Script.WriteToConsole("Saving world...")
  rconclient.Send(None, None, rconpass, "saveworld")
  Script.WriteToConsole(String.Format("RCon Response: {0}", rconclient.ReadResponse()))
  Script.WriteToConsole("Done.")
  

Arma 3

  • Go to the game's settings. Add a variable named Mods and another named ServerMods.
  • Go to the game's command lines. Add the variables to the game's command line: -mod=![Mods] -servermod=![ServerMods]
  • Go to the game's steam settings. Set Workshop File Id Format to ![FileId] and Workshop File Id Separator to ;
Operating System: Any
Description: Configure mod
Script Engine: IronPython
Event: After Workshop Content Installed
Ignore execution errors Unchecked
 import clr
 from System import Array, String
 from System.IO import File, Path, Directory, SearchOption
  
 servertag="Server"
 servermods=""
 mods=""
 
 if ThisService.Variables.HasValue("ServerMods") :
   servermods=ThisService.Variables["ServerMods"]
 
 if ThisService.Variables.HasValue("Mods") :
   mods=ThisService.Variables["Mods"]
   
 if Array.IndexOf(TagsArray, servertag) == -1 :
   ThisService.Variables["Mods"]=String.Format("@{0}", FileId) + ";" + mods    
 else :
   ThisService.Variables["ServerMods"]=String.Format("@{0}", FileId) + ";" + servermods  
 
 # Move folder to correct location
 modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", FileId))
 if Directory.Exists(modfolder) :
   Directory.Delete(modfolder, True)
 Directory.Move(InstallPath, modfolder)
 
 # Move keys to root key folder
 modkeys=Path.Combine(modfolder, "keys")
 rootkeys=Path.Combine(ThisService.RootDirectory, "keys")
 Directory.CreateDirectory(rootkeys)
 if Directory.Exists(modkeys) :
   for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
     keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
     if File.Exists(keyfile) :
       File.Delete(keyfile)
     File.Move(file, keyfile)
 
 # Move key to root key folder
 modkeys=Path.Combine(modfolder, "key")
 if Directory.Exists(modkeys) :
   for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
     keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
     if File.Exists(keyfile) :
       File.Delete(keyfile)
     File.Move(file, keyfile)
 
 # Update command line
 ThisService.Save()
 ThisService.Configure()
Operating System: Any
Description: Configure mod
Script Engine: IronPython
Event: After Workshop Content Uninstalled
Ignore execution errors Unchecked
 import clr
 from System import Array, String
 from System.IO import Path, Directory
 
 servermods=""
 mods=""
 
 if ThisService.Variables.HasValue("ServerMods") :
   servermods=ThisService.Variables["ServerMods"]
 
 if ThisService.Variables.HasValue("Mods") :
   mods=ThisService.Variables["Mods"]
   
 ThisService.Variables["ServerMods"]=servermods.Replace(String.Format("@{0};", FileId), "")
 ThisService.Variables["Mods"]=mods.Replace(String.Format("@{0};", FileId), "")
 
 # Delete mod folder
 modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", FileId))
 if Directory.Exists(modfolder) :
   Directory.Delete(modfolder, True)
 
 # Update command line
 ThisService.Save()
 ThisService.Configure()
Operating System: Any
Description: Clear workshop mod variables
Script Engine: IronPython
Event: After Reinstalled
Ignore execution errors Unchecked
 ThisService.Variables["ServerMods"]=""
 ThisService.Variables["Mods"]=""
 ThisService.Save()

America's Army Proving Grounds

Operating System: Any
Description: Updates [SteamUGCManager.SteamUGCManager] in AASteamUGCManager.ini
Script Engine: IronPython
Events: After Workshop Content Installed, After Workshop Content Uninstalled
Ignore execution errors Unchecked
 import clr
 clr.AddReference("INIFileParser")

 from IniParser.Model.Configuration import IniParserConfiguration
 from IniParser.Parser import IniDataParser
 from IniParser import FileIniDataParser
 from System.IO import Path, File
 from System import String
 
 #Remove all keys from [SteamUGCManager.SteamUGCManager]
 inifile = Path.Combine(ThisService.RootDirectory, String.Format("AAGame/Config/ServerConfig/AASteamUGCManager.ini"))
 iniconfig = IniParserConfiguration()
 iniconfig.AllowDuplicateKeys = True
 dataparser = IniDataParser(iniconfig)
 ini = FileIniDataParser(dataparser)
 data = ini.ReadFile(inifile)
 data["SteamUGCManager.SteamUGCManager"].RemoveAllKeys()
 ini.WriteFile(inifile, data)
 
 #Add mods under [SteamUGCManager.SteamUGCManager]
 i=0
 items=""
 while i < len(FileIdsArray):
  	items = items + String.Format("ServerSubscribedItems=(IdString={0})\n",FileIdsArray[i])
  	i += 1
 
 contents=File.ReadAllText(inifile)
 contents=contents.Replace("[SteamUGCManager.SteamUGCManager]", "[SteamUGCManager.SteamUGCManager]\n" + items)
 File.WriteAllText(inifile, contents)

Broke Protocol: Online City RPG

Operating System: Windows
Description: Moves mod files.
Script Engine: Batch
Event: After Workshop Content Installed
Ignore execution errors Unchecked
 move /y %InstallPath% %ThisService_RootDirectory%\AssetBundles\%FileId%
Operating System: Windows
Description: Delete mod files.
Script Engine: Batch
Event: After Workshop Content Uninstalled
Ignore execution errors Unchecked
 rd /q /s %ThisService_RootDirectory%\AssetBundles\%FileId%

Conan Exiles

Operating System: Any
Description: Moves mod files.
Script Engine: IronPython
Event: After Workshop Content Installed
Ignore execution errors Unchecked
These scripts have been updated to keep the order of modlist.txt
 import clr
 from System import String
 from System.IO import Directory, File, Path, SearchOption, DirectoryInfo
 
 import System
 clr.AddReference("System.Core")
 clr.ImportExtensions(System.Linq)
 
 modpath = Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods")
 Directory.CreateDirectory(modpath)
 
 #Save a list of the mod's files so we can delete them when uninstalling the mod. 
 modpakfile = "";
 modfilelist=Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods", String.Format("{0}.txt", FileId))
 for file in Directory.GetFiles(InstallPath, "*", SearchOption.AllDirectories):
   modfile = Path.Combine(modpath, Path.GetFileName(file))
   if(File.Exists(modfile)) :
    File.Delete(modfile)
   File.Move(file, modfile)
   modfilename=Path.GetFileName(file)
   File.AppendAllText(modfilelist, String.Format("{0}\r", modfilename))
   if Path.GetExtension(modfilename) == ".pak" :
     modpakfile = modfilename
 
 #Create modlist.txt
 modlisttxt=Path.Combine(modpath, "modlist.txt")
 if File.Exists(modlisttxt) :
   lines = File.ReadAllLines(modlisttxt)
   File.Delete(modlisttxt)
   with File.AppendText(modlisttxt) as fs :
     for line in lines :
       fs.WriteLine(line)
    
 with File.AppendText(modlisttxt) as fs :
   fs.WriteLine(modpakfile)
Operating System: Any
Description: Delete mod files
Script Engine: IronPython
Event: After Workshop Content Uninstalled
Ignore execution errors Unchecked
 import clr
 from System import String
 from System.IO import Directory, File, Path, SearchOption, DirectoryInfo
 
 import System
 clr.AddReference("System.Core")
 clr.ImportExtensions(System.Linq)
 
 modpath = Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods")
 modfilelist=Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods", String.Format("{0}.txt", FileId))
 
 modpakfile = "";
 if File.Exists(modfilelist) :
   modfiles = File.ReadAllLines(modfilelist)
   for modfile in modfiles :
     if Path.GetExtension(modfile) == ".pak" :
       modpakfile = modfile
     modfile = Path.Combine(modpath, modfile)
     if File.Exists(modfile) :
       File.Delete(modfile)
   File.Delete(modfilelist)
  
 #Create modlist.txt
 modlisttxt=Path.Combine(modpath, "modlist.txt")
 if(File.Exists(modlisttxt)) :
   lines = File.ReadAllLines(modlisttxt).Where(lambda l: not l.Contains(modpakfile)).ToArray()
   File.Delete(modlisttxt)
   with File.AppendText(modlisttxt) as fs :
     for line in lines :
       fs.WriteLine(line)
Operating System: Any
Description: Moves mod files.
Script Engine: IronPython
Event: After Workshop Content Updated
Ignore execution errors Unchecked
This is basically the install script but with the part that updates modlist.txt removed so we don't lose the mod order.
 import clr
 from System import String
 from System.IO import Directory, File, Path, SearchOption, DirectoryInfo
 
 import System
 clr.AddReference("System.Core")
 clr.ImportExtensions(System.Linq)
 
 modpath = Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods")
 Directory.CreateDirectory(modpath)
 
 #Save a list of the mod's files so we can delete them when uninstalling the mod. 
 modpakfile = "";
 modfilelist=Path.Combine(ThisService.RootDirectory, "ConanSandbox", "Mods", String.Format("{0}.txt", FileId))
 for file in Directory.GetFiles(InstallPath, "*", SearchOption.AllDirectories):
   modfile = Path.Combine(modpath, Path.GetFileName(file))
   if(File.Exists(modfile)) :
    File.Delete(modfile)
   File.Move(file, modfile)
   modfilename=Path.GetFileName(file)
   File.AppendAllText(modfilelist, String.Format("{0}\r", modfilename))
   if Path.GetExtension(modfilename) == ".pak" :
     modpakfile = modfilename

DayZ

Scripts thanks to TRUgaming

  • Create a variable named Mods
  • Add this to the command line: -mod=![Mods]
Operating System: Any
Description: After Workshop Content Installed
Script Engine: IronPython
Event: AfterWorkshopInstall
Ignore execution errors: Unchecked
 import clr
 from System import Array, String
 from System.IO import File, Path, Directory, SearchOption
 from System.Threading import Thread
 import re
 
 # Setup variables
 servertag="Server"
 mods=""
 y=[]
 ModName=""
 
 # Get commandline list of mods
 if ThisService.Variables.HasValue("Mods") :
   mods=ThisService.Variables["Mods"]
 else:
   raise Exception("Missing Mod information. Installation Failed!")
 
 # Check for FileTitle
 if len(FileTitle) != 0:
 # Check FileTitle for special character
   for i in range(len(FileTitle)):
     if bool(re.search('[a-zA-Z0-9 \]\[_\'+-]', FileTitle[i])) : 
       y.append(FileTitle[i])
     else:
       y.append("")
         
     ModName= "".join(str(x) for x in y)
     ModName= ModName.Replace("  "," ").Replace(" ","_")
     ModName= ModName.strip()
 else:  
    ModName = FileId
   
 if (mods.find(';') != -1) :
    ThisService.Variables["Mods"]=String.Format("@{0}", ModName) + ";" + mods   
 else :  
   if len(mods) > 0 :
      ThisService.Variables["Mods"]=String.Format("@{0}", ModName) + ";" + mods   
   else :
      ThisService.Variables["Mods"]=String.Format("@{0}", ModName) + mods   
       
 modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", ModName))
   
 # If mod exists delete the folder
 if Directory.Exists(modfolder) : 
    Directory.Delete(modfolder, True)
 
 # Move mod to root using renamed file name as folder name 
 Directory.Move(InstallPath, modfolder)
 
 # Setup keys path to to copy .bikey file root \keys folder
 modkeys=Path.Combine(modfolder, "keys")
 rootkeys=Path.Combine(ThisService.RootDirectory, "keys")
 
 # Check for \keys folder in root
 if not Directory.Exists(rootkeys) :
   Directory.CreateDirectory(rootkeys)
 
 # Check for mod \keys folder
 # Get list of all files on mods \keys folder
 # Copy file(s) from mods \keys folder to root \keys folder
 if Directory.Exists(modkeys) :
   for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
     keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
     if not File.Exists(keyfile) :
       File.Copy(file, keyfile)
 
 # Update command line
 ThisService.Save()
 ThisService.Configure()
Operating System: Any
Description: After Workshop Content Uninstalled
Script Engine: IronPython
Event: AfterWorkshopUninstall
Ignore execution errors: Unchecked
 import clr
 from System import Array, String
 from System.IO import Directory, File, Path, SearchOption, DirectoryInfo
 import re
 
 # Setup variables
 mods=""
 mlist=""
 lastmod=""
 ModName=""
 y=[]
 
 # Check for Variable
 # Get commandline mods list
 if ThisService.Variables.HasValue("Mods") :
   mods=ThisService.Variables["Mods"]
 else:
   raise Exception("Missing Mod information. Uninstall Failed!")
 
 # Check if the mod being uninstalled is the last mod or is the only mod in the commandline
 # Convert commandline modlist string to List
 mlist = mods.split('@')
 
 def is_last(alist,choice):
   if choice == alist[-1]:
     return True
   else:
     return False
   
 if len(FileTitle) != 0:
   for i in range(len(FileTitle)):
     if bool(re.search('[a-zA-Z0-9 \]\[_\'+-]', FileTitle[i])) : 
       y.append(FileTitle[i])
     else:
       y.append("")
         
     ModName= "".join(str(x) for x in y)
     ModName= ModName.Replace("  "," ").Replace(" ","_")
     ModName= ModName.strip()
 else:
   ModName = FileId
 
 # If mod to be uninstalled is the last mod set flag to true
 if is_last(mlist,ModName) == True :
    lastmod = "y" 
 else:
    lastmod = "n"
                    
 # Remove mod name from commandline variable
 # If the mod is the last mod in the list or the only mod in the list remove FileTitle and not FileTitle;, 
 #     attempting to remove FileTitle; will fail and the mod will not be removed from the commandline
 # If mod removal result; in only one mod being left make sure there is not trailing semi-colon
 if (mods.find(';') != -1) :
   if lastmod == "n" :
 # Multiple mods not last mod
      ThisService.Variables["Mods"]=mods.Replace(String.Format("@{0};", ModName), "")
   else :
 # Last Mod in list
      ThisService.Variables["Mods"]=mods.Replace(String.Format(";@{0}", ModName), "")
 else :  
 # Only Mod
   ThisService.Variables["Mods"]=mods.Replace(String.Format("@{0}", ModName), "")
   
 # Setup folders for removal
 modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", ModName))
 modkeys=Path.Combine(modfolder, "keys")
 rootkeys=Path.Combine(ThisService.RootDirectory, "keys")
 
 # Delete mod key files
 if Directory.Exists(modkeys) :
   for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
     keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
     File.Delete(keyfile)
 
 # Delete mod folder
 if Directory.Exists(modfolder) :
   Directory.Delete(modfolder, True)
 
 # Becuase some devs develope more tyhan one mod and use the same .bikey file
 # Parse the mod folders and make sire the .bikey files are in the \keys folder
 # If not copy the /bikey file
 dirinfo = DirectoryInfo(ThisService.RootDirectory)
 for i in  dirinfo.GetDirectories("@*") :
   modfolder=Path.Combine(ThisService.RootDirectory, i.Name)
   modkeys=Path.Combine(modfolder, "keys")
   if Directory.Exists(modkeys) :
     for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
       keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
       if not File.Exists(keyfile) :
         File.Copy(file, keyfile)
 
 # Update command line
 ThisService.Save()
 ThisService.Configure()
Operating System: Any
Description: After Workshop Content Updated
Script Engine: IronPython
Event: AfterWorkshopUpdate
Ignore execution errors: Unchecked
import clr
from System import Array, String
from System.IO import File, Path, Directory, SearchOption
from System.Threading import Thread
import re
 
# Setup variables
servertag="Server"
mods=""
y=[]
ModName=""
 
# Get commandline list of mods
if ThisService.Variables.HasValue("Mods") :
  mods=ThisService.Variables["Mods"]
else:
  raise Exception("Missing Mod information. Installation Failed!")
 
# Check for FileTitle
if len(FileTitle) != 0:
# Check FileTitle for special character
  for i in range(len(FileTitle)):
    if bool(re.search('[a-zA-Z0-9 \]\[_\'+-]', FileTitle[i])) : 
      y.append(FileTitle[i])
    else:
      y.append("")
         
    ModName= "".join(str(x) for x in y)
    ModName= ModName.Replace("  "," ").Replace(" ","_")
    ModName= ModName.strip()
else:  
   ModName = FileId
   
       
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("@{0}", ModName))
   
# If mod exists delete the folder
if Directory.Exists(modfolder) : 
   Directory.Delete(modfolder, True)
 
# Move mod to root using renamed file name as folder name 
Directory.Move(InstallPath, modfolder)
 
# Setup keys path to to copy .bikey file root \keys folder
modkeys=Path.Combine(modfolder, "keys")
rootkeys=Path.Combine(ThisService.RootDirectory, "keys")
 
# Check for \keys folder in root
if not Directory.Exists(rootkeys) :
  Directory.CreateDirectory(rootkeys)
 
# Check for mod \keys folder
# Get list of all files on mods \keys folder
# Copy file(s) from mods \keys folder to root \keys folder
if Directory.Exists(modkeys) :
  for file in Directory.GetFiles(modkeys, "*", SearchOption.AllDirectories):
    keyfile = Path.Combine(rootkeys, Path.GetFileName(file))
    if not File.Exists(keyfile) :
      File.Copy(file, keyfile)
 
# Update command line
ThisService.Save()
ThisService.Configure()

Garry's Mod

  • Updated 2021/05/23. Fixed errors when file already exists. Gave execute permissions to bin/gmad_linux.
Operating System: Any
Description: After Workshop Content Installed
Script Engine: IronPython
Event: AfterWorkshopInstall
Ignore execution errors: Unchecked
import clr
clr.AddReference("TCAdmin.SDK")
from System import Environment, PlatformID, String
from System.IO import Directory, File, Path, SearchOption
from System.Net import WebClient
from System.Diagnostics import Process
from TCAdmin.SDK.Misc import CompressionTools, Linux

addon=Path.Combine(ThisService.RootDirectory, "garrysmod", "addons", FileId.ToString())

urldownload=True;
#File downloaded from Steam not from URL. Update variable values.
if String.IsNullOrEmpty(FileUrl) :
  for file in Directory.GetFiles(InstallPath) :
    urldownload=False
    FileName=Path.GetFileName(file)
    FileNameNoPath=FileName
    FileNameSavePath=file

#Rename .gm to .gma
if FileNameSavePath.EndsWith(".gm") :
  if File.Exists(FileNameSavePath + "a") : 
    File.Delete(FileNameSavePath + "a")
  File.Move(FileNameSavePath, FileNameSavePath + "a")
  FileNameSavePath=FileNameSavePath + "a"
  FileName=FileName + "a"
  FileNameNoPath=FileNameNoPath + "a"

#If not .gma just move to addons
if not FileNameSavePath.EndsWith(".gma") :
  Directory.CreateDirectory(addon)
  if File.Exists(Path.Combine(addon, FileNameNoPath)) : 
    File.Delete(Path.Combine(addon, FileNameNoPath))
  File.Move(FileNameSavePath, Path.Combine(addon, FileNameNoPath))
  Script.Exit();

#Important: Only 7zip 9.20 is able to extract the file.
if Environment.OSVersion.Platform == PlatformID.Win32NT :
  _7zaurl="https://www.7-zip.org/a/7za920.zip"
  _7zazip=Path.Combine(TCAdminFolder, "Monitor", "Tools", "7za.zip")
  _7zapath=Path.Combine(TCAdminFolder, "Monitor", "Tools", "7za")
  _7zaexe = Path.Combine(_7zapath, "7za.exe")
  gmad = Path.Combine(ThisService.RootDirectory, "bin", "gmad.exe")
else :
  _7zaurl="http://cdnsource.tcadmin.net/games/__files/p7zip_9.20_x86_linux_bin.tar.bz2"
  _7zazip=Path.Combine(TCAdminFolder, "Monitor", "Tools", "7za.tar.bz2")
  _7zapath=Path.Combine(TCAdminFolder, "Monitor", "Tools", "7za")
  _7zaexe = Path.Combine(_7zapath, "p7zip_9.20", "bin", "7za")
  gmad = Path.Combine(ThisService.RootDirectory, "bin", "gmad_linux")
  Linux.MakeExecutable(gmad)
 
#Download 7zip if needed.
if not File.Exists(_7zaexe) :
  Script.WriteToConsole("Downloading 7zip...")
  wc = WebClient()
  wc.DownloadFile(_7zaurl, _7zazip);
  c=CompressionTools()
  c.Decompress(_7zazip, _7zapath)
  File.Delete(_7zazip)

#Only files downloaded from URL need to be extracted
if urldownload :
  p = Process()
  p.StartInfo.FileName=_7zaexe
  p.StartInfo.Arguments=String.Format('e -y -o"{0}" "{1}"', addon, FileNameSavePath)
  p.StartInfo.WorkingDirectory=ThisService.WorkingDirectory
  p.Start()
  p.WaitForExit()
  extractedfile=Path.Combine(addon, FileNameNoPath).Replace(".gma", String.Empty)
  realgma=Path.Combine(ThisService.RootDirectory, "garrysmod", "addons", String.Format("{0}.gma", FileId))
  if File.Exists(realgma) : 
    File.Delete(realgma)
  File.Move(extractedfile, realgma)
else :
  realgma=Path.Combine(Path.Combine(ThisService.RootDirectory, "garrysmod", "addons"), FileId.ToString() + ".gma")
  if File.Exists(realgma) : 
    File.Delete(realgma)
  File.Move(FileNameSavePath, realgma)
  
p = Process()
p.StartInfo.FileName=gmad
p.StartInfo.Arguments=String.Format('extract -file "{0}"', realgma)
p.StartInfo.WorkingDirectory=addon
p.Start()
p.WaitForExit()

File.Delete(realgma)
Operating System: Any
Description: After Workshop Content Uninstalled
Script Engine: IronPython
Event: AfterWorkshopUninstall
Ignore execution errors: Unchecked
 import clr
 from System.IO import Directory, Path
 
 addon = Path.Combine(ThisService.RootDirectory, "garrysmod", "addons", FileId.ToString())
 if Directory.Exists(addon) : 
   Directory.Delete(addon, True);

Space Engineers

Operating System: Any
Description: Adds mod info.
Script Engine: IronPython
Event: After Workshop Content Installed
Ignore execution errors Unchecked
 import clr
 clr.AddReference("System.Xml")
 
 from System import Exception
 from System import String
 from System.IO import Path
 from System.Xml import XmlDocument
 
 configpath=Path.Combine(ThisService.RootDirectory, "Sandbox.sbc")
 xmldoc = XmlDocument()
 xmldoc.Load(configpath)
 
 mods=xmldoc.SelectSingleNode("MyObjectBuilder_Checkpoint/Mods")
 if mods is None:
   raise Exception("Could not find Mods section")
   
 #Add only if mod does not exist
 modinfo=mods.SelectSingleNode(String.Format("ModItem[PublishedFileId='{0}']", FileId))
 if modinfo is None:
    modinfo = xmldoc.CreateElement("ModItem")
    modname = xmldoc.CreateElement("Name")
    modfileid = xmldoc.CreateElement("PublishedFileId")
    modname.InnerText=String.Format("{0}.sbm", FileId)
    modfileid.InnerText=FileId.ToString()
    modinfo.AppendChild(modname)
    modinfo.AppendChild(modfileid)
    mods.AppendChild(modinfo)
    xmldoc.Save(configpath)
Operating System: Any
Description: Adds mod info.
Script Engine: IronPython
Event: After Workshop Content Uninstalled
Ignore execution errors Unchecked
 import clr
 clr.AddReference("System.Xml")
 
 from System import Exception
 from System import String
 from System.IO import Path
 from System.Xml import XmlDocument
 
 configpath=Path.Combine(ThisService.RootDirectory, "Sandbox.sbc")
 xmldoc = XmlDocument()
 xmldoc.Load(configpath)
 
 mods=xmldoc.SelectSingleNode("MyObjectBuilder_Checkpoint/Mods")
 if mods is None:
   raise Exception("Could not find Mods section")
 
 #Remove mod and save file only if the mod exists in the file
 modinfo=mods.SelectSingleNode(String.Format("ModItem[PublishedFileId='{0}']", FileId))
 if not modinfo is None:
   mods.RemoveChild(modinfo)
   xmldoc.Save(configpath)

Known Issues

The remote server returned an error: (429) Too Many Requests.
This is a temporary error from the Steam api. Wait a few seconds and start the task again.
Retrieved from "https://help.tcadmin.com/index.php?title=Workshop_Browser&oldid=2546"