Source2Roblox Asset Uploading does NOT work:

Source2Roblox is a C# program that can upload Source Engine Maps / Models / Pictures and turn them into Roblox assets. Well one day the fork I was using from someone’s Source2Roblox AssetManager build ended up breaking at the beginning of this month. Someone please help, I’m willing to pay.


It seems to me like every year or half a year’s time. Source2Roblox stops working with a new API update. This is the current fork I’m using:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

using RobloxFiles;

using Source2Roblox.Forms;

using System.Threading;
using RobloxFiles.DataTypes;
using System.Net.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Source2Roblox.Upload
{
    public struct AssetUploadResponse
    {
        public bool Success;
        public string Message;

        public long? AssetId;
        public long? BackingAssetId;
    }

    public class AssetManager
    {
        private string XsrfToken;

        private const string uploadAsset = "https://apis.roblox.com/assets/v1/assets";
        private const string getOperation = "https://apis.roblox.com/assets/v1/operations";

        private const string apikey = "";
        private const string userId = "";

        private const string ROBLOX_COOKIE = "";

        private const string UploadDecal = "https://data.roblox.com/Data/Upload.ashx?json=1&type=Decal";
        private const string UploadMesh = "https://data.roblox.com/ide/publish/UploadNewMesh?";
        //private const string UploadAudio = "https://data.roblox.com/Data/Upload.ashx?json=1&type=Audio";

        private const string decalToImage = "https://f3xteam.com/bt/getDecalImageID/";

        public readonly string RootDir;
        public readonly string RbxAssetDir;

        private readonly SemaphoreSlim UploadSemaphore = new SemaphoreSlim(1, 4);
        private readonly Dictionary<string, bool> UploadConsent = new Dictionary<string, bool>();

        public AssetManager(string rootDir, string rbxAssetDir)
        {
            RootDir = rootDir;
            RbxAssetDir = rbxAssetDir;
        }

        public Func<Task<string>> BatchAssetId(string localPath)
        {
            if (localPath.EndsWith(".vtf"))
                localPath = localPath.Replace(".vtf", ".png");

            string filePath = Path.Combine(RootDir, localPath);
            byte[] content;

            if (File.Exists(filePath))
                content = File.ReadAllBytes(filePath);
            else
                content = Array.Empty<byte>();

            var info = new FileInfo(filePath);
            string dir = info.DirectoryName;

            string fileName = info.Name;
            string assetPath = filePath + ".asset";

            if (fileName.EndsWith(".mesh"))
            {
                if (fileName.StartsWith("cluster_") || fileName.StartsWith("disp_"))
                {
                    var assetHash = SharedString
                        .FromBuffer(content)
                        .ComputedKey
                        .Replace("+", "-")
                        .Replace("/", "_")
                        .TrimEnd('=');

                    string assetsDir = Path.Combine(dir, "assets");
                    assetPath = Path.Combine(assetsDir, assetHash + ".asset");

                    Directory.CreateDirectory(assetsDir);
                }
            }

            if (!File.Exists(assetPath) && !Program.LOCAL_ONLY)
            {
                if (!UploadConsent.TryGetValue(assetPath, out bool canUpload))
                {
                    var uploadForm = new Uploader(filePath);

                    if (!uploadForm.IsDisposed)
                        uploadForm.ShowDialog();

                    canUpload = uploadForm.Upload;
                    UploadConsent[assetPath] = canUpload;
                }

                if (canUpload)
                {
                    return () => Task.Run(async () =>
                    {
                        if (File.Exists(assetPath))
                        {
                            string assetId = File.ReadAllText(assetPath);
                            return $"rbxassetid://{assetId}";
                        }

                        var extension = info.Extension;
                        string name = info.Name.Replace(extension, "");

                        if (extension != "")
                        {
                            string uploadName = name;
                            string uploadDesc = "source to roblox";

                            for (int retry = 0; retry < 5; retry++)
                            {
                                try
                                {
                                    if (extension == ".png")
                                    {

                                        var requestContent = new MultipartFormDataContent
                                        {
                                            { new StringContent("{ \"assetType\": \"Decal\",  \"displayName\": \"" + uploadName + "\", \"description\": \"" + uploadDesc + "\", \"creationContext\": { \"creator\": { \"userId\": \"" + userId + "\" } } }"), "request" },
                                            { new StreamContent(File.OpenRead(filePath)), "fileContent", Path.GetFileName(filePath) }
                                        };

                                        var client = new HttpClient();
                                        client.DefaultRequestHeaders.Add("x-api-key", apikey);
                                        var response = await client.PostAsync(uploadAsset, requestContent);
                                        
                                        var responseContent = await response.Content.ReadAsStringAsync();
                                        var responseJson = JObject.Parse(responseContent);

                                        if (responseJson["message"] != null)
                                        {
                                            throw new Exception(responseJson["message"].ToString());
                                        }
                                        string pathValue = responseJson["path"].ToString();

                                        string[] pathParts = pathValue.Split(new string[] { "operations/" }, StringSplitOptions.None);

                                        string operationId = pathParts[1];

                                        string getOperationURL = $"{getOperation}/{operationId}";

                                        var response_2 = await client.GetAsync(getOperationURL);
                                        response_2.EnsureSuccessStatusCode();

                                        var response_2Content = await response_2.Content.ReadAsStringAsync();

                                        var response_2Json = JObject.Parse(response_2Content);

                                        var isDone = response_2Json["done"];

                                        int retryIsDone = 0;

                                        while (string.IsNullOrEmpty((string)isDone) && !string.IsNullOrEmpty((string)response_2Json["path"]) && retryIsDone < 5)
                                        {
                                            Console.WriteLine("Upload is not done waiting, attempt #" + retryIsDone);
                                            retryIsDone++;
                                            response_2 = await client.GetAsync(getOperationURL);

                                            response_2Content = await response_2.Content.ReadAsStringAsync();

                                            response_2Json = JObject.Parse(response_2Content);

                                            isDone = response_2Json["done"];
                                            await Task.Delay(3000);
                                        }

                                        string assetId = response_2Json["response"]["assetId"].ToString();
                                        if (string.IsNullOrEmpty(assetId))
                                        {
                                            throw new Exception("wtf lmao");
                                        }

                                        var decalToImageResponse = await client.GetAsync(decalToImage + assetId);

                                        string decalToImageId = await decalToImageResponse.Content.ReadAsStringAsync();


                                        if (string.IsNullOrEmpty(decalToImageId))
                                        {
                                            throw new Exception("NOT COOL man.");
                                        }

                                        File.WriteAllText(assetPath, decalToImageId);
                                        Console.WriteLine($"Uploaded {localPath}: {decalToImageId} -> {assetPath}");
                                        await Task.Delay(500);
                                        break;
                                    }
                                    else if (extension == ".mesh")
                                    {

                                        string postUrl = $"{UploadMesh}name={WebUtility.UrlEncode(uploadName)}&description={WebUtility.UrlEncode(uploadDesc)}";
                                        var request = WebRequest.Create(postUrl) as HttpWebRequest;

                                        request.Method = "POST";
                                        request.ContentType = "*/*";
                                        request.UserAgent = "RobloxStudio/WinInet";

                                        request.Headers.Set("Cookie", $".ROBLOSECURITY={ROBLOX_COOKIE}");
                                        request.Headers.Set("X-CSRF-TOKEN", XsrfToken);

                                        using (var writeStream = request.GetRequestStream())
                                        {
                                            writeStream.Write(content, 0, content.Length);
                                            writeStream.Close();
                                        }

                                        var webResponse = request.GetResponse() as HttpWebResponse;

                                        using (var readStream = webResponse.GetResponseStream())
                                        using (var reader = new StreamReader(readStream))
                                        {
                                            string response = reader.ReadToEnd();
                                            string asset = response.ToString();

                                            if (!long.TryParse(response, out long assetId))
                                            {
                                                var upload = JsonConvert.DeserializeObject<AssetUploadResponse>(response);
                                                if (!upload.Success)
                                                    throw new Exception(upload.Message);

                                                asset = upload.BackingAssetId.ToString();
                                            }

                                            File.WriteAllText(assetPath, asset);
                                            Console.WriteLine($"Uploaded: {asset} -> {assetPath}");

                                            await Task.Delay(500);
                                            break;
                                        }
                                    }
                                }
                                catch (Exception e)
                                {
                                    bool xsrf = false;

                                    if (e is WebException webEx)
                                    {
                                        var response = webEx.Response as HttpWebResponse;
                                        xsrf = response.StatusDescription.Contains("XSRF");

                                        if (xsrf)
                                            XsrfToken = response.Headers.Get("X-CSRF-TOKEN");

                                        response.Close();
                                    }

                                    if (xsrf)
                                        continue;

                                    Console.WriteLine(e.Message + ": " + e.StackTrace);
                                    if (!e.Message.ToLower().Contains("moderated") && !e.Message.ToLower().Contains("inappropriate"))
                                    {
                                        Console.WriteLine("Cooling down from possible asset overload...");
                                        await Task.Delay(30000);
                                        continue;
                                    }
                                    uploadName = "source to roblox";
                                }
                            }
                        }

                        if (File.Exists(assetPath))
                        {
                            string assetId = File.ReadAllText(assetPath);
                            return $"rbxassetid://{assetId}";
                        }

                        return $"{RbxAssetDir}/{localPath}";
                    });
                }
            }

            return () => Task.Run(() =>
            {
                if (File.Exists(assetPath) && !Program.LOCAL_ONLY)
                {
                    string assetId = File.ReadAllText(assetPath);
                    return $"rbxassetid://{assetId}";
                }

                return $"{RbxAssetDir}/{localPath}";
            });
        }

        public void BindAssetId(string localPath, List<Task> uploadPool, Instance target, string property)
        {
            if (string.IsNullOrEmpty(apikey) | string.IsNullOrEmpty(userId))
            {
                Console.WriteLine("[AssetUploader] apikey or userId variables are empty, open Util/AssetManager.cs to edit the file with your own");
                return;
            }
            Property prop = target.GetProperty(property) ?? throw new Exception($"Unknown property {property} in {target.ClassName}");

            // Prompt should be synchronously shown to user.
            var batchAssetId = BatchAssetId(localPath);

            // Hand off upload to asynchronous task.
            Task bind = Task.Run(async () =>
            {
                await UploadSemaphore.WaitAsync();
                var getAssetId = batchAssetId();

                string assetId = await getAssetId.ConfigureAwait(false);
                prop.Value = assetId;

                UploadSemaphore.Release();
            });

            // Add to the upload task pool.
            uploadPool.Add(bind);
        }
    }
}```
1 Like

Well, uhh, umm, he apparently nuked it i guess???
image

bro that version didnt even get published when I made this post that’s so dumb

Source2Roblox does not work anymore, it is outdated. Either you need to fix it yourself, else I would suggest Installing Plumber, which is a free to use blender addon to import Source Assets and maps. After that, you could just upload the map as an fbx file.

Why using Plumber? What about SourceIO? What are the differences between them? Which one I should be using? Can I import large maps to Roblox? Would that scale to studs properly?

I agree that’s dumb, but in my case the textures arent even loading, and half of the models are turned in the opposite of normal direction!


Whatever, if you know C#, then try fixing the thing by yourself, thats how i got the converter somehow working. Maybe we would find the solution one day, avoiding MaximumADHD. Are you using the Roblox Studio Mod Manager by the same guy who made the converter (MaximumADHD)? Try downgrading the studio using that tool, maybe youll find the perfect version eventually.

Plumber imports with textures, and I generally prefer it over SourceIO, mainly because you dont need to decompile stuff. Note that Plumber does not support Blender 4.1. yet, but you can download older blender versions here . Here is a screenshot of d1_trainstation_01 from Half-Life 2, imported into Blender using Plumber:

If you have any further questions, feel free to PM me or send a reply :slight_smile:

Ive tried Plumber before, but it REQUIRED decompiling:
I had to unpack addons which the map required, from .gma, and REPACK them to .vpk! It also required some setup, while SourceIO almost worked out of the box!
But on the other hand, ill reconsider Plumber, maybe because importing from .vmf is cool, i guess.
I hope it wouldnt rotate some entities, like here:

How i can export vmf map from blender to roblox WITH textures and materials?
After export to obj or fbx with Plumber, all textures and materials are missing on the map

Manually export the materials.

Actually, there is a way, but you would be mentally insane if you try it. (Oh yeah, I totally forgot to tell the full guide @IVIICHAELxx @trolled_empyrean)

First things first: get the converter to work and convert the map (yes I am serious, the full guide to how to get the thing to work I guess would be soon, but for now do it by yourself)
Then if nothing critical went wrong, then you would get the converted map. If the textures arent present then they are actually, they are just arent loaded for now because your converter forgot to change the file types. To fix that black and white mess - you can use the:

  1. Old reliable way: save the map into .rbxlx format, and wait untill it saves (its quite a long wait). Then close the map in Studio, open the just saved .rblxl map file in notepad and replace .vtf and .vmt with .png EVERYWHERE. Save the file, and reopen it in Studio. Wait a little bit, then you should see all the textures loaded. Save the map once again, but this time back into default .rblx format.
  2. The newer way: Just beg the Assistant to replace .vtf and .vmt with .png, if that works, save the map. If it doesnt, try the old reliable way with notepad.

Now you should have the converted map with loaded textures.
But we are still far from finish: every mesh and the texture is local to us, and we cant just publish the map file yet.
To fix that problem: Ask the Assistant to sort every mesh on the map, then upload every texture to Roblox manually. This can be a mess, so prepare the spreadsheet, and optionally an alt account.
After uploading the textures, you can either:

  1. Download and open the “Crowbar” tool, then use it to search for models required for our map, decompile, load into blender with either Blender Source Tools or Source IO addons, then export them into .obj files, and then to Roblox.
  2. Use SourceIO addon to load the map into Blender, or decompile the map into .vmf then use Plumber addon to load the map into Blender.
  3. If the meshes on the converted map are in the good shape then: Select a mesh in studio on the map, duplicate it and position it at the 0,0,0 coordinates, export it into .obj, import back to Roblox, and beg the Assistant to hopefully replace local meshes with yours.
  4. Recreate everything by yourself using parts. (the slowest way, but the most reliable and you wont get copyright striked by the holy Valve and someone else who made the map. Also you wont be blamed for stealing everything.)

After all of that, if everything looks good, and everything is no longer local, then congratulations - you are finally done, and you can finally publish your (if you recreated the assets instead of reimporting) game!

I hope you understood everything, and didnt became completely mentally insane :slight_smile:

1 Like

yo the plumber thing is working but too hard to deal with. its like its impossible. everytime you start importing it idk if it will also import like source2roblox with the lightings and the pbr but it is hard because roblox will also make errors for the import and stops everything and it loads longer and will break everything. have you upload a full map to roblox with plumber before? if so let me see