Enabling linked collections in Garry’s Mod dedicated server

Ah Garry’s Mod, a game that is both ingenious and infuriating. And it supports the Steam Workshop to add content.

The Workshop has a nice feature called “linked collections” which basically allow you to put all the maps in one collection and all the models into another. You can then add a third collection to link both together. In theory.

Practically this feature does not work in Garry’s Mod, the game processes all items returned from the Workshop API as downloadable content, regardless of whether that’s true or not (hint: if the filetype is 2, it’s a collection, not an addon!).

Being sick of waiting for Facepunch to fix this trivial problem, I figured I could simply rewire the request to another host which will do the pre-processing of collection data for me. The basic idea is that all the contents of collections linked to the primary collection will get “pushed” into the primary collection so Garry’s Mod will be fooled into downloading the contents of three collections “as one”.

Here’s how I did it (warning: Windows only!), I’m sure there are plenty of better ways to go about this:

  1. Install Fiddler, enable traffic capture and customize the rules for OnBeforeRequest by adding code like this:
    // Rewrite the Steam Workshop request for getting collection contents to target our emulator.
    if (oSession.HostnameIs("api.steampowered.com") && (oSession.PathAndQuery=="/ISteamRemoteStorage/GetCollectionDetails/v0001/")) {
      oSession.hostname="example.com";
      oSession.PathAndQuery="/steam_collections.php";
    }
  2. Create a new PHP script named steam_collections.php on your webserver example.com and edit $process_collection to fit your needs:
    <?php
    
    // Prepare the output header!
    header('Content-Type: text/json');
    
    // Only this collection will be processed, all other collections are passed through.
    $process_collection = '123456789';
    
    $api_url = "//api.steampowered.com/ISteamRemoteStorage/GetCollectionDetails/v0001/";
    
    // These values will be delivered by srcds's POST request.
    $api_key = $_POST['key'];
    $primary_collection = $_POST['publishedfileids'][0];
    $collectioncount = $_POST['collectioncount'];
    $format = $_POST['format'];
    
    // Must be global so every collection can access it.
    $sortorder = 1;
    
    function AddToPrimaryCollection(&$target_collection, $keys_to_add)
    {
     foreach($keys_to_add as &$key)
     {
     $target_collection[] = $key;
     }
    }
    
    function GetCollectionDetails($collection_id, $is_primary_collection = false, $process_children = false)
    {
     global $api_url;
     global $api_key;
     global $collectioncount;
     global $format;
     global $sortorder;
    
     $final_data = array();
    
     $post_fields = array(
     'collectioncount' => $collectioncount,
     'publishedfileids[0]' => $collection_id,
     'key' => $api_key,
     'format' => $format
     );
    
     $post_options = array(
     'http' => array(
     'header' => "Content-type: application/x-www-form-urlencoded\r\n",
     'method' => 'POST',
     'content' => http_build_query($post_fields),
     'timeout' => 120
     ),
     );
    
     $request_context = stream_context_create($post_options);
     $request_result = file_get_contents($api_url, false, $request_context);
     $json_data = json_decode($request_result, true);
    
     if($process_children)
     {
     if ($is_primary_collection)
     {
    
     foreach ($json_data['response']['collectiondetails'][0]['children'] as $key => &$collection_item) {
     if ($collection_item['filetype'] == '2')
     {
     // Grab the subcollection contents and add them to the mix list
     $sub_collection = GetCollectionDetails($collection_item['publishedfileid'], false, false);
     AddToPrimaryCollection($final_data, $sub_collection['response']['collectiondetails'][0]['children']);
    
     // Get rid of the collection reference
     unset($json_data['response']['collectiondetails'][0]['children'][$key]);
     }
     }
    
     // Now mix the aggregated list of all subcollections with the primary collection
     AddToPrimaryCollection($final_data, $json_data['response']['collectiondetails'][0]['children']);
     $json_data['response']['collectiondetails'][0]['children'] = $final_data;
     }
     
     // When in the primary collection, return the merged data array.
     if ($is_primary_collection)
     {
     foreach ($json_data['response']['collectiondetails'][0]['children'] as $key => &$collection_item)
     {
     $collection_item['sortorder'] = $sortorder;
     $sortorder += 1;
     }
     }
     }
    
     return $json_data;
    }
    
    if($primary_collection == $process_collection)
     // It's our target collection with subcollections, process it!
     $result = GetCollectionDetails($primary_collection, true, true);
    else
     // It's something else... don't bother!
     $result = GetCollectionDetails($primary_collection, true, false);
    
    // Now encode the data back to json and let srcds do it's thing...
    echo json_encode($result);
    
    ?>
  3. Launch srcds with the +host_workshop_collection 123456789 parameter and watch the magic happen. The start might take a little longer than usually.

It would be really nice if this would finally get fixed, it has been reported ages ago.

Published by

tsukasa

The fool's herald.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.