Workshop Browser

The Steam Workshop browser allows your users to download and install mods and maps from Steam with a single click.

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 installed.
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.

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

These scripts require Python 2.7 installed in the default location. Update the paths as needed.

Operating System: Any
Description: Extract .z, copy to ShooterGame/Content/Mods and update GameUserSettings.ini
Script Engine: IronPython
Event: After Workshop Content Installed
Ignore execution errors Unchecked
import clr
clr.AddReference("INIFileParser")

from System.IO import Directory, File, Path, SearchOption
from System import Environment, PlatformID, String
from IniParser import FileIniDataParser
from IniParser.Model import IniData

import sys
if Environment.OSVersion.Platform == PlatformID.Win32NT :
 sys.path.append("C:\\Python27\\Lib")
else :
 sys.path.append("/usr/lib/python2.7/Lib")  


########################################
# 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):
    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)                

#######################################################################
# 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):
            Script.WriteToConsole("[x] Failed to locate mod.info. Cannot Continue.  Aborting")
            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):
            Script.WriteToConsole("[x] Failed To Locate modmeta.info. Cannot continue without it.  Aborting")
            return False

        with open(mod_meta, "rb") as f:

            total_pairs = struct.unpack('i', f.read(4))[0]

            for i in range(total_pairs):

                key, value = "", ""

                key_bytes = struct.unpack('i', f.read(4))[0]
                key_flag = False
                if key_bytes < 0:
                    key_flag = True
                    key_bytes -= 1

                if not key_flag and key_bytes > 0:

                    raw = f.read(key_bytes)
                    key = raw[:-1].decode()

                value_bytes = struct.unpack('i', f.read(4))[0]
                value_flag = False
                if value_bytes < 0:
                    value_flag = True
                    value_bytes -= 1

                if not value_flag and value_bytes > 0:
                    raw = f.read(value_bytes)
                    value = raw[:-1].decode()

                # TODO This is a potential issue if there is a key but no value
                if key and value:
                    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)
            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 )
# Extract and delete all .z files
for zfile in Directory.GetFiles(noeditor, "*.z", SearchOption.AllDirectories):
 file=Path.Combine(Path.GetDirectoryName(zfile), Path.GetFileNameWithoutExtension(zfile))
 z_unpack(zfile, file)
 Script.WriteToConsole("Extracted " + file)
 File.Delete(zfile)
 File.Delete(zfile + ".uncompressed_size")

# Move folder to correct location
modfolder=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}", FileId))
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))
ini = FileIniDataParser()
data = ini.ReadFile(inifile)
data["ServerSettings"]["ActiveMods"] = FileIds
ini.WriteFile(inifile, data)

#Create .mod
parse_base_info(FileId.ToString())
parse_meta_data(FileId.ToString())
create_mod_file(FileId.ToString())
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
clr.AddReference("INIFileParser")

from System.IO import Path, Directory, File
from System import Environment, PlatformID, String
from IniParser import FileIniDataParser
from IniParser.Model import IniData

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

#Delete .mod
modfile=Path.Combine(ThisService.RootDirectory, String.Format("ShooterGame/Content/Mods/{0}.mod", FileId))
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))
ini = FileIniDataParser()
data = ini.ReadFile(inifile)
data["ServerSettings"]["ActiveMods"] = FileIds
ini.WriteFile(inifile, data)

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))
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))
    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)

Conan Exiles

Operating System: Any
Description: Moves mod files.
Script Engine: IronPython
Event: After Workshop Content Installed
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")
Directory.CreateDirectory(modpath)

#Save a list of the mod's files so we can delete them when uninstalling the mod. 
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)
  File.AppendAllText(modfilelist, String.Format("{0}\r", Path.GetFileName(file)))

#Create modlist.txt
modlisttxt=Path.Combine(modpath, "modlist.txt")
if(File.Exists(modlisttxt)) :
  File.Delete(modlisttxt)

modpathinfo = DirectoryInfo(modpath)
pakfiles=modpathinfo.GetFiles("*.pak", SearchOption.AllDirectories).OrderBy(lambda f: f.CreationTime).ToArray()	
for file in pakfiles :
  File.AppendAllText(modlisttxt, String.Format("{0}\r", Path.GetFileName(file.FullName)))

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=1788"