public static OuterJson GenerateOuterJSON(VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionParameters expparams, string id, string name)
    {
        OuterJson oj = new OuterJson();

        oj.id   = id;
        oj.name = name;
        const string ADDRESS_PREFIX = "/avatar/parameters/";
        int          nonempty;

        if (expparams == null)
        {
            expparams            = ScriptableObject.CreateInstance <VRCExpressionParameters>();
            expparams.parameters = new VRCExpressionParameters.Parameter[] {
                new VRCExpressionParameters.Parameter {
                    defaultValue = 0,
                    saved        = false,
                    name         = "VRCEmote",
                    valueType    = VRCExpressionParameters.ValueType.Int
                },
                new VRCExpressionParameters.Parameter {
                    defaultValue = 0,
                    saved        = false,
                    name         = "VRCFaceBlendH",
                    valueType    = VRCExpressionParameters.ValueType.Float
                },
                new VRCExpressionParameters.Parameter {
                    defaultValue = 0,
                    saved        = false,
                    name         = "VRCFaceBlendV",
                    valueType    = VRCExpressionParameters.ValueType.Float
                }
            };
        }
        nonempty = expparams.parameters.Length;
        foreach (var p in expparams.parameters)
        {
            if (p.name.Length == 0)
            {
                nonempty--;
            }
        }
        oj.parameters = new InnerJson[OSC_BUILTIN_PARAMETERS.Length + nonempty];
        int idx = 0;

        // VRC writes these in reverse order. No idea why.
        foreach (var p in expparams.parameters)
        {
            if (p.name.Length != 0)
            {
                oj.parameters[nonempty - idx - 1] = new InnerJson {
                    name  = p.name,
                    input = new InputOutputPath {
                        address = ADDRESS_PREFIX + p.name,
                        type    = (p.valueType == VRCExpressionParameters.ValueType.Int ? "Int" :
                                   (p.valueType == VRCExpressionParameters.ValueType.Float ? "Float" : "Bool"))
                    },
                    output = new InputOutputPath {
                        address = ADDRESS_PREFIX + p.name,
                        type    = (p.valueType == VRCExpressionParameters.ValueType.Int ? "Int" :
                                   (p.valueType == VRCExpressionParameters.ValueType.Float ? "Float" : "Bool"))
                    }
                };
                idx++;
            }
        }
        for (int i = 0; i < OSC_BUILTIN_PARAMETERS.Length; i++)
        {
            var bname = OSC_BUILTIN_PARAMETERS[i][0];
            var btype = OSC_BUILTIN_PARAMETERS[i][1];
            oj.parameters[idx] = new InnerJson {
                name   = bname,
                output = new InputOutputPath {
                    address = ADDRESS_PREFIX + bname,
                    type    = btype
                }
            };
            idx++;
        }
        return(oj);
    }
    public void EnsureOSCJSONConfig(VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionParameters expparams, string avatarid, string name)
    {
        try {
            string localLowPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            if (localLowPath.EndsWith("Local"))
            {
                localLowPath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(localLowPath), "LocalLow");
            }
            string userid     = null;
            string vrcOSCPath = System.IO.Path.Combine(localLowPath, "VRChat", "vrchat", "OSC");

            System.Type apiusertype = System.Type.GetType("VRC.Core.APIUser, VRCCore-Editor");
            if (apiusertype != null)
            {
                var idprop = apiusertype.GetProperty("id", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
                var prop   = apiusertype.GetProperty("CurrentUser", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
                // Debug.Log("idprop " + idprop);
                if (idprop != null && prop != null)
                {
                    var apiuserinst = prop.GetValue(null);
                    if (apiuserinst != null)
                    {
                        // Debug.Log("apiuser " + apiuserinst);
                        userid = (string)idprop.GetValue(apiuserinst);
                    }
                }
            }
            try {
                System.IO.Directory.CreateDirectory(vrcOSCPath);
            } catch (System.IO.IOException) {
            }
            if (userid == null || userid.Length == 0)
            {
                // do not have a known user account.
                // find the most recent user folder.
                DateTime dt = DateTime.MinValue;
                // Debug.Log("lets-a look at " + vrcOSCPath);
                foreach (string file in System.IO.Directory.GetDirectories(vrcOSCPath, "*", System.IO.SearchOption.TopDirectoryOnly))
                {
                    // Debug.Log("enumerate a file " + file);
                    DateTime thisdt = System.IO.File.GetLastWriteTime(file);
                    if (thisdt > dt)
                    {
                        userid = System.IO.Path.GetFileName(file);
                        dt     = thisdt;
                    }
                }
            }
            if (userid == null || userid.Length == 0)
            {
                OSCAvatarID   = "not_logged_in";
                OSCFilePath   = "No User folder was found. Please play VRC or login.";
                OSCJsonConfig = GenerateOuterJSON(expparams, "not_logged_in", name);
                return;
            }
            string avatarDirectory = System.IO.Path.Combine(vrcOSCPath, userid, "Avatars");
            try {
                System.IO.Directory.CreateDirectory(avatarDirectory);
            } catch (System.IO.IOException) {
            }
            if (avatarid != null && UseRealPipelineIdJSONFile)
            {
                OSCAvatarID = avatarid; // json file already exists: let's use it.
                OSCFilePath = System.IO.Path.Combine(avatarDirectory, avatarid + ".json");
            }
            else
            {
                avatarid    = AVTR_EMULATOR_PREFIX + (whichtest ? "A" : "B");
                OSCFilePath = System.IO.Path.Combine(avatarDirectory, avatarid + ".json");
                whichtest   = !whichtest;
                OSCAvatarID = avatarid;
                WriteJSON(OSCFilePath, GenerateOuterJSON(expparams, avatarid, name));
            }
            if (System.IO.File.Exists(OSCFilePath))
            {
                try {
                    OSCJsonConfig = ReadJSON(OSCFilePath);
                } catch (Exception e) {
                    Debug.LogException(e);
                    Debug.Log("File failed to load. Generating new JSON for " + OSCAvatarID);
                    OSCJsonConfig = GenerateOuterJSON(expparams, OSCAvatarID, name);
                }
            }
            else
            {
                Debug.Log("File does not exist. Generating new JSON for " + OSCAvatarID);
                OSCJsonConfig = GenerateOuterJSON(expparams, OSCAvatarID, name);
            }
        } catch (Exception e) {
            Debug.LogException(e);
            OSCAvatarID = "exception_generating_json";
            OSCFilePath = e.Message;
            Debug.Log("Unable to determine Avatar ID or JSON file path. Generating config.");
            OSCJsonConfig = GenerateOuterJSON(expparams, "exception_generating_json", name);
        }
    }