diff --git a/.gitignore b/.gitignore index a297d11..cb80522 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ ExportedObj/ *.pdb.meta *.mdb.meta +# Unity3D temp meta files +[Tt]emp.meta + # Unity3D generated file on crash reports sysinfo.txt diff --git a/Component.meta b/Component.meta new file mode 100644 index 0000000..425ddd6 --- /dev/null +++ b/Component.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6070dc9ff4364a4a96ab4aa47b0d90cc +timeCreated: 1727374867 \ No newline at end of file diff --git a/Component/MemoryOptimizerComponent.cs b/Component/MemoryOptimizerComponent.cs new file mode 100644 index 0000000..c634841 --- /dev/null +++ b/Component/MemoryOptimizerComponent.cs @@ -0,0 +1,429 @@ +#if UNITY_EDITOR + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JeTeeS.MemoryOptimizer.Helper; +using JeTeeS.MemoryOptimizer.Shared; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; +using VRC.SDKBase; +using static JeTeeS.MemoryOptimizer.Shared.MemoryOptimizerConstants; + +namespace JeTeeS.MemoryOptimizer +{ + [Serializable] + [DisallowMultipleComponent] + [RequireComponent(typeof(VRCAvatarDescriptor))] + internal class MemoryOptimizerComponent : MonoBehaviour, IEditorOnly, ISerializationCallbackReceiver + { + [Serializable] + internal class ParameterConfig : MemoryOptimizerListData + { + public string info = string.Empty; + + /** + * We set this flag for parameters that have lost their connection, but may re-gain it + */ + public bool isOrphanParameter = false; + + [SerializeField] private int hashCode = -1; + + public ParameterConfig(VRCExpressionParameters.Parameter parameter) : base(parameter, false, false) + { + CalculateHashCode(); + } + + public ParameterConfig(string parameterName, VRCExpressionParameters.ValueType parameterType) : base(new VRCExpressionParameters.Parameter() + { + name = parameterName, + valueType = parameterType, + networkSynced = true + }, false, false) + { + CalculateHashCode(); + } + + internal void CalculateHashCode() + { + hashCode = $"name:{this.param.name}-type:{paramTypes[(int)this.param.valueType]}".GetHashCode(); + } + + public override int GetHashCode() + { + if (hashCode == -1) + { + CalculateHashCode(); + } + + return hashCode; + } + + public override bool Equals(object obj) + { + if (obj is ParameterConfig o) + { + return o.GetHashCode() == GetHashCode(); + } + + return base.Equals(obj); + } + + public MemoryOptimizerListData CopyBase(VRCExpressionParameters.Parameter inherit = null) + { + return new MemoryOptimizerListData(inherit ?? new VRCExpressionParameters.Parameter() + { + name = param.name, + valueType = param.valueType, + networkSynced = true + }, selected, willBeOptimized); + } + } + + [Serializable] + internal struct ComponentIssue + { + public string message; + public int level; + + [SerializeField] private int hashCode; + + public static implicit operator ComponentIssue((string, int) data) + { + return new ComponentIssue() + { + message = data.Item1, + level = data.Item2, + hashCode = $"{data.Item1}-level:{data.Item2}".GetHashCode() + }; + } + + public override bool Equals(object obj) + { + if (obj is ComponentIssue o) + { + return o.hashCode == hashCode; + } + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return hashCode; + } + } + + [NonSerialized] private VRCAvatarDescriptor _vrcAvatarDescriptor; + + public bool changeDetection = false; + public int syncSteps = 2; + public float stepDelay = 0.2f; + public int wdOption = 0; + + [SerializeField] internal List componentIssues = new(16); + [SerializeField] internal List parameterConfigs = new(1024); + + [NonSerialized] private Dictionary lookupParameterConfigs = new(1024); + + public void OnBeforeSerialize() + { } + + public void OnAfterDeserialize() + { + foreach (var parameterConfig in parameterConfigs) + { + lookupParameterConfigs.Add(parameterConfig.GetHashCode(), parameterConfig); + } + } + + internal void LoadParameters() + { + componentIssues = new(0); + + foreach (var parameterConfig in parameterConfigs) + { + parameterConfig.isOrphanParameter = true; + } + + // get descriptor + _vrcAvatarDescriptor ??= gameObject.GetComponent(); + + var descriptorParameters = (_vrcAvatarDescriptor?.expressionParameters?.parameters ?? Array.Empty()).Where(p => p.networkSynced).ToList(); + + if (MemoryOptimizerHelper.IsSystemInstalled(_vrcAvatarDescriptor)) + { + componentIssues.Add(("MemoryOptimizer is already installed in the current FX-Layer.", 3 /* Error */)); + } + + // collect all descriptor parameters that are synced + foreach (var savedParameterConfiguration in descriptorParameters.Select(p => new ParameterConfig(p) { info = "From Avatar Descriptor" })) + { + AddParameterConfig(savedParameterConfiguration); + } + +#if MemoryOptimizer_VRCFury_IsInstalled + // since all VRCFury components are IEditorOnly, we can find them like this + // there is no better way as VRCFury has decided to mark their classes as internal only + List vrcfComponents = gameObject.GetComponentsInChildren(true) + .Where(x => x.GetType().ToString().Contains("VRCFury")) + .ToList(); + + // collect all VRCFury parameters + // this is where the real fun begins... + if (vrcfComponents.Any()) + { + foreach (IEditorOnly vrcfComponent in vrcfComponents) + { + // get type name + var vrcfType = vrcfComponent.GetType().ToString(); + + // get containing object + var vrcfGameObject = ((Component)vrcfComponent).gameObject; + + // VRCFuryHapticSocket components are handled differently + if (vrcfType.Contains("VRCFuryHapticSocket")) + { + var hapticName = string.Empty; + if (vrcfComponent.GetFieldValue("name") is string _name && !string.IsNullOrEmpty(_name)) + { + hapticName = _name; + } + + // VRCFuryHapticSocket generate a parameter called _stealth + AddParameterConfig(new ParameterConfig("VF##_stealth", VRCExpressionParameters.ValueType.Bool)); + + var hapticAddMenuItem = vrcfComponent.GetFieldValue("addMenuItem") is true; + + // ignore _Unknown or no menu item + if (string.IsNullOrEmpty(hapticName) || !hapticAddMenuItem) + { + continue; + } + + AddParameterConfig(new ParameterConfig($"VF##_{hapticName}", VRCExpressionParameters.ValueType.Bool)); + + // we can go to the next component + continue; + } + // VRCFuryHapticPlug don't generate a parameter because ??? + else if (vrcfType.Contains("VRCFuryHapticPlug")) + { + continue; + } + + // the normal components are handled via VRCFury.content + // extract the content field from VRCFury, this is where the actual "component" data can be found + object contentValue = null; + try + { + contentValue = vrcfComponent.GetFieldValue("content"); + } + catch + { + continue; + } + + // not all components have content + if (string.IsNullOrEmpty(contentValue?.GetType()?.ToString())) + { + continue; + } + + // from here on out it depends on what we have + var contentType = contentValue.GetType().ToString(); + + // Notes: + // VRCFury generates parameters with the following format: VF\d+_{name} + // since we have no access to the number unless we build the avatar, we just display VF##_{name} and map it correctly later + + if (contentType.Contains("UnlimitedParameters")) + { + componentIssues.Add(("VRCFury 'Parameter Compressor' (formerly 'Unlimited Parameters') was found on Avatar.\nOur system IS NOT COMPATIBLE with this.", 3 /* Error */)); + } + // Toggle + else if (contentType.Contains("Toggle")) + { + // Toggles are rather simple, they can be in any of these configurations: + // 1. being a normal toggle (auto-generated bool parameter) + // 2. using a global parameter (defined bool parameter) + // 3. being a slider (auto-generated float parameter) + // 4. being a slider using a global parameter (defined float parameter) + + var toggleName = "VF##_"; + if (contentValue.GetFieldValue("name") is string _toggleName && !string.IsNullOrEmpty(_toggleName)) + { + toggleName = _toggleName; + } + + var isSlider = contentValue.GetFieldValue("slider") is true; + + var useGlobalParameter = contentValue.GetFieldValue("useGlobalParam") is true; + var globalParameter = string.Empty; + if (useGlobalParameter && contentValue.GetFieldValue("globalParam") is string _globalParameter && !string.IsNullOrEmpty(_globalParameter)) + { + globalParameter = _globalParameter; + } + else + { + useGlobalParameter = false; + } + + // if the toggle is a non described empty one and there isn't a global parameter + // that is being targeted, we skip this as we cannot correctly map it back + if (toggleName.Equals("VF##_") && !useGlobalParameter) + { + continue; + } + + ParameterConfig parameterConfig = new ParameterConfig(useGlobalParameter ? globalParameter : $"VF##_{toggleName}", isSlider ? VRCExpressionParameters.ValueType.Float : VRCExpressionParameters.ValueType.Bool) + { + info = $"From Toggle: {toggleName} on {gameObject.name}" + }; + + AddParameterConfig(parameterConfig); + } + // Full Controller + else if (contentType.Contains("FullController")) + { + // get global parameters + List globalParameters = new List(0); + if (contentValue.GetFieldValue("globalParams") is List _globalParameters) + { + globalParameters = _globalParameters; + } + + var containsStar = globalParameters.Contains("*"); + var isGoGo = false; + + // get the parameter list + if (contentValue.GetFieldValue("prms") is IEnumerable _parametersList) + { + var generatedConfigs = new Dictionary(1024); + // remap to actual expression parameters + foreach (var slot in _parametersList) + { + // the parameters are a field which is wrapped again + if (slot.GetFieldValue("parameters")?.GetFieldValue("objRef") is VRCExpressionParameters cast) + { + // add parameters + foreach (var parameter in cast.parameters) + { + // so we can fix them later + if (parameter.name.Equals("Go/Locomotion")) + { + isGoGo = true; + } + + // ignore un-synced + if (!parameter.networkSynced) + { + continue; + } + + var parameterConfig = new ParameterConfig((containsStar && !globalParameters.Contains($"!{parameter.name}")) || globalParameters.Contains(parameter.name) ? parameter.name : $"VF##_{parameter.name}", parameter.valueType) + { + info = $"From FullController on {vrcfGameObject.name}" + }; + + // avoid duplicates + generatedConfigs.TryAdd(parameterConfig.GetHashCode(), parameterConfig); + } + } + } + + // add to component + foreach (var parameterConfig in generatedConfigs.Select(generatedConfig => generatedConfig.Value)) + { + // since GoGo has an issue with their global parameters using 'a *' in most versions, we remap them ourselves correctly + if (isGoGo) + { + var fixedName = parameterConfig.param.name; + if (fixedName.StartsWith("VF##_")) + { + fixedName = fixedName["VF##_".Length..]; + } + + if (!fixedName.StartsWith("Go/")) + { + fixedName = "Go/" + fixedName; + } + + parameterConfig.param = new VRCExpressionParameters.Parameter() + { + name = fixedName, + valueType = parameterConfig.param.valueType, + networkSynced = parameterConfig.param.networkSynced + }; + + parameterConfig.CalculateHashCode(); + } + + AddParameterConfig(parameterConfig); + } + } + } + } + } +#endif + if (parameterConfigs.Count <= 0) + { + componentIssues.Add(("This avatar has no loaded parameters.", 2 /* Warning */)); + } + } + + internal void ClearOrphans() + { + foreach (var parameterConfig in parameterConfigs.ToArray()) + { + if (parameterConfig.isOrphanParameter) + { + parameterConfigs.Remove(parameterConfig); + lookupParameterConfigs.Remove(parameterConfig.GetHashCode()); + } + } + } + + private void Reset() + { + parameterConfigs = new(1024); + lookupParameterConfigs = new Dictionary(1024); + + LoadParameters(); + } + + private bool AddParameterConfig(ParameterConfig config) + { + if (AnimatorExclusions.Contains(config.param.name)) + { + return false; + } + + if (lookupParameterConfigs.TryGetValue(config.GetHashCode(), out var stored)) + { + stored.isOrphanParameter = false; + } + else + { + if (lookupParameterConfigs.TryAdd(config.GetHashCode(), config)) + { + parameterConfigs.Add(config); + } + } + + return true; + } + + private void AddIssue(ComponentIssue issue) + { + if (!componentIssues.Contains(issue)) + { + componentIssues.Add(issue); + } + } + } +} + +#endif diff --git a/Component/MemoryOptimizerComponent.cs.meta b/Component/MemoryOptimizerComponent.cs.meta new file mode 100644 index 0000000..7303e03 --- /dev/null +++ b/Component/MemoryOptimizerComponent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e86612edceff463ab4d3a82c82845b35 +timeCreated: 1727373554 \ No newline at end of file diff --git a/Component/_InternalsVisibleTo.cs b/Component/_InternalsVisibleTo.cs new file mode 100644 index 0000000..194b3ff --- /dev/null +++ b/Component/_InternalsVisibleTo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer.Editor")] +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer.Pipeline")] diff --git a/Component/_InternalsVisibleTo.cs.meta b/Component/_InternalsVisibleTo.cs.meta new file mode 100644 index 0000000..425f165 --- /dev/null +++ b/Component/_InternalsVisibleTo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6888c1db769a42fb82aa0cd76a939ffd +timeCreated: 1727468151 \ No newline at end of file diff --git a/Component/dev.jetees.memoryoptimizer.Component.asmdef b/Component/dev.jetees.memoryoptimizer.Component.asmdef new file mode 100644 index 0000000..03c1b1d --- /dev/null +++ b/Component/dev.jetees.memoryoptimizer.Component.asmdef @@ -0,0 +1,27 @@ +{ + "name": "dev.jetees.memoryoptimizer.Component", + "rootNamespace": "", + "references": [ + "VRC.SDK3A", + "VRC.SDK3A.Editor", + "VRC.SDKBase", + "VRC.SDKBase.Editor", + "dev.jetees.memoryoptimizer.Helper", + "dev.jetees.memoryoptimizer.Shared" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.vrcfury.vrcfury", + "expression": "0.0.0", + "define": "MemoryOptimizer_VRCFury_IsInstalled" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Component/dev.jetees.memoryoptimizer.Component.asmdef.meta b/Component/dev.jetees.memoryoptimizer.Component.asmdef.meta new file mode 100644 index 0000000..5342ba1 --- /dev/null +++ b/Component/dev.jetees.memoryoptimizer.Component.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c2651830b02b4624a7baea543f362d4b +timeCreated: 1727375242 \ No newline at end of file diff --git a/Editor/MemoryOptimizerComponentEditor.cs b/Editor/MemoryOptimizerComponentEditor.cs new file mode 100644 index 0000000..665c013 --- /dev/null +++ b/Editor/MemoryOptimizerComponentEditor.cs @@ -0,0 +1,133 @@ +using System.Linq; +using JeTeeS.MemoryOptimizer.Patcher; +using JeTeeS.MemoryOptimizer.Pipeline; +using UnityEditor; +using UnityEngine; +using static JeTeeS.MemoryOptimizer.Shared.MemoryOptimizerConstants; + +namespace JeTeeS.MemoryOptimizer +{ + [CustomEditor(typeof(MemoryOptimizerComponent))] + internal class MemoryOptimizerComponentEditor : Editor + { + private MemoryOptimizerComponent _component; + + private void Awake() + { + _component ??= (MemoryOptimizerComponent)target; + + _component.LoadParameters(); + } + + public override void OnInspectorGUI() + { + bool hasErrors = _component.componentIssues.Any(x => x.level >= 3); + + if (_component.componentIssues.Any()) + { + using (new MemoryOptimizerWindow.SqueezeScope((0, 0, MemoryOptimizerWindow.SqueezeScope.SqueezeScopeType.Horizontal, EditorStyles.helpBox))) + { + GUILayout.Label("Problems:"); + } + + foreach (var issue in _component.componentIssues) + { + var type = issue.level switch + { + 0 => MessageType.None, + 1 => MessageType.Info, + 2 => MessageType.Warning, + 3 => MessageType.Error, + _ => MessageType.None + }; + + EditorGUILayout.HelpBox(issue.message, type); + } + + GUILayout.Space(5); + } + + using (new MemoryOptimizerWindow.SqueezeScope((0, 0, MemoryOptimizerWindow.SqueezeScope.SqueezeScopeType.Horizontal, EditorStyles.helpBox))) + { + GUI.enabled = !hasErrors; + + if (GUILayout.Button("Configure")) + { + _component.LoadParameters(); + + MemoryOptimizerWindow.ShowWindowInternal(_component); + } + + GUI.enabled = true; + } + + var foldoutState = EditorGUILayout.Foldout(EditorPrefs.GetBool(EditorKeyInspectComponent), "Component Configuration"); + EditorPrefs.SetBool(EditorKeyInspectComponent, foldoutState); + if (foldoutState) + { + GUI.enabled = false; + + using (new MemoryOptimizerWindow.SqueezeScope((0, 0, MemoryOptimizerWindow.SqueezeScope.SqueezeScopeType.Horizontal, EditorStyles.helpBox))) + { + GUILayout.Label("Write Defaults:"); + GUILayout.Label($"{wdOptions[_component.wdOption]}", GUILayout.Width(100)); + } + + using (new MemoryOptimizerWindow.SqueezeScope((0, 0, MemoryOptimizerWindow.SqueezeScope.SqueezeScopeType.Horizontal, EditorStyles.helpBox))) + { + GUILayout.Label("Change Detection"); + + GUI.backgroundColor = _component.changeDetection ? Color.green : Color.red; + GUILayout.Button(_component.changeDetection ? "On" : "Off", GUILayout.Width(100)); + GUI.backgroundColor = Color.white; + } + + using (new MemoryOptimizerWindow.SqueezeScope((0, 0, MemoryOptimizerWindow.SqueezeScope.SqueezeScopeType.Horizontal, EditorStyles.helpBox))) + { + GUILayout.Label("Sync Steps:"); + GUILayout.Label($"{_component.syncSteps}", GUILayout.Width(100)); + } + + using (new MemoryOptimizerWindow.SqueezeScope((0, 0, MemoryOptimizerWindow.SqueezeScope.SqueezeScopeType.Horizontal, EditorStyles.helpBox))) + { + GUILayout.Label("Step Delay:"); + GUILayout.Label($"{_component.stepDelay}s", GUILayout.Width(100)); + } + + GUI.enabled = true; + + foldoutState = EditorGUILayout.Foldout(EditorPrefs.GetBool(EditorKeyInspectParameters), "Parameter Configurations"); + EditorPrefs.SetBool(EditorKeyInspectParameters, foldoutState); + if (foldoutState) + { + foreach (var parameterConfig in _component.parameterConfigs) + { + using (new MemoryOptimizerWindow.SqueezeScope((0, 0, MemoryOptimizerWindow.SqueezeScope.SqueezeScopeType.Horizontal))) + { + GUILayout.Space(5); + + EditorGUILayout.HelpBox($"{parameterConfig.param.name} - {paramTypes[(int)parameterConfig.param.valueType]}\n -> {parameterConfig.info}", MessageType.None); + + GUI.enabled = false; + + if (parameterConfig.isOrphanParameter) + { + GUI.backgroundColor = Color.gray; + GUILayout.Button("Orphan", GUILayout.Width(203)); + } + else + { + GUI.backgroundColor = parameterConfig.selected ? (parameterConfig.willBeOptimized ? Color.green : Color.yellow) : Color.red; + GUILayout.Button($"{(parameterConfig.selected ? (parameterConfig.willBeOptimized ? "Will optimize." : "Can't be optimized." ) : "Won't be optimized.")}", GUILayout.Width(203)); + } + + GUI.enabled = true; + + GUI.backgroundColor = Color.white; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Editor/MemoryOptimizerComponentEditor.cs.meta b/Editor/MemoryOptimizerComponentEditor.cs.meta new file mode 100644 index 0000000..8f06a33 --- /dev/null +++ b/Editor/MemoryOptimizerComponentEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0508e9fa1fe14611995c477295f8bc55 +timeCreated: 1727374890 \ No newline at end of file diff --git a/Editor/MemoryOptimizerMain.cs b/Editor/MemoryOptimizerMain.cs index 818bbdd..6bf6415 100644 --- a/Editor/MemoryOptimizerMain.cs +++ b/Editor/MemoryOptimizerMain.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JeTeeS.MemoryOptimizer.Shared; using UnityEditor; using UnityEditor.Animations; using UnityEngine; @@ -8,51 +9,19 @@ using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDKBase; using JeTeeS.TES.HelperFunctions; +using static JeTeeS.MemoryOptimizer.Shared.MemoryOptimizerConstants; using static JeTeeS.TES.HelperFunctions.TESHelperFunctions; namespace JeTeeS.MemoryOptimizer { - public static class MemoryOptimizerMain + internal static class MemoryOptimizerMain { - public class MemoryOptimizerListData - { - public VRCExpressionParameters.Parameter param; - public bool selected = false; - public bool willBeOptimized = false; - - public MemoryOptimizerListData(VRCExpressionParameters.Parameter parameter, bool isSelected, bool willOptimize) - { - param = parameter; - selected = isSelected; - willBeOptimized = willOptimize; - } - } - - public class ParamDriversAndStates + internal class ParamDriversAndStates { public VRCAvatarParameterDriver paramDriver = ScriptableObject.CreateInstance(); - public List states = new List(); + public List states = new(); } - private const string discordLink = "https://discord.gg/N7snuJhzkd"; - private const string prefix = "MemOpt_"; - private const string syncingLayerName = prefix + "Syncing Layer"; - private const string syncingLayerIdentifier = prefix + "Syncer"; - private const string mainBlendTreeIdentifier = prefix + "MainBlendTree"; - private const string mainBlendTreeLayerName = prefix + "Main BlendTree"; - private const string smoothingAmountParamName = prefix + "ParamSmoothing"; - private const string smoothedVerSuffix = "_S"; - private const string SmoothingTreeName = "SmoothingParentTree"; - private const string DifferentialTreeName = "DifferentialParentTree"; - private const string DifferentialSuffix = "_Delta"; - private const string constantOneName = prefix + "ConstantOne"; - private const string indexerParamName = prefix + "Indexer "; - private const string boolSyncerParamName = prefix + "BoolSyncer "; - private const string intNFloatSyncerParamName = prefix + "IntNFloatSyncer "; - private const string oneFrameBufferAnimName = prefix + "OneFrameBuffer"; - private const string oneSecBufferAnimName = prefix + "OneSecBuffer"; - private const float changeSensitivity = 0.05f; - private class MemoryOptimizerState { public AnimationClip oneSecBuffer; @@ -68,47 +37,30 @@ private class MemoryOptimizerState public AnimatorControllerLayer syncingLayer; } - public static bool IsSystemInstalled(AnimatorController controller) + public static void InstallMemOpt(VRCAvatarDescriptor avatarIn, AnimatorController fxLayer, VRCExpressionParameters expressionParameters, List optimizeBoolList, List optimizeIntNFloatList, int syncSteps, float stepDelay, bool generateChangeDetection, int wdOption, string mainFilePath) { - if (controller == null) - return false; - if (controller.FindHiddenIdentifier(syncingLayerIdentifier).Count == 1) - return true; - if (controller.FindHiddenIdentifier(mainBlendTreeIdentifier).Count == 1) - return true; - - return false; - } - - public static void InstallMemOpt(VRCAvatarDescriptor avatarIn, AnimatorController fxLayer, VRCExpressionParameters expressionParameters, List boolsToOptimize, List intsNFloatsToOptimize, int syncSteps, float stepDelay, bool generateChangeDetection, int wdOption, string mainFilePath) - { - string generatedAssetsFilePath = mainFilePath + "/GeneratedAssets/"; + var generatedAssetsFilePath = mainFilePath + "/GeneratedAssets/"; ReadyPath(generatedAssetsFilePath); - MemoryOptimizerState optimizerState = new MemoryOptimizerState + MemoryOptimizerState optimizerState = new() { avatar = avatarIn, FXController = fxLayer, expressionParameters = expressionParameters, - boolsToOptimize = boolsToOptimize, - intsNFloatsToOptimize = intsNFloatsToOptimize, - boolsNIntsWithCopies = new List(), - /*Debug stuff - Debug.Log("[MemoryOptimizer] Optimizing Params..."); - foreach (MemoryOptimizerListData listData in boolsToOptimize) Debug.Log("[MemoryOptimizer] Optimizing: " + listData.param.name + " that is the type: " + listData.param.valueType.ToString()); - foreach (MemoryOptimizerListData listData in intsNFloatsToOptimize) Debug.Log("[MemoryOptimizer] Optimizing: " + listData.param.name + " that is the type: " + listData.param.valueType.ToString()); - */ + boolsToOptimize = optimizeBoolList, + intsNFloatsToOptimize = optimizeIntNFloatList, + boolsNIntsWithCopies = new(), - syncingLayer = new AnimatorControllerLayer + syncingLayer = new() { defaultWeight = 1, name = syncingLayerName, - stateMachine = new AnimatorStateMachine + stateMachine = new() { hideFlags = HideFlags.HideInHierarchy, name = syncingLayerName, - anyStatePosition = new Vector3(20, 20, 0), - entryPosition = new Vector3(20, 50, 0) + anyStatePosition = new(20, 20, 0), + entryPosition = new(20, 50, 0) } } }; @@ -117,26 +69,33 @@ public static void InstallMemOpt(VRCAvatarDescriptor avatarIn, AnimatorControlle (optimizerState.oneFrameBuffer, optimizerState.oneSecBuffer) = BufferAnims(generatedAssetsFilePath); fxLayer.AddUniqueParam("IsLocal", AnimatorControllerParameterType.Bool); - - AnimatorControllerParameter constantOneParam = fxLayer.AddUniqueParam(constantOneName, AnimatorControllerParameterType.Float, 1); + fxLayer.AddUniqueParam(constantOneName, AnimatorControllerParameterType.Float, 1); fxLayer.AddUniqueParam(smoothingAmountParamName); - string syncStepsBinary = (syncSteps - 1).DecimalToBinary().ToString(); - for (int i = 0; i < syncStepsBinary.Count(); i++) + var syncStepsBinary = (syncSteps - 1).DecimalToBinary().ToString(); + for (var i = 0; i < syncStepsBinary.Count(); i++) + { AddUniqueSyncedParamToController(indexerParamName + (i + 1).ToString(), fxLayer, expressionParameters, AnimatorControllerParameterType.Bool, VRCExpressionParameters.ValueType.Bool); + } - for (int j = 0; j < boolsToOptimize.Count / syncSteps; j++) + for (var j = 0; j < optimizeBoolList.Count / syncSteps; j++) + { AddUniqueSyncedParamToController(boolSyncerParamName + j, optimizerState.FXController, optimizerState.expressionParameters, AnimatorControllerParameterType.Bool, VRCExpressionParameters.ValueType.Bool); + } - for (int j = 0; j < intsNFloatsToOptimize.Count / syncSteps; j++) + for (var j = 0; j < optimizeIntNFloatList.Count / syncSteps; j++) + { AddUniqueSyncedParamToController(intNFloatSyncerParamName + j, optimizerState.FXController, optimizerState.expressionParameters, AnimatorControllerParameterType.Int, VRCExpressionParameters.ValueType.Int); + } CreateLocalRemoteSplit(optimizerState); if (generateChangeDetection) + { GenerateDeltas(optimizerState, generatedAssetsFilePath); + } - AnimatorState localEntryState = optimizerState.localStateMachine.AddState("Entry", new Vector3(0, 100, 0)); + var localEntryState = optimizerState.localStateMachine.AddState("Entry", new(0, 100, 0)); localEntryState.hideFlags = HideFlags.HideInHierarchy; localEntryState.motion = optimizerState.oneFrameBuffer; @@ -149,86 +108,112 @@ public static void InstallMemOpt(VRCAvatarDescriptor avatarIn, AnimatorControlle CreateParameterDrivers(optimizerState, syncSteps, generateChangeDetection); - bool setWD = true; - if (wdOption == 0) + var setWD = true; + switch (wdOption) { - int foundWD = fxLayer.FindWDInController(); - if (foundWD == -1) setWD = true; - else if (foundWD == 0) setWD = false; - else if (foundWD == 1) setWD = true; + case 0: + { + var foundWD = fxLayer.FindWDInController(); + setWD = foundWD switch + { + -1 => true, + 0 => false, + 1 => true, + _ => setWD + }; + break; + } + case 1: + setWD = false; + break; + default: + setWD = true; + break; } - else if (wdOption == 1) - setWD = false; - else - setWD = true; - foreach (var state in optimizerState.syncingLayer.FindAllStatesInLayer()) state.state.writeDefaultValues = setWD; + foreach (var state in optimizerState.syncingLayer.FindAllStatesInLayer()) + { + state.state.writeDefaultValues = setWD; + } optimizerState.FXController.AddLayer(optimizerState.syncingLayer); optimizerState.FXController.SaveUnsavedAssetsToController(); - foreach (var param in boolsToOptimize) + foreach (var param in optimizeBoolList) + { param.param.networkSynced = false; + } - foreach (var param in intsNFloatsToOptimize) + foreach (var param in optimizeIntNFloatList) + { param.param.networkSynced = false; - + } + EditorUtility.SetDirty(expressionParameters); AssetDatabase.SaveAssets(); SetupParameterDrivers(optimizerState); - EditorApplication.Beep(); + // EditorApplication.Beep(); Debug.Log("[MemoryOptimizer] Installation Complete"); } private static void GenerateDeltas(MemoryOptimizerState optimizerState, string generatedAssetsFilePath) { - List boolsToOptimize = optimizerState.boolsToOptimize; - List intsNFloatsToOptimize = optimizerState.intsNFloatsToOptimize; + var boolsToOptimize = optimizerState.boolsToOptimize; + var intsNFloatsToOptimize = optimizerState.intsNFloatsToOptimize; - List boolsDifferentials = new List(); - List intsNFloatsDifferentials = new List(); - //Add smoothed ver of every param in the list - foreach (MemoryOptimizerListData param in boolsToOptimize) + List boolsDifferentials = new(); + List intsNFloatsDifferentials = new(); + + // Add smoothed ver of every param in the list + foreach (var param in boolsToOptimize) { - List paramMatches = optimizerState.FXController.parameters.Where(x => x.name == param.param.name).ToList(); - AnimatorControllerParameter paramMatch = paramMatches[0]; - if (paramMatch.type == AnimatorControllerParameterType.Int || paramMatch.type == AnimatorControllerParameterType.Bool) + var paramMatches = optimizerState.FXController.parameters.Where(x => x.name == param.param.name).ToList(); + var paramMatch = paramMatches[0]; + + if (paramMatch.type is AnimatorControllerParameterType.Int or AnimatorControllerParameterType.Bool) { - AnimatorControllerParameter paramCopy = optimizerState.FXController.AddUniqueParam(prefix + paramMatch.name + "_Copy"); + var paramCopy = optimizerState.FXController.AddUniqueParam(prefix + paramMatch.name + "_Copy"); optimizerState.boolsNIntsWithCopies.Add(paramMatch); - AnimatorControllerParameter smoothedParam = paramCopy.AddSmoothedVer(0, 1, optimizerState.FXController, prefix + paramCopy.name + smoothedVerSuffix, generatedAssetsFilePath, smoothingAmountParamName, mainBlendTreeIdentifier, mainBlendTreeLayerName, SmoothingTreeName, constantOneName); + + var smoothedParam = paramCopy.AddSmoothedVer(0, 1, optimizerState.FXController, prefix + paramCopy.name + smoothedVerSuffix, generatedAssetsFilePath, smoothingAmountParamName, mainBlendTreeIdentifier, mainBlendTreeLayerName, SmoothingTreeName, constantOneName); boolsDifferentials.Add(AddParamDifferential(paramCopy, smoothedParam, optimizerState.FXController, generatedAssetsFilePath, 0, 1, prefix + paramCopy.name + DifferentialSuffix, mainBlendTreeIdentifier, mainBlendTreeLayerName, DifferentialTreeName, constantOneName)); } else if (paramMatch.type == AnimatorControllerParameterType.Float) { - AnimatorControllerParameter smoothedParam = paramMatch.AddSmoothedVer(0, 1, optimizerState.FXController, prefix + paramMatch.name + smoothedVerSuffix, generatedAssetsFilePath, smoothingAmountParamName, mainBlendTreeIdentifier, mainBlendTreeLayerName, SmoothingTreeName, constantOneName); + var smoothedParam = paramMatch.AddSmoothedVer(0, 1, optimizerState.FXController, prefix + paramMatch.name + smoothedVerSuffix, generatedAssetsFilePath, smoothingAmountParamName, mainBlendTreeIdentifier, mainBlendTreeLayerName, SmoothingTreeName, constantOneName); boolsDifferentials.Add(AddParamDifferential(paramMatch, smoothedParam, optimizerState.FXController, generatedAssetsFilePath, 0, 1, prefix + paramMatch.name + DifferentialSuffix, mainBlendTreeIdentifier, mainBlendTreeLayerName, DifferentialTreeName, constantOneName)); } else + { Debug.LogError("[MemoryOptimizer] Param " + param.param.name + "is not bool, int or float!"); + } } - foreach (MemoryOptimizerListData param in intsNFloatsToOptimize) + foreach (var param in intsNFloatsToOptimize) { - List paramMatches = optimizerState.FXController.parameters.Where(x => x.name == param.param.name).ToList(); - AnimatorControllerParameter paramMatch = paramMatches[0]; - if (paramMatch.type == AnimatorControllerParameterType.Int || paramMatch.type == AnimatorControllerParameterType.Bool) + var paramMatches = optimizerState.FXController.parameters.Where(x => x.name == param.param.name).ToList(); + var paramMatch = paramMatches[0]; + + if (paramMatch.type is AnimatorControllerParameterType.Int or AnimatorControllerParameterType.Bool) { - AnimatorControllerParameter paramCopy = optimizerState.FXController.AddUniqueParam(prefix + paramMatch.name + "_Copy"); + var paramCopy = optimizerState.FXController.AddUniqueParam(prefix + paramMatch.name + "_Copy"); optimizerState.boolsNIntsWithCopies.Add(paramMatch); - AnimatorControllerParameter smoothedParam = paramCopy.AddSmoothedVer(0, 1, optimizerState.FXController, prefix + paramCopy.name + smoothedVerSuffix, generatedAssetsFilePath, smoothingAmountParamName, mainBlendTreeIdentifier, mainBlendTreeLayerName, SmoothingTreeName, constantOneName); + + var smoothedParam = paramCopy.AddSmoothedVer(0, 1, optimizerState.FXController, prefix + paramCopy.name + smoothedVerSuffix, generatedAssetsFilePath, smoothingAmountParamName, mainBlendTreeIdentifier, mainBlendTreeLayerName, SmoothingTreeName, constantOneName); intsNFloatsDifferentials.Add(AddParamDifferential(paramCopy, smoothedParam, optimizerState.FXController, generatedAssetsFilePath, 0, 1, prefix + paramCopy.name + DifferentialSuffix, mainBlendTreeIdentifier, mainBlendTreeLayerName, DifferentialTreeName, constantOneName)); } else if (paramMatch.type == AnimatorControllerParameterType.Float) { - AnimatorControllerParameter smoothedParam = paramMatch.AddSmoothedVer(-1, 1, optimizerState.FXController, prefix + paramMatch.name + smoothedVerSuffix, generatedAssetsFilePath, smoothingAmountParamName, mainBlendTreeIdentifier, mainBlendTreeLayerName, SmoothingTreeName, constantOneName); + var smoothedParam = paramMatch.AddSmoothedVer(-1, 1, optimizerState.FXController, prefix + paramMatch.name + smoothedVerSuffix, generatedAssetsFilePath, smoothingAmountParamName, mainBlendTreeIdentifier, mainBlendTreeLayerName, SmoothingTreeName, constantOneName); intsNFloatsDifferentials.Add(AddParamDifferential(paramMatch, smoothedParam, optimizerState.FXController, generatedAssetsFilePath, -1, 1, prefix + paramMatch.name + DifferentialSuffix, mainBlendTreeIdentifier, mainBlendTreeLayerName, DifferentialTreeName, constantOneName)); } else + { Debug.LogError("[MemoryOptimizer] Param " + param.param.name + "is not bool, int or float!"); + } } optimizerState.boolsDifferentials = boolsDifferentials; @@ -237,28 +222,30 @@ private static void GenerateDeltas(MemoryOptimizerState optimizerState, string g private static void CreateTransitions(MemoryOptimizerState optimizerState, int syncSteps, float stepDelay, bool generateChangeDetection) { - List boolsToOptimize = optimizerState.boolsToOptimize; - List intsNFloatsToOptimize = optimizerState.intsNFloatsToOptimize; - List localSetStates = optimizerState.localSetStates; - List remoteSetStates = optimizerState.remoteSetStates; - List localResetStates = optimizerState.localResetStates; - List boolsDifferentials = optimizerState.boolsDifferentials; - List intsNFloatsDifferentials = optimizerState.intsNFloatsDifferentials; - AnimatorStateMachine remoteStateMachine = optimizerState.remoteStateMachine; - - string syncStepsBinary = (syncSteps - 1).DecimalToBinary().ToString(); - - AnimatorState waitForIndexer = remoteStateMachine.AddState("WaitForIndexer", new Vector3(0, 400, 0)); + var optimizeBoolList = optimizerState.boolsToOptimize; + var optimizeIntNFloatList = optimizerState.intsNFloatsToOptimize; + var localSetStates = optimizerState.localSetStates; + var remoteSetStates = optimizerState.remoteSetStates; + var localResetStates = optimizerState.localResetStates; + var differentialsBool = optimizerState.boolsDifferentials; + var differentialsIntNFloat = optimizerState.intsNFloatsDifferentials; + var remoteStateMachine = optimizerState.remoteStateMachine; + + var syncStepsBinary = (syncSteps - 1).DecimalToBinary().ToString(); + + var waitForIndexer = remoteStateMachine.AddState("WaitForIndexer", new(0, 400, 0)); waitForIndexer.hideFlags = HideFlags.HideInHierarchy; waitForIndexer.motion = optimizerState.oneFrameBuffer; - for (int i = 0; i < syncSteps; i++) + for (var i = 0; i < syncSteps; i++) { - string currentIndex = i.DecimalToBinary().ToString(); + var currentIndex = i.DecimalToBinary().ToString(); while (currentIndex.Length < syncStepsBinary.Length) + { currentIndex = "0" + currentIndex; + } - AnimatorStateTransition toSetterTransition = new AnimatorStateTransition() + AnimatorStateTransition toSetterTransition = new() { destinationState = remoteSetStates[i], exitTime = 0, @@ -269,15 +256,15 @@ private static void CreateTransitions(MemoryOptimizerState optimizerState, int s }; - //Make a list of transitions that go to the "wait" state - List toWaitTransitions = new List(); + // Make a list of transitions that go to the "wait" state + List toWaitTransitions = new(); - //loop through each character of the binary number - for (int j = 1; j <= currentIndex.Length; j++) + // loop through each character of the binary number + for (var j = 1; j <= currentIndex.Length; j++) { - bool isZero = currentIndex[currentIndex.Length - j].ToString() == "0"; + var isZero = currentIndex[^j].ToString() == "0"; toSetterTransition.AddCondition(isZero ? AnimatorConditionMode.IfNot : AnimatorConditionMode.If, 0, indexerParamName + j); - toWaitTransitions.Add(new AnimatorStateTransition + toWaitTransitions.Add(new() { destinationState = waitForIndexer, exitTime = 0, @@ -293,10 +280,10 @@ private static void CreateTransitions(MemoryOptimizerState optimizerState, int s { void SetupLocalResetStateTransitions(string differentialName) { - //add transitions from value changed state to appropriate reset state - foreach (AnimatorState state in localSetStates) + // add transitions from value changed state to appropriate reset state + foreach (var state in localSetStates) { - AnimatorStateTransition transition = new AnimatorStateTransition + AnimatorStateTransition transition = new() { destinationState = localResetStates[i], exitTime = 0, @@ -305,10 +292,11 @@ void SetupLocalResetStateTransitions(string differentialName) duration = 0f, hideFlags = HideFlags.HideInHierarchy }; + transition.AddCondition(AnimatorConditionMode.Less, changeSensitivity * -1, differentialName); state.AddTransition(transition); - transition = new AnimatorStateTransition + transition = new() { destinationState = localResetStates[i], exitTime = 0, @@ -317,84 +305,118 @@ void SetupLocalResetStateTransitions(string differentialName) duration = 0f, hideFlags = HideFlags.HideInHierarchy }; + transition.AddCondition(AnimatorConditionMode.Greater, changeSensitivity, differentialName); state.AddTransition(transition); } } - for (int j = 0; j < boolsToOptimize.Count / syncSteps; j++) + for (var j = 0; j < optimizeBoolList.Count / syncSteps; j++) { - string differentialName = boolsDifferentials[i * (boolsToOptimize.Count() / syncSteps) + j].name; - SetupLocalResetStateTransitions(differentialName); + SetupLocalResetStateTransitions(differentialsBool[i * (optimizeBoolList.Count() / syncSteps) + j].name); } - for (int j = 0; j < intsNFloatsToOptimize.Count / syncSteps; j++) + for (var j = 0; j < optimizeIntNFloatList.Count / syncSteps; j++) { - string differentialName = intsNFloatsDifferentials[i * (intsNFloatsToOptimize.Count() / syncSteps) + j].name; - SetupLocalResetStateTransitions(differentialName); + SetupLocalResetStateTransitions(differentialsIntNFloat[i * (optimizeIntNFloatList.Count() / syncSteps) + j].name); } } - //add the transitions from remote set states to the wait state - foreach (AnimatorStateTransition transition in toWaitTransitions) + // add the transitions from remote set states to the wait state + foreach (var transition in toWaitTransitions) + { remoteSetStates[i].AddTransition(transition); + } - //add transition from wait state to current set state + // add transition from wait state to current set state waitForIndexer.AddTransition(toSetterTransition); } - for (int i = 0; i < localSetStates.Count; i++) - localSetStates[i].AddTransition(new AnimatorStateTransition() { destinationState = localSetStates[(i + 1) % localSetStates.Count], exitTime = stepDelay, hasExitTime = true, hasFixedDuration = true, duration = 0f, hideFlags = HideFlags.HideInHierarchy }); + for (var i = 0; i < localSetStates.Count; i++) + { + localSetStates[i].AddTransition(new AnimatorStateTransition() + { + destinationState = localSetStates[(i + 1) % localSetStates.Count], + exitTime = stepDelay, + hasExitTime = true, + hasFixedDuration = true, + duration = 0f, + hideFlags = HideFlags.HideInHierarchy + }); + } } private static void CreateParameterDrivers(MemoryOptimizerState optimizerState, int syncSteps, bool generateChangeDetection) { - List localSetStates = optimizerState.localSetStates; - List localResetStates = optimizerState.localResetStates; - List remoteSetStates = optimizerState.remoteSetStates; - List boolsToOptimize = optimizerState.boolsToOptimize; - List intsNFloatsToOptimize = optimizerState.intsNFloatsToOptimize; - - List localSettersParameterDrivers = new List(); - List remoteSettersParameterDrivers = new List(); - string syncStepsBinary = (syncSteps - 1).DecimalToBinary().ToString(); - for (int i = 0; i < syncSteps; i++) - { - string currentIndex = i.DecimalToBinary().ToString(); + var localSetStates = optimizerState.localSetStates; + var localResetStates = optimizerState.localResetStates; + var remoteSetStates = optimizerState.remoteSetStates; + + var optimizeBoolList = optimizerState.boolsToOptimize; + var optimizeIntNFloatList = optimizerState.intsNFloatsToOptimize; + + List localSettersParameterDrivers = new(); + List remoteSettersParameterDrivers = new(); + + var syncStepsBinary = (syncSteps - 1).DecimalToBinary().ToString(); + for (var i = 0; i < syncSteps; i++) + { + var currentIndex = i.DecimalToBinary().ToString(); while (currentIndex.Length < syncStepsBinary.Length) + { currentIndex = "0" + currentIndex; + } - localSettersParameterDrivers.Add(new ParamDriversAndStates()); + localSettersParameterDrivers.Add(new()); localSettersParameterDrivers.Last().states.Add(localSetStates[i]); if (generateChangeDetection) { localSettersParameterDrivers.Last().states.Add(localResetStates[i]); - foreach (AnimatorControllerParameter param in optimizerState.boolsNIntsWithCopies) - localSettersParameterDrivers.Last().paramDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter() { name = prefix + param.name + "_Copy", source = param.name, type = VRC_AvatarParameterDriver.ChangeType.Copy }); + foreach (var param in optimizerState.boolsNIntsWithCopies) + { + localSettersParameterDrivers.Last().paramDriver.parameters.Add(new() + { + name = prefix + param.name + "_Copy", + source = param.name, + type = VRC_AvatarParameterDriver.ChangeType.Copy + }); + } } - remoteSettersParameterDrivers.Add(new ParamDriversAndStates()); + remoteSettersParameterDrivers.Add(new()); remoteSettersParameterDrivers.Last().states.Add(remoteSetStates[i]); - //loop through each character of the binary number - for (int j = 1; j <= currentIndex.Length; j++) + // loop through each character of the binary number + for (var j = 1; j <= currentIndex.Length; j++) { - int value = currentIndex[currentIndex.Length - j].ToString() == "0" ? 0 : 1; - localSettersParameterDrivers.Last().paramDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter() { name = indexerParamName + j, value = value, type = VRC_AvatarParameterDriver.ChangeType.Set }); - remoteSettersParameterDrivers.Last().paramDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter() { name = indexerParamName + j, value = value, type = VRC_AvatarParameterDriver.ChangeType.Set }); + var value = currentIndex[^j].ToString() == "0" ? 0 : 1; + localSettersParameterDrivers.Last().paramDriver.parameters.Add(new() + { + name = indexerParamName + j, + value = value, + type = VRC_AvatarParameterDriver.ChangeType.Set + }); + + remoteSettersParameterDrivers.Last().paramDriver.parameters.Add(new() + { + name = indexerParamName + j, + value = value, + type = VRC_AvatarParameterDriver.ChangeType.Set + }); } - for (int j = 0; j < boolsToOptimize.Count / syncSteps; j++) + for (var j = 0; j < optimizeBoolList.Count / syncSteps; j++) { - VRCExpressionParameters.Parameter param = boolsToOptimize.ElementAt(i * (boolsToOptimize.Count() / syncSteps) + j).param; - localSettersParameterDrivers.Last().paramDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter() + var param = optimizeBoolList.ElementAt(i * (optimizeBoolList.Count() / syncSteps) + j).param; + localSettersParameterDrivers.Last().paramDriver.parameters.Add(new() { name = boolSyncerParamName + j, source = param.name, type = VRC_AvatarParameterDriver.ChangeType.Copy }); - remoteSettersParameterDrivers.Last().paramDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter() + + remoteSettersParameterDrivers.Last().paramDriver.parameters.Add(new() { name = param.name, source = boolSyncerParamName + j, @@ -402,19 +424,20 @@ private static void CreateParameterDrivers(MemoryOptimizerState optimizerState, }); } - for (int j = 0; j < intsNFloatsToOptimize.Count / syncSteps; j++) + for (var j = 0; j < optimizeIntNFloatList.Count / syncSteps; j++) { - VRCExpressionParameters.Parameter param = intsNFloatsToOptimize.ElementAt(i * (intsNFloatsToOptimize.Count() / syncSteps) + j).param; + var param = optimizeIntNFloatList.ElementAt(i * (optimizeIntNFloatList.Count() / syncSteps) + j).param; if (param.valueType == VRCExpressionParameters.ValueType.Int) { - localSettersParameterDrivers.Last().paramDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter() + localSettersParameterDrivers.Last().paramDriver.parameters.Add(new() { name = intNFloatSyncerParamName + j, source = param.name, type = VRC_AvatarParameterDriver.ChangeType.Copy }); + remoteSettersParameterDrivers.Last().paramDriver.parameters.Add( - new VRC_AvatarParameterDriver.Parameter() + new() { name = param.name, source = intNFloatSyncerParamName + j, @@ -423,7 +446,7 @@ private static void CreateParameterDrivers(MemoryOptimizerState optimizerState, } else if (param.valueType == VRCExpressionParameters.ValueType.Float) { - localSettersParameterDrivers.Last().paramDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter() + localSettersParameterDrivers.Last().paramDriver.parameters.Add(new() { name = intNFloatSyncerParamName + j, source = param.name, @@ -434,21 +457,23 @@ private static void CreateParameterDrivers(MemoryOptimizerState optimizerState, sourceMin = 0, sourceMax = 1 }); - remoteSettersParameterDrivers.Last().paramDriver.parameters.Add( - new VRC_AvatarParameterDriver.Parameter() - { - name = param.name, - source = intNFloatSyncerParamName + j, - type = VRC_AvatarParameterDriver.ChangeType.Copy, - convertRange = true, - destMin = 0, - destMax = 1, - sourceMin = 0, - sourceMax = 255 - }); + + remoteSettersParameterDrivers.Last().paramDriver.parameters.Add(new() + { + name = param.name, + source = intNFloatSyncerParamName + j, + type = VRC_AvatarParameterDriver.ChangeType.Copy, + convertRange = true, + destMin = 0, + destMax = 1, + sourceMin = 0, + sourceMax = 255 + }); } else + { Debug.LogError("[MemoryOptimizer] " + param.name + " is not an int or a float!"); + } } } @@ -458,28 +483,32 @@ private static void CreateParameterDrivers(MemoryOptimizerState optimizerState, private static void CreateStates(MemoryOptimizerState optimizerState, int syncSteps, float stepDelay, bool generateChangeDetection) { - string syncStepsBinary = (syncSteps - 1).DecimalToBinary().ToString(); - AnimatorStateMachine localStateMachine = optimizerState.localStateMachine; - AnimatorStateMachine remoteStateMachine = optimizerState.remoteStateMachine; + var syncStepsBinary = (syncSteps - 1).DecimalToBinary().ToString(); + + var localStateMachine = optimizerState.localStateMachine; + var remoteStateMachine = optimizerState.remoteStateMachine; - List localSetStates = new List(); - List localResetStates = new List(); - List remoteSetStates = new List(); - for (int i = 0; i < syncSteps; i++) + List localSetStates = new(); + List localResetStates = new(); + List remoteSetStates = new(); + + for (var i = 0; i < syncSteps; i++) { - //convert i to binary so it can be used for the binary counter - string currentIndex = i.DecimalToBinary().ToString(); + // convert i to binary, so it can be used for the binary counter + var currentIndex = i.DecimalToBinary().ToString(); while (currentIndex.Length < syncStepsBinary.Length) + { currentIndex = "0" + currentIndex; + } - //add the local set and reset states - localSetStates.Add(localStateMachine.AddState("Set Value " + (i + 1), AngleRadiusToPos(((float)i / syncSteps + 0.5f) * (float)Math.PI * 2f, 400f, new Vector3(0, 600, 0)))); + // add the local set and reset states + localSetStates.Add(localStateMachine.AddState("Set Value " + (i + 1), AngleRadiusToPos(((float)i / syncSteps + 0.5f) * (float)Math.PI * 2f, 400f, new(0, 600, 0)))); localSetStates.Last().hideFlags = HideFlags.HideInHierarchy; localSetStates.Last().motion = optimizerState.oneSecBuffer; if (generateChangeDetection) { - localResetStates.Add(localStateMachine.AddState("Reset Change Detection " + (i + 1), AngleRadiusToPos(((float)i / syncSteps + 0.5f) * (float)Math.PI * 2f + ((float)Math.PI * 0.25f), 480f, new Vector3(0, 600, 0)))); + localResetStates.Add(localStateMachine.AddState("Reset Change Detection " + (i + 1), AngleRadiusToPos(((float)i / syncSteps + 0.5f) * (float)Math.PI * 2f + ((float)Math.PI * 0.25f), 480f, new(0, 600, 0)))); localResetStates.Last().hideFlags = HideFlags.HideInHierarchy; localResetStates.Last().motion = optimizerState.oneSecBuffer; @@ -494,8 +523,8 @@ private static void CreateStates(MemoryOptimizerState optimizerState, int syncSt }); } - //add the remote set states - remoteSetStates.Add(remoteStateMachine.AddState("Set values for index " + (i + 1), AngleRadiusToPos(((float)i / syncSteps + 0.5f) * (float)Math.PI * 2f, 250f, new Vector3(0, 400, 0)))); + // add the remote set states + remoteSetStates.Add(remoteStateMachine.AddState("Set values for index " + (i + 1), AngleRadiusToPos(((float)i / syncSteps + 0.5f) * (float)Math.PI * 2f, 250f, new(0, 400, 0)))); remoteSetStates.Last().hideFlags = HideFlags.HideInHierarchy; remoteSetStates.Last().motion = optimizerState.oneFrameBuffer; } @@ -507,194 +536,227 @@ private static void CreateStates(MemoryOptimizerState optimizerState, int syncSt private static (AnimationClip oneFrameBuffer, AnimationClip oneSecBuffer) BufferAnims(string generatedAssetsFilePath) { - //create and overwrite single frame buffer animation - AnimationClip oneFrameBuffer = new AnimationClip() { name = oneFrameBufferAnimName, }; - AnimationCurve oneFrameBufferCurve = new AnimationCurve(); + // create and overwrite single frame buffer animation + AnimationClip oneFrameBuffer = new() { name = oneFrameBufferAnimName, }; + AnimationCurve oneFrameBufferCurve = new(); + oneFrameBufferCurve.AddKey(0, 0); oneFrameBufferCurve.AddKey(1 / 60f, 1); + oneFrameBuffer.SetCurve("", typeof(GameObject), "DO NOT CHANGE THIS ANIMATION", oneFrameBufferCurve); + AssetDatabase.DeleteAsset(generatedAssetsFilePath + oneFrameBuffer.name + ".anim"); AssetDatabase.CreateAsset(oneFrameBuffer, generatedAssetsFilePath + oneFrameBuffer.name + ".anim"); - //create and overwrite one second buffer animation - AnimationClip oneSecBuffer = new AnimationClip() { name = oneSecBufferAnimName, }; - AnimationCurve oneSecBufferCurve = new AnimationCurve(); + // create and overwrite one second buffer animation + AnimationClip oneSecBuffer = new() { name = oneSecBufferAnimName, }; + AnimationCurve oneSecBufferCurve = new(); + oneSecBufferCurve.AddKey(0, 0); oneSecBufferCurve.AddKey(1, 1); + oneSecBuffer.SetCurve("", typeof(GameObject), "DO NOT CHANGE THIS ANIMATION", oneSecBufferCurve); + AssetDatabase.DeleteAsset(generatedAssetsFilePath + oneSecBuffer.name + ".anim"); AssetDatabase.CreateAsset(oneSecBuffer, generatedAssetsFilePath + oneSecBuffer.name + ".anim"); + return (oneFrameBuffer, oneSecBuffer); } private static void SetupParameterDrivers(MemoryOptimizerState optimizerState) { - List localSettersParameterDrivers = optimizerState.localSettersParameterDrivers; - List remoteSettersParameterDrivers = optimizerState.remoteSettersParameterDrivers; - List localSetStates = optimizerState.localSetStates; - List localResetStates = optimizerState.localResetStates; + var localSettersParameterDrivers = optimizerState.localSettersParameterDrivers; + var remoteSettersParameterDrivers = optimizerState.remoteSettersParameterDrivers; + var localSetStates = optimizerState.localSetStates; + var localResetStates = optimizerState.localResetStates; - foreach (ParamDriversAndStates driver in localSettersParameterDrivers) + foreach (var driver in localSettersParameterDrivers) { - foreach (AnimatorState state in driver.states) + foreach (var state in driver.states) { - VRCAvatarParameterDriver temp = state.AddStateMachineBehaviour(); + var temp = state.AddStateMachineBehaviour(); temp.parameters = driver.paramDriver.parameters.ToList(); } } - foreach (ParamDriversAndStates driver in remoteSettersParameterDrivers) + foreach (var driver in remoteSettersParameterDrivers) { - foreach (AnimatorState state in driver.states) + foreach (var state in driver.states) { - VRCAvatarParameterDriver temp = state.AddStateMachineBehaviour(); + var temp = state.AddStateMachineBehaviour(); temp.parameters = driver.paramDriver.parameters; } } - foreach (AnimatorState state in localSetStates) + foreach (var state in localSetStates) { - VRCAvatarParameterDriver temp = (VRCAvatarParameterDriver)state.behaviours[0]; - temp.parameters.Add(new VRC_AvatarParameterDriver.Parameter() { name = smoothingAmountParamName, type = VRC_AvatarParameterDriver.ChangeType.Set, value = 0 }); + var temp = (VRCAvatarParameterDriver)state.behaviours[0]; + temp.parameters.Add(new() + { + name = smoothingAmountParamName, + type = VRC_AvatarParameterDriver.ChangeType.Set, + value = 0 + }); } - foreach (AnimatorState state in localResetStates) + foreach (var state in localResetStates) { - VRCAvatarParameterDriver temp = (VRCAvatarParameterDriver)state.behaviours[0]; - temp.parameters.Add(new VRC_AvatarParameterDriver.Parameter() { name = smoothingAmountParamName, type = VRC_AvatarParameterDriver.ChangeType.Set, value = 1 }); + var temp = (VRCAvatarParameterDriver)state.behaviours[0]; + temp.parameters.Add(new() + { + name = smoothingAmountParamName, + type = VRC_AvatarParameterDriver.ChangeType.Set, + value = 1 + }); } } private static void CreateLocalRemoteSplit(MemoryOptimizerState optimizerState) { - AnimatorControllerLayer syncingLayer = optimizerState.syncingLayer; - AnimatorState localRemoteSplitState = - syncingLayer.stateMachine.AddState("Local/Remote split", position: new Vector3(0, 100, 0)); + var syncingLayer = optimizerState.syncingLayer; + var localRemoteSplitState = syncingLayer.stateMachine.AddState("Local/Remote split", position: new(0, 100, 0)); + localRemoteSplitState.motion = optimizerState.oneFrameBuffer; localRemoteSplitState.hideFlags = HideFlags.HideInHierarchy; syncingLayer.stateMachine.defaultState = localRemoteSplitState; - AnimatorStateMachine localStateMachine = - syncingLayer.stateMachine.AddStateMachine("Local", position: new Vector3(100, 200, 0)); + var localStateMachine = syncingLayer.stateMachine.AddStateMachine("Local", position: new(100, 200, 0)); localStateMachine.hideFlags = HideFlags.HideInHierarchy; - AnimatorStateMachine remoteStateMachine = syncingLayer.stateMachine.AddStateMachine("Remote", position: new Vector3(-100, 200, 0)); + var remoteStateMachine = syncingLayer.stateMachine.AddStateMachine("Remote", position: new(-100, 200, 0)); remoteStateMachine.hideFlags = HideFlags.HideInHierarchy; - localStateMachine.anyStatePosition = new Vector3(20, 20, 0); - localStateMachine.entryPosition = new Vector3(20, 50, 0); + localStateMachine.anyStatePosition = new(20, 20, 0); + localStateMachine.entryPosition = new(20, 50, 0); - remoteStateMachine.anyStatePosition = new Vector3(20, 20, 0); - remoteStateMachine.entryPosition = new Vector3(20, 50, 0); + remoteStateMachine.anyStatePosition = new(20, 20, 0); + remoteStateMachine.entryPosition = new(20, 50, 0); - AnimatorStateTransition localTransition = localRemoteSplitState.AddTransition(localStateMachine); - AnimatorStateTransition remoteTransition = localRemoteSplitState.AddTransition(remoteStateMachine); - localTransition.AddCondition(AnimatorConditionMode.If, 0, "IsLocal"); - remoteTransition.AddCondition(AnimatorConditionMode.IfNot, 0, "IsLocal"); + var localTransition = localRemoteSplitState.AddTransition(localStateMachine); + var remoteTransition = localRemoteSplitState.AddTransition(remoteStateMachine); + + // in some rare cases some types of FaceTracking (or other OSC based components) use a float-based IsLocal parameter + // this will prevent the layer from working correctly as the transition binding no longer matches the type + if (optimizerState.FXController.parameters.Any(p => p.name.Equals("IsLocal") && p.type == AnimatorControllerParameterType.Float)) + { + localTransition.AddCondition(AnimatorConditionMode.Greater, 0.5f, "IsLocal"); + remoteTransition.AddCondition(AnimatorConditionMode.Less, 0.5f, "IsLocal"); + } + else + { + localTransition.AddCondition(AnimatorConditionMode.If, 0, "IsLocal"); + remoteTransition.AddCondition(AnimatorConditionMode.IfNot, 0, "IsLocal"); + } + optimizerState.localStateMachine = localStateMachine; optimizerState.remoteStateMachine = remoteStateMachine; } public static void UninstallMemOpt(VRCAvatarDescriptor avatar, AnimatorController fxLayer, VRCExpressionParameters expressionParameters) { - List generatedExpressionParams = new List(); - List optimizedParams = new List(); - List generatedAnimatorParams = new List(); - foreach (AnimatorControllerParameter controllerParam in fxLayer.parameters) + List generatedExpressionParams = new(); + List optimizedParams = new(); + List generatedAnimatorParams = new(); + + foreach (var controllerParam in fxLayer.parameters) + { if (controllerParam.name.Contains(prefix)) + { generatedAnimatorParams.Add(controllerParam); + } + } - List mainBlendTreeLayers = fxLayer.FindHiddenIdentifier(mainBlendTreeIdentifier); - List syncingLayers = fxLayer.FindHiddenIdentifier(syncingLayerIdentifier); + var mainBlendTreeLayers = fxLayer.FindHiddenIdentifier(mainBlendTreeIdentifier); + var syncingLayers = fxLayer.FindHiddenIdentifier(syncingLayerIdentifier); if (mainBlendTreeLayers.Count > 1) - if (UninstallErrorDialogWithDiscordLink( - $"Too many MemOptBlendtrees found", - $"Too many MemOptBlendtrees found! {mainBlendTreeLayers.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", - discordLink) != 0 - ) + { + if (UninstallErrorDialogWithDiscordLink($"Too many MemOptBlendtrees found", $"Too many MemOptBlendtrees found! {mainBlendTreeLayers.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", discordLink) != 0) + { return; + } + } if (syncingLayers.Count != 1) { - string s = (mainBlendTreeLayers.Count > 1) ? "many" : "few"; - if (UninstallErrorDialogWithDiscordLink( - $"Too {s} syncing layers found", - $"Too {s} syncing layers found! {syncingLayers.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", - discordLink) != 0 - ) + var s = (mainBlendTreeLayers.Count > 1) ? "many" : "few"; + if (UninstallErrorDialogWithDiscordLink($"Too {s} syncing layers found", $"Too {s} syncing layers found! {syncingLayers.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", discordLink) != 0) + { return; + } } else { - List states = syncingLayers[0].FindAllStatesInLayer(); - List setStates = states.Where(x => x.state.name.Contains("Set Value ")).ToList(); - foreach (ChildAnimatorState state in setStates) + var states = syncingLayers[0].FindAllStatesInLayer(); + var setStates = states.Where(x => x.state.name.Contains("Set Value ")).ToList(); + foreach (var state in setStates) { - VRCAvatarParameterDriver paramdriver = (VRCAvatarParameterDriver)state.state.behaviours[0]; - List paramdriverParams = paramdriver.parameters; - foreach (VRC_AvatarParameterDriver.Parameter param in paramdriverParams) - if (!String.IsNullOrEmpty(param.source)) - foreach (VRCExpressionParameters.Parameter item in expressionParameters.parameters.Where(x => x.name == param.source)) - optimizedParams.Add(item); + var avatarParameterDriver = (VRCAvatarParameterDriver)state.state.behaviours[0]; + var avatarParameterDriverParameters = avatarParameterDriver.parameters; + foreach (var param in avatarParameterDriverParameters.Where(param => !string.IsNullOrEmpty(param.source))) + { + optimizedParams.AddRange(expressionParameters.parameters.Where(x => x.name == param.source)); + } } } - foreach (VRCExpressionParameters.Parameter item in expressionParameters.parameters.Where(x => x.name.Contains(prefix))) + foreach (var item in expressionParameters.parameters.Where(x => x.name.Contains(prefix))) + { generatedExpressionParams.Add(item); - + } + if (generatedExpressionParams.Count <= 0) - if (UninstallErrorDialogWithDiscordLink( - "Too few generated expressions found", - $"Too few generated expressions found! {generatedExpressionParams.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", - discordLink) != 0 - ) + { + if (UninstallErrorDialogWithDiscordLink("Too few generated expressions found", $"Too few generated expressions found! {generatedExpressionParams.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", discordLink) != 0) + { return; + } + } if (generatedAnimatorParams.Count <= 0) - if (UninstallErrorDialogWithDiscordLink( - "Too few generated animator parameters found!", - $"Too few generated animator parameters found! {generatedAnimatorParams.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", - discordLink) != 0 - ) + { + if (UninstallErrorDialogWithDiscordLink("Too few generated animator parameters found!", $"Too few generated animator parameters found! {generatedAnimatorParams.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", discordLink) != 0) + { return; + } + } if (optimizedParams.Count < 2) - if (UninstallErrorDialogWithDiscordLink( - "Too few optimized parameters found!", - $"Too few generated animator parameters found! {optimizedParams.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", - discordLink) != 0 - ) + { + if (UninstallErrorDialogWithDiscordLink("Too few optimized parameters found!", $"Too few generated animator parameters found! {optimizedParams.Count} found. \nPlease join the discord for support. \nKeep in mind there are backups made by default by the script!", discordLink) != 0) + { return; + } + } - foreach (AnimatorControllerLayer mainBlendTreeLayer in mainBlendTreeLayers) + foreach (var mainBlendTreeLayer in mainBlendTreeLayers) { - //Debug.Log("[MemoryOptimizer] Animator layer " + mainBlendTreeLayer.name + " of index " + fxLayer.FindLayerIndex(mainBlendTreeLayer) + " is being deleted"); + // Debug.Log("[MemoryOptimizer] Animator layer " + mainBlendTreeLayer.name + " of index " + fxLayer.FindLayerIndex(mainBlendTreeLayer) + " is being deleted"); DeleteBlendTreeFromAsset((BlendTree)mainBlendTreeLayer.stateMachine.states[0].state.motion); fxLayer.RemoveLayer(mainBlendTreeLayer); } - foreach (AnimatorControllerLayer syncingLayer in syncingLayers) + foreach (var syncingLayer in syncingLayers) { - //Debug.Log("[MemoryOptimizer] Animator layer " + syncingLayer.name + " of index " + fxLayer.FindLayerIndex(syncingLayer) + " is being deleted"); + // Debug.Log("[MemoryOptimizer] Animator layer " + syncingLayer.name + " of index " + fxLayer.FindLayerIndex(syncingLayer) + " is being deleted"); fxLayer.RemoveLayer(syncingLayer); } - foreach (VRCExpressionParameters.Parameter param in generatedExpressionParams) + foreach (var param in generatedExpressionParams) { - //Debug.Log("[MemoryOptimizer] Expression param " + param.name + " of type: " + param.valueType + " is being deleted"); + // Debug.Log("[MemoryOptimizer] Expression param " + param.name + " of type: " + param.valueType + " is being deleted"); expressionParameters.parameters = expressionParameters.parameters.Where(x => x != param).ToArray(); } - foreach (AnimatorControllerParameter param in generatedAnimatorParams) + foreach (var param in generatedAnimatorParams) { - //Debug.Log("[MemoryOptimizer] Controller param " + param.name + " of type: " + param.type + " is being deleted"); + // Debug.Log("[MemoryOptimizer] Controller param " + param.name + " of type: " + param.type + " is being deleted"); fxLayer.RemoveParameter(param); } - foreach (VRCExpressionParameters.Parameter param in optimizedParams) + foreach (var param in optimizedParams) { - //Debug.Log("[MemoryOptimizer] Optimized param " + param.name + " of type: " + param.valueType + " setting to sync"); + // Debug.Log("[MemoryOptimizer] Optimized param " + param.name + " of type: " + param.valueType + " setting to sync"); param.networkSynced = true; } diff --git a/Editor/MemoryOptimizerWindow.cs b/Editor/MemoryOptimizerWindow.cs index 34d6d9e..a5565e2 100644 --- a/Editor/MemoryOptimizerWindow.cs +++ b/Editor/MemoryOptimizerWindow.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using JeTeeS.MemoryOptimizer.Helper; +using JeTeeS.MemoryOptimizer.Shared; +using JeTeeS.TES.HelperFunctions; using UnityEditor; using UnityEditor.Animations; using UnityEngine; @@ -9,29 +12,22 @@ using static JeTeeS.MemoryOptimizer.MemoryOptimizerWindow.SqueezeScope; using static JeTeeS.MemoryOptimizer.MemoryOptimizerWindow.SqueezeScope.SqueezeScopeType; using static JeTeeS.TES.HelperFunctions.TESHelperFunctions; +using static JeTeeS.MemoryOptimizer.Shared.MemoryOptimizerConstants; namespace JeTeeS.MemoryOptimizer { - public class MemoryOptimizerWindow : EditorWindow + internal class MemoryOptimizerWindow : EditorWindow { - private const string menuPath = "Tools/TES/MemoryOptimizer"; - private const string defaultSavePath = "Assets/TES/MemOpt"; - private string currentSavePath; - DefaultAsset savePathOverride = null; - - private const string prefKey = "Mem_Opt_Pref_"; - private const string unlockSyncStepsEPKey = prefKey + "UnlockSyncSteps"; - private const string backUpModeEPKey = prefKey + "BackUpMode"; - private const string savePathPPKey = prefKey + "SavePath"; - private const int maxUnsyncedParams = 8192; + private static MemoryOptimizerComponent _component; + private DateTime _nextReconnectCheck = DateTime.Now.AddDays(-1); + + private string currentSavePath; + private DefaultAsset savePathOverride = null; + private bool unlockSyncSteps = false; private int backupMode = 0; - private readonly string[] paramTypes = { "Int", "Float", "Bool" }; - public readonly string[] wdOptions = { "Auto-Detect", "Off", "On" }; - public readonly string[] backupModes = { "On", "Off", "Ask" }; - private int tab = 0; private Vector2 scrollPosition; private bool runOnce; @@ -40,11 +36,12 @@ public class MemoryOptimizerWindow : EditorWindow private AnimatorController avatarFXLayer; private VRCExpressionParameters expressionParameters; - private List selectedBools = new List(); - private List boolsToOptimize = new List(); - private List selectedIntsNFloats = new List(); - private List intsNFloatsToOptimize = new List(); - private List paramList; + private List selectedBools = new(); + private List boolsToOptimize = new(); + private List selectedIntsNFloats = new(); + private List intsNFloatsToOptimize = new(); + private List paramList = new(); + private int installationIndexers; private int installationBoolSyncers; private int installationIntSyncers; @@ -59,78 +56,143 @@ public class MemoryOptimizerWindow : EditorWindow [MenuItem(menuPath)] public static void ShowWindow() { - //Show existing window instance. If one doesn't exist, make one. - EditorWindow window = GetWindow(typeof(MemoryOptimizerWindow), false, "Memory Optimizer", true); + // if you open by menu path, open the normal editor + ShowWindowInternal(null); + } + + internal static void ShowWindowInternal(MemoryOptimizerComponent component) + { + // make sure to re-open for bindings and avoiding bugs + if (HasOpenInstances() && GetWindow() is { } instance) + { + instance.Close(); + } + + _component = component; + + // Open new instance + EditorWindow window = GetWindow(false, "Memory Optimizer" + (component is not null ? " (Component Editor)" : string.Empty), true); window.minSize = new Vector2(600, 900); } + private void Awake() + { + if (_component is not null) + { + LoadFromComponent(); + } + else + { + TryReconnectingWithComponent(); + } + } + private void OnGUI() { unlockSyncSteps = EditorPrefs.GetBool(unlockSyncStepsEPKey); backupMode = EditorPrefs.GetInt(backUpModeEPKey); + string savePathEP = PlayerPrefs.GetString(savePathPPKey); - if (!String.IsNullOrEmpty(savePathEP) && AssetDatabase.IsValidFolder(savePathEP)) + if (!string.IsNullOrEmpty(savePathEP) && AssetDatabase.IsValidFolder(savePathEP)) + { savePathOverride = (DefaultAsset)AssetDatabase.LoadAssetAtPath(savePathEP, typeof(DefaultAsset)); + } - if (savePathOverride && AssetDatabase.IsValidFolder(AssetDatabase.GetAssetPath(savePathOverride))) + if (savePathOverride is not null && AssetDatabase.IsValidFolder(AssetDatabase.GetAssetPath(savePathOverride))) + { currentSavePath = AssetDatabase.GetAssetPath(savePathOverride); - else + } + else + { currentSavePath = defaultSavePath; + } - tab = GUILayout.Toolbar (tab, new string[] {"Install menu", "Settings menu"}); - switch (tab) + if (_component is null) { - case 0: + TryReconnectingWithComponent(); + } + tab = GUILayout.Toolbar(tab, new[] { _component is null ? "Install menu" : "Configure", "Settings menu" }); + switch (tab) + { + case 0: using (new SqueezeScope((0, 0, Vertical, EditorStyles.helpBox))) { using (new SqueezeScope((0, 0, Vertical, EditorStyles.helpBox))) { + EditorGUI.BeginDisabledGroup(_component is not null); + using (new SqueezeScope((0, 0, Horizontal, EditorStyles.helpBox))) { void OnAvatarChange() { if (avatarDescriptor) { - avatarFXLayer = FindFXLayer(avatarDescriptor); - expressionParameters = FindExpressionParams(avatarDescriptor); + if (!TryReconnectingWithComponent()) + { + avatarFXLayer = FindFXLayer(avatarDescriptor); + expressionParameters = FindExpressionParams(avatarDescriptor); + } } else { avatarFXLayer = null; expressionParameters = null; } + ResetParamSelection(); } - + using (new ChangeCheckScope(OnAvatarChange)) { avatarDescriptor = (VRCAvatarDescriptor)EditorGUILayout.ObjectField("Avatar", avatarDescriptor, typeof(VRCAvatarDescriptor), true); - if (avatarDescriptor == null) + if (_component is null && avatarDescriptor is null) + { if (GUILayout.Button("Auto-detect")) + { FillAvatarFields(null, avatarFXLayer, expressionParameters); + } + } } } + + EditorGUI.EndDisabledGroup(); - using (new SqueezeScope((0, 0, Horizontal, EditorStyles.helpBox))) + if (_component is null) { - avatarFXLayer = (AnimatorController)EditorGUILayout.ObjectField("FX Layer", avatarFXLayer, typeof(AnimatorController), true); - if (avatarFXLayer == null) - if (GUILayout.Button("Auto-Detect")) - FillAvatarFields(avatarDescriptor, null, expressionParameters); - } + using (new SqueezeScope((0, 0, Horizontal, EditorStyles.helpBox))) + { + avatarFXLayer = (AnimatorController)EditorGUILayout.ObjectField("FX Layer", avatarFXLayer, typeof(AnimatorController), true); + if (avatarFXLayer is null) + { + if (GUILayout.Button("Auto-Detect")) + { + FillAvatarFields(avatarDescriptor, null, expressionParameters); + } + } + } - using (new SqueezeScope((0, 0, Horizontal, EditorStyles.helpBox))) - { - expressionParameters = (VRCExpressionParameters)EditorGUILayout.ObjectField("Parameters", expressionParameters, typeof(VRCExpressionParameters), true); - if (expressionParameters == null) - if (GUILayout.Button("Auto-Detect")) - FillAvatarFields(avatarDescriptor, avatarFXLayer, null); + using (new SqueezeScope((0, 0, Horizontal, EditorStyles.helpBox))) + { + expressionParameters = (VRCExpressionParameters)EditorGUILayout.ObjectField("Parameters", expressionParameters, typeof(VRCExpressionParameters), true); + if (expressionParameters is null) + { + if (GUILayout.Button("Auto-Detect")) + { + FillAvatarFields(avatarDescriptor, avatarFXLayer, null); + } + } + } } if (!runOnce) { - FillAvatarFields(null, null, null); + // skip for component editor + if (_component is null) + { + FillAvatarFields(null, null, null); + } + runOnce = true; } @@ -159,7 +221,9 @@ void OnAvatarChange() { GUI.backgroundColor = Color.green; if (GUILayout.Button("On", GUILayout.Width(203))) + { changeDetectionEnabled = !changeDetectionEnabled; + } GUI.backgroundColor = Color.white; } @@ -167,19 +231,22 @@ void OnAvatarChange() { GUI.backgroundColor = Color.red; if (GUILayout.Button("Off", GUILayout.Width(203))) + { changeDetectionEnabled = !changeDetectionEnabled; + } GUI.backgroundColor = Color.white; } } + GUILayout.Space(5); } + GUILayout.Space(5); - if (avatarDescriptor != null && avatarFXLayer != null && expressionParameters != null) + + if (avatarDescriptor is not null && avatarFXLayer is not null && expressionParameters is not null) { - longestParamName = 0; - foreach (var x in expressionParameters.parameters) - longestParamName = Math.Max(longestParamName, x.name.Count()); + longestParamName = expressionParameters.parameters.Max(x => x.name.Length); using (new SqueezeScope((0, 0, Horizontal, EditorStyles.helpBox))) { @@ -187,98 +254,143 @@ void OnAvatarChange() if (GUILayout.Button("Deselect Prefix")) { - EditorInputDialog.Show("", "Please enter your prefix to deselect", "", name => + EditorInputDialog.Show("", "Please enter your prefix to deselect", "", prefix => { - if (!string.IsNullOrEmpty(name)) - foreach (MemoryOptimizerMain.MemoryOptimizerListData param in paramList.FindAll(x => x.param.name.StartsWith(name, true, null))) param.selected = false; - OnChangeUpdate(); + using (new TESPerformanceLogger("MemoryOptimizer.OnGUI:'Deselect Prefix' finished in {0}")) + { + // skip for null or empty prefix + if (string.IsNullOrEmpty(prefix)) + { + return; + } + + foreach (var data in paramList.Where(data => data.param.name.StartsWith(prefix, true, null))) + { + data.selected = false; + } + + OnChangeUpdate(); + } }); } if (GUILayout.Button("Select All")) { - foreach (MemoryOptimizerMain.MemoryOptimizerListData param in paramList) param.selected = true; - OnChangeUpdate(); + using (new TESPerformanceLogger("MemoryOptimizer.OnGUI:'Select All' finished in {0}")) + { + foreach (MemoryOptimizerListData param in paramList) + { + param.selected = true; + } + + OnChangeUpdate(); + } } if (GUILayout.Button("Clear Selected Parameters")) + { ResetParamSelection(); + } } scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true); - + + // make sure the param list is always the same size as avatar's expression parameters + if (paramList.Count != expressionParameters.parameters.Length) + { + ResetParamSelection(); + } + + var isSystemInstalled = MemoryOptimizerHelper.IsSystemInstalled(avatarFXLayer); + for (int i = 0; i < expressionParameters.parameters.Length; i++) { + var data = paramList[i]; + var param = expressionParameters.parameters[i]; + using (new SqueezeScope((0, 0, Horizontal))) { - //make sure the param list is always the same size as avatar's expression parameters - if (paramList == null || paramList.Count != expressionParameters.parameters.Length) - ResetParamSelection(); - using (new SqueezeScope((0, 0, Horizontal))) { GUI.enabled = false; - EditorGUILayout.TextArea(expressionParameters.parameters[i].name, GUILayout.MinWidth(longestParamName * 8)); - EditorGUILayout.Popup((int)expressionParameters.parameters[i].valueType, paramTypes, GUILayout.Width(50)); - //EditorGUILayout.Toggle(avatarDescriptor.expressionParameters.parameters[i].networkSynced, GUILayout.MaxWidth(15)); + EditorGUILayout.TextArea(param.name, GUILayout.MinWidth(longestParamName * 8)); + EditorGUILayout.Popup((int)param.valueType, paramTypes, GUILayout.Width(50)); + GUI.enabled = true; - //System already installed - if (MemoryOptimizerMain.IsSystemInstalled(avatarFXLayer)) + // system already installed + if (isSystemInstalled) { GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 1); GUI.enabled = false; GUILayout.Button("System Already Installed!", GUILayout.Width(203)); } - //Param isn't network synced - else if (!expressionParameters.parameters[i].networkSynced) + // param isn't network synced + else if (!param.networkSynced) { GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 1); GUI.enabled = false; GUILayout.Button("Param Not Synced", GUILayout.Width(203)); } - //Param isn't in FX layer - else if (!(avatarFXLayer.parameters.Count(x => x.name == expressionParameters.parameters[i].name) > 0)) + // ignore check if parameter editor otherwise check if param isn't in FX layer + else if (_component is null && !avatarFXLayer.parameters.Any(x => x.name.Equals(param.name))) { - paramList[i].selected = false; + data.selected = false; GUI.backgroundColor = Color.yellow; if (GUILayout.Button("Add To FX", GUILayout.Width(100))) + { avatarFXLayer.AddUniqueParam(paramList[i].param.name); + } GUI.enabled = false; GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 1); GUILayout.Button("Param Not In FX", GUILayout.Width(100)); } - //Param isn't selected - else if (!paramList[i].selected) + // Param isn't selected + else if (!data.selected) { GUI.backgroundColor = Color.red; using (new ChangeCheckScope(OnChangeUpdate)) + { if (GUILayout.Button("Optimize", GUILayout.Width(203))) - paramList[i].selected = !paramList[i].selected; + { + data.selected = !data.selected; + } + } } - //Param won't be optimized - else if (!paramList[i].willBeOptimized) + // Param won't be optimized + else if (!data.willBeOptimized) { GUI.backgroundColor = Color.yellow; using (new ChangeCheckScope(OnChangeUpdate)) + { if (GUILayout.Button("Optimize", GUILayout.Width(203))) - paramList[i].selected = !paramList[i].selected; + { + data.selected = !data.selected; + } + } } - //Param will be optimized + // Param will be optimized else { GUI.backgroundColor = Color.green; using (new ChangeCheckScope(OnChangeUpdate)) + { if (GUILayout.Button("Optimize", GUILayout.Width(203))) - paramList[i].selected = !paramList[i].selected; + { + data.selected = !data.selected; + } + } } + GUI.enabled = true; } } + GUI.backgroundColor = Color.white; } + GUILayout.EndScrollView(); using (new SqueezeScope((0, 0, EditorH))) @@ -294,19 +406,10 @@ void OnAvatarChange() LabelWithHelpBox($"Amount You Will Save: {expressionParameters.CalcTotalCost() - newParamCost}"); LabelWithHelpBox($"Total Sync Time: {syncSteps * stepDelay}s"); } - /* - using (new SqueezeScope((0, 0, EditorH))) - { - LabelWithHelpBox($"Bools to be optimized: {boolsToOptimize.Count}"); - LabelWithHelpBox($"Ints and Floats to be optimized: {intsNFloatsToOptimize.Count}"); - LabelWithHelpBox($"Indexers: {installationIndexers}"); - LabelWithHelpBox($"Bool Syncers: {installationBoolSyncers}"); - LabelWithHelpBox($"Int Syncers: {installationIntSyncers}"); - } - */ + using (new SqueezeScope((0, 0, EditorH, EditorStyles.helpBox))) { - if (MemoryOptimizerMain.IsSystemInstalled(avatarFXLayer)) + if (isSystemInstalled) { GUI.backgroundColor = Color.black; GUILayout.Label("System Already Installed!", EditorStyles.boldLabel); @@ -324,28 +427,77 @@ void OnAvatarChange() { GUILayout.Label("Syncing Steps", GUILayout.MaxWidth(100)); using (new ChangeCheckScope(OnChangeUpdate)) + { syncSteps = EditorGUILayout.IntSlider(syncSteps, 2, unlockSyncSteps ? maxSyncSteps : Math.Min(maxSyncSteps, 4)); + } } + GUI.backgroundColor = Color.white; } + GUI.enabled = true; } + if (syncSteps > maxSyncSteps) + { syncSteps = maxSyncSteps; + } - if (MemoryOptimizerMain.IsSystemInstalled(avatarFXLayer)) + if (_component is null) { - if (GUILayout.Button("Uninstall")) - MemoryOptimizerMain.UninstallMemOpt(avatarDescriptor, avatarFXLayer, expressionParameters); + if (MemoryOptimizerHelper.IsSystemInstalled(avatarFXLayer)) + { + if (GUILayout.Button("Uninstall")) + { + MemoryOptimizerMain.UninstallMemOpt(avatarDescriptor, avatarFXLayer, expressionParameters); + } + } + else + { + GUI.enabled = false; + GUILayout.Button("Uninstall"); + GUI.enabled = true; + } } - else + + if (_component is not null) { - GUI.enabled = false; - GUILayout.Button("Uninstall"); - GUI.enabled = true; - } + if (expressionParameters.parameters.Length + (installationBoolSyncers + installationIntSyncers + installationIndexers) >= maxUnsyncedParams) + { + GUI.enabled = false; + GUI.backgroundColor = Color.red; + GUILayout.Button($"Generated params will exceed {maxUnsyncedParams}!"); + } + else if (GUILayout.Button("Save")) + { + // write our configuration back + _component.wdOption = wdOptionSelected; + _component.syncSteps = syncSteps; + _component.stepDelay = stepDelay; + _component.changeDetection = changeDetectionEnabled; + + foreach (var param in paramList) + { + var match = _component.parameterConfigs.FirstOrDefault(p => p.param.name.Equals(param.param.name) && p.param.valueType == param.param.valueType); - if (MemoryOptimizerMain.IsSystemInstalled(avatarFXLayer)) + // skip if we have no match + if (match is null) + { + continue; + } + + match.selected = param.selected; + match.willBeOptimized = param.willBeOptimized; + } + + // clear reference + _component = null; + + // close the window + Close(); + } + } + else if (MemoryOptimizerHelper.IsSystemInstalled(avatarFXLayer)) { GUI.enabled = false; GUI.backgroundColor = Color.black; @@ -381,77 +533,118 @@ void OnAvatarChange() { backupMode = EditorPrefs.GetInt(backUpModeEPKey); if (backupMode == 0) + { MakeBackupOf(new List { avatarFXLayer, expressionParameters }, currentSavePath + "/Backup/"); + } else if (backupMode == 2) + { if (EditorUtility.DisplayDialog("", "Do you want to make a backup of your controller and parameters?", "Yes", "No")) + { MakeBackupOf(new List { avatarFXLayer, expressionParameters }, currentSavePath + "/Backup/"); + } + } MemoryOptimizerMain.InstallMemOpt(avatarDescriptor, avatarFXLayer, expressionParameters, boolsToOptimize, intsNFloatsToOptimize, syncSteps, stepDelay, changeDetectionEnabled, wdOptionSelected, currentSavePath); } } } + break; case 1: - using (new SqueezeScope((0,0, Vertical, EditorStyles.helpBox))) + using (new SqueezeScope((0, 0, Vertical, EditorStyles.helpBox))) { - //Backup Mode - using (new SqueezeScope((0, 0, Horizontal, EditorStyles.helpBox))) + // hide backup mode for component editing + if (_component is null) { - EditorGUILayout.LabelField("Backup Mode: ", EditorStyles.boldLabel); - EditorPrefs.SetInt(backUpModeEPKey, EditorGUILayout.Popup(backupMode, backupModes, new GUIStyle(EditorStyles.popup) { fixedHeight = 18, stretchWidth = false })); + // Backup Mode + using (new SqueezeScope((0, 0, Horizontal, EditorStyles.helpBox))) + { + EditorGUILayout.LabelField("Backup Mode: ", EditorStyles.boldLabel); + EditorPrefs.SetInt(backUpModeEPKey, EditorGUILayout.Popup(backupMode, backupModes, new GUIStyle(EditorStyles.popup) { fixedHeight = 18, stretchWidth = false })); + } + + GUILayout.Space(5); } - GUILayout.Space(5); + // Unlock sync steps button + GUI.backgroundColor = unlockSyncSteps ? Color.green : Color.red; - //Unlock sync steps button - if (unlockSyncSteps) - GUI.backgroundColor = Color.green; - else - GUI.backgroundColor = Color.red; if (GUILayout.Button("Unlock sync steps")) + { EditorPrefs.SetBool(unlockSyncStepsEPKey, !unlockSyncSteps); + } + GUI.backgroundColor = Color.white; GUILayout.Space(5); - //save path - using (new SqueezeScope((0, 0, Vertical, EditorStyles.helpBox))) + // hide save path for component editing + if (_component is null) { - using (new SqueezeScope((0, 0, Horizontal))) + // save path + using (new SqueezeScope((0, 0, Vertical, EditorStyles.helpBox))) { - using (new ChangeCheckScope(SavePathChange)) + using (new SqueezeScope((0, 0, Horizontal))) { - EditorGUILayout.LabelField("Select folder to save generated assets to: "); - savePathOverride = (DefaultAsset)EditorGUILayout.ObjectField("", savePathOverride, typeof(DefaultAsset), false); + using (new ChangeCheckScope(SavePathChange)) + { + EditorGUILayout.LabelField("Select folder to save generated assets to: "); + savePathOverride = (DefaultAsset)EditorGUILayout.ObjectField("", savePathOverride, typeof(DefaultAsset), false); + } + + void SavePathChange() + { + PlayerPrefs.SetString(savePathPPKey, AssetDatabase.GetAssetPath(savePathOverride)); + } + } + + if (savePathOverride && AssetDatabase.IsValidFolder(AssetDatabase.GetAssetPath(savePathOverride))) + { + EditorGUILayout.HelpBox($"Valid folder! Now saving to: {currentSavePath}", MessageType.Info, true); } - void SavePathChange() + else { - PlayerPrefs.SetString(savePathPPKey, AssetDatabase.GetAssetPath(savePathOverride)); + EditorGUILayout.HelpBox($"Not valid! Now saving to: {currentSavePath}", MessageType.Info, true); } } - if (savePathOverride && AssetDatabase.IsValidFolder(AssetDatabase.GetAssetPath(savePathOverride))) - EditorGUILayout.HelpBox($"Valid folder! Now saving to: {currentSavePath}", MessageType.Info, true); - else - EditorGUILayout.HelpBox($"Not valid! Now saving to: {currentSavePath}", MessageType.Info, true); + GUILayout.Space(5); } - GUILayout.Space(5); - - //Step delay + // Step delay using (new SqueezeScope((0, 0, Vertical, EditorStyles.helpBox))) { - EditorGUILayout.HelpBox($"Not recommended editing!", MessageType.Error, true); + EditorGUILayout.HelpBox("It is recommended not editing this value!", MessageType.Error, true); using (new SqueezeScope((0, 0, Horizontal))) { GUILayout.Label("Step delay", GUILayout.MaxWidth(100)); using (new ChangeCheckScope(OnChangeUpdate)) + { stepDelay = EditorGUILayout.FloatField(stepDelay); + } } + if (GUILayout.Button("Reset value")) + { stepDelay = 0.2f; + } + } + + if (_component is not null) + { + // delete orphans option + using (new SqueezeScope((0, 0, Vertical, EditorStyles.helpBox))) + { + EditorGUILayout.HelpBox("Clearing Orphans will delete their configurations!", MessageType.Error, true); + GUILayout.Label($"Current Orphan count: {_component.parameterConfigs.Count(x => x.isOrphanParameter)}"); + if (GUILayout.Button("Clear Orphans")) + { + _component.ClearOrphans(); + } + } } } + break; } @@ -459,109 +652,223 @@ void SavePathChange() GUI.enabled = true; } - public void OnChangeUpdate() + /// + /// Attempts to reconnect with a MemoryOptimizerComponent on the avatar + /// + private bool TryReconnectingWithComponent() { - if (paramList == null) ResetParamSelection(); + if (_nextReconnectCheck > DateTime.Now) + { + return false; + } + + if (avatarDescriptor?.gameObject.GetComponent() is not { } foundComponent) + { + // only try to reconnect all 10 seconds + _nextReconnectCheck = DateTime.Now.AddSeconds(10); + return false; + } + + _nextReconnectCheck = DateTime.Now.AddDays(-1); + + _component = foundComponent; + + LoadFromComponent(); - foreach (MemoryOptimizerMain.MemoryOptimizerListData param in paramList) + return true; + } + + private void LoadFromComponent() + { + avatarDescriptor = _component.gameObject.GetComponent(); + + avatarFXLayer = new AnimatorController(); + expressionParameters = CreateInstance(); + + avatarFXLayer.name = "Temporary Generated FX Layer"; + expressionParameters.name = "Temporary Generated Parameters"; + + expressionParameters.parameters = _component.parameterConfigs.Where(p => !p.isOrphanParameter).Select(s => { - param.willBeOptimized = false; - if (avatarFXLayer && (!param.param.networkSynced || !(avatarFXLayer.parameters.Count(x => x.name == param.param.name) > 0))) - param.selected = false; + avatarFXLayer.AddUniqueParam(s.param.name, s.param.valueType.ValueTypeToParamType()); + return s.param; + }).ToArray(); + + if (_component.syncSteps > 4) + { + unlockSyncSteps = true; + EditorPrefs.SetBool(unlockSyncStepsEPKey, true); } - selectedBools = new List(); - selectedIntsNFloats = new List(); - foreach(var param in paramList) + wdOptionSelected = _component.wdOption; + maxSyncSteps = unlockSyncSteps ? 32 : 4; + syncSteps = _component.syncSteps; + stepDelay = _component.stepDelay; + changeDetectionEnabled = _component.changeDetection; + + OnChangeUpdate(); + } + + private void OnChangeUpdate() + { + selectedBools = new List(); + selectedIntsNFloats = new List(); + + foreach (var param in paramList) { - if(param.selected) + param.willBeOptimized = false; + + // ignore for component editing + if (_component is null && avatarFXLayer is not null && (!param.param.networkSynced || !(avatarFXLayer.parameters.Count(x => x.name == param.param.name) > 0))) + { + param.selected = false; + } + + if (param.selected) + { + switch (param.param.valueType) { - if(param.param.valueType == VRCExpressionParameters.ValueType.Bool) + case VRCExpressionParameters.ValueType.Bool: selectedBools.Add(param); - else + break; + case VRCExpressionParameters.ValueType.Int: + case VRCExpressionParameters.ValueType.Float: + default: selectedIntsNFloats.Add(param); + break; } + } } - maxSyncSteps = Math.Max(Math.Max(selectedBools.Count(), selectedIntsNFloats.Count()), 1); + maxSyncSteps = Math.Max(Math.Max(selectedBools.Count, selectedIntsNFloats.Count), 1); if (maxSyncSteps == 1) { installationIndexers = 0; installationBoolSyncers = 0; installationIntSyncers = 0; - newParamCost = expressionParameters == null ? 0 : expressionParameters.CalcTotalCost(); + + newParamCost = expressionParameters?.CalcTotalCost() ?? 0; + return; } + if (syncSteps < 2) + { syncSteps = 2; + } + + var allocationBool = selectedBools.Count - (selectedBools.Count % syncSteps); + var allocationIntNFloat = selectedIntsNFloats.Count - (selectedIntsNFloats.Count % syncSteps); + + boolsToOptimize = new(allocationBool); + intsNFloatsToOptimize = new(allocationIntNFloat); + + foreach (var param in selectedBools) + { + if (allocationBool > 0) + { + param.willBeOptimized = true; + + boolsToOptimize.Add(param); + } - boolsToOptimize = selectedBools.Take(selectedBools.Count - (selectedBools.Count % syncSteps)).ToList(); - intsNFloatsToOptimize = selectedIntsNFloats.Take(selectedIntsNFloats.Count - (selectedIntsNFloats.Count % syncSteps)).ToList(); + allocationBool--; + } - foreach (MemoryOptimizerMain.MemoryOptimizerListData param in boolsToOptimize) - param.willBeOptimized = true; - foreach (MemoryOptimizerMain.MemoryOptimizerListData param in intsNFloatsToOptimize) - param.willBeOptimized = true; - - installationIndexers = (syncSteps - 1).DecimalToBinary().ToString().Count(); + foreach (var param in selectedIntsNFloats) + { + if (allocationIntNFloat > 0) + { + param.willBeOptimized = true; + + intsNFloatsToOptimize.Add(param); + } + + allocationIntNFloat--; + } + + installationIndexers = Convert.ToString((syncSteps - 1), 2).Length; installationBoolSyncers = boolsToOptimize.Count / syncSteps; - installationIntSyncers = intsNFloatsToOptimize.Count / syncSteps; + installationIntSyncers = intsNFloatsToOptimize.Count / syncSteps; newParamCost = expressionParameters.CalcTotalCost() + installationIndexers + installationBoolSyncers + (installationIntSyncers * 8) - (boolsToOptimize.Count + (intsNFloatsToOptimize.Count * 8)); } - public void ResetParamSelection() + private void ResetParamSelection() { - paramList = new List(); + paramList = new List(); - if (expressionParameters != null && expressionParameters.parameters.Length > 0) + if (_component is not null) { - foreach (VRCExpressionParameters.Parameter param in expressionParameters.parameters) - paramList.Add(new MemoryOptimizerMain.MemoryOptimizerListData(param, false, false)); + foreach (var value in _component.parameterConfigs.Where(p => !p.isOrphanParameter)) + { + paramList.Add(value.CopyBase()); + } } - - maxSyncSteps = 1; - syncSteps = 1; + else + { + if (expressionParameters is not null && expressionParameters.parameters.Length > 0) + { + foreach (var param in expressionParameters.parameters) + { + paramList.Add(new MemoryOptimizerListData(param, false, false)); + } + } + + maxSyncSteps = 1; + syncSteps = 1; + } + OnChangeUpdate(); } - public void FillAvatarFields(VRCAvatarDescriptor descriptor, AnimatorController controller, VRCExpressionParameters parameters) + private void FillAvatarFields(VRCAvatarDescriptor descriptor, AnimatorController controller, VRCExpressionParameters parameters) { - if (descriptor == null) + if (descriptor is null) + { avatarDescriptor = FindObjectOfType(); + } else { - if (controller == null) + if (controller is null) + { avatarFXLayer = FindFXLayer(avatarDescriptor); - if (parameters == null) + } + + if (parameters is null) + { expressionParameters = FindExpressionParams(avatarDescriptor); + } } OnChangeUpdate(); } - public class ChangeCheckScope : IDisposable + internal class ChangeCheckScope : IDisposable { public Action callBack; public ChangeCheckScope(Action callBack) { this.callBack = callBack; + EditorGUI.BeginChangeCheck(); } public void Dispose() { if (EditorGUI.EndChangeCheck()) + { callBack(); + } } } - public class SqueezeScope : IDisposable + internal class SqueezeScope : IDisposable { private readonly SqueezeSettings[] settings; - public enum SqueezeScopeType + internal enum SqueezeScopeType { Horizontal, Vertical, @@ -569,38 +876,33 @@ public enum SqueezeScopeType EditorV } - public SqueezeScope(SqueezeSettings input) : this(new[] { input }) + internal SqueezeScope(SqueezeSettings input) : this(new[] { input }) { } - public SqueezeScope(params SqueezeSettings[] input) + internal SqueezeScope(params SqueezeSettings[] input) { settings = input; foreach (var squeezeSettings in input) { - BeginSqueeze(squeezeSettings); - } - } + switch (squeezeSettings.type) + { + case Horizontal: + GUILayout.BeginHorizontal(squeezeSettings.style); + break; + case Vertical: + GUILayout.BeginVertical(squeezeSettings.style); + break; + case EditorH: + EditorGUILayout.BeginHorizontal(squeezeSettings.style); + break; + case EditorV: + EditorGUILayout.BeginVertical(squeezeSettings.style); + break; + } - private void BeginSqueeze(SqueezeSettings squeezeSettings) - { - switch (squeezeSettings.type) - { - case Horizontal: - GUILayout.BeginHorizontal(squeezeSettings.style); - break; - case Vertical: - GUILayout.BeginVertical(squeezeSettings.style); - break; - case EditorH: - EditorGUILayout.BeginHorizontal(squeezeSettings.style); - break; - case EditorV: - EditorGUILayout.BeginVertical(squeezeSettings.style); - break; + GUILayout.Space(squeezeSettings.width1); } - - GUILayout.Space(squeezeSettings.width1); } public void Dispose() @@ -627,7 +929,7 @@ public void Dispose() } } - public struct SqueezeSettings + internal struct SqueezeSettings { public int width1; public int width2; @@ -639,21 +941,21 @@ public static implicit operator SqueezeSettings((int, int) val) return new SqueezeSettings { width1 = val.Item1, width2 = val.Item2, type = Horizontal, style = GUIStyle.none }; } - public static implicit operator SqueezeSettings((int, int, SqueezeScopeType) val) + public static implicit operator SqueezeSettings((int, int, /* For some reason Unity can't resolve SqueezeScopeType unless SqueezeScope is specified */ SqueezeScope.SqueezeScopeType) val) { return new SqueezeSettings { width1 = val.Item1, width2 = val.Item2, type = val.Item3, style = GUIStyle.none }; } - public static implicit operator SqueezeSettings((int, int, SqueezeScopeType, GUIStyle) val) + public static implicit operator SqueezeSettings((int, int, /* For some reason Unity can't resolve SqueezeScopeType unless SqueezeScope is specified */ SqueezeScope.SqueezeScopeType, GUIStyle) val) { return new SqueezeSettings { width1 = val.Item1, width2 = val.Item2, type = val.Item3, style = val.Item4 }; } } - //https://forum.unity.com/threads/is-there-a-way-to-input-text-using-a-unity-editor-utility.473743/#post-7191802 - //https://forum.unity.com/threads/is-there-a-way-to-input-text-using-a-unity-editor-utility.473743/#post-7229248 - //Thanks to JelleJurre for help - public class EditorInputDialog : EditorWindow + // https://forum.unity.com/threads/is-there-a-way-to-input-text-using-a-unity-editor-utility.473743/#post-7191802 + // https://forum.unity.com/threads/is-there-a-way-to-input-text-using-a-unity-editor-utility.473743/#post-7229248 + // Thanks to JelleJurre for help + internal class EditorInputDialog : EditorWindow { string description, inputText; string okButton, cancelButton; @@ -663,7 +965,6 @@ public class EditorInputDialog : EditorWindow bool shouldClose = false; Vector2 maxScreenPos; - #region OnGUI() void OnGUI() { // Check if Esc/Return have been pressed @@ -677,7 +978,6 @@ void OnGUI() shouldClose = true; e.Use(); break; - // Enter pressed case KeyCode.Return: case KeyCode.KeypadEnter: @@ -689,9 +989,9 @@ void OnGUI() } if (shouldClose) - { // Close this dialog + { + // Close this dialog Close(); - //return; } // Draw our control @@ -703,7 +1003,8 @@ void OnGUI() EditorGUILayout.Space(8); GUI.SetNextControlName("inText"); inputText = EditorGUILayout.TextField("", inputText); - GUI.FocusControl("inText"); // Focus text field + // Focus text field + GUI.FocusControl("inText"); EditorGUILayout.Space(12); // Draw OK / Cancel buttons @@ -718,7 +1019,8 @@ void OnGUI() r.x += r.width; if (GUI.Button(r, cancelButton)) { - inputText = null; // Cancel - delete inputText + // Cancel - delete inputText + inputText = null; shouldClose = true; } @@ -748,9 +1050,7 @@ void OnGUI() Focus(); } } - #endregion OnGUI() - #region Show() /// /// Returns text player entered, or null if player cancelled the dialog. /// @@ -768,7 +1068,9 @@ public static void Show(string title, string description, string inputText, Acti var maxPos = GUIUtility.GUIToScreenPoint(new Vector2(Screen.width, Screen.height)); if (EditorWindow.HasOpenInstances()) + { return; + } var window = CreateInstance(); window.maxScreenPos = maxPos; @@ -780,7 +1082,6 @@ public static void Show(string title, string description, string inputText, Acti window.onOKButton += () => callBack(window.inputText); window.ShowPopup(); } - #endregion Show() } } -} +} \ No newline at end of file diff --git a/Editor/Patcher.meta b/Editor/Patcher.meta new file mode 100644 index 0000000..c00cfe2 --- /dev/null +++ b/Editor/Patcher.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5e91901a6e191b2489709026072734c0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Patcher/MemoryOptimizerVRCFuryPatcher.cs b/Editor/Patcher/MemoryOptimizerVRCFuryPatcher.cs new file mode 100644 index 0000000..b489b5d --- /dev/null +++ b/Editor/Patcher/MemoryOptimizerVRCFuryPatcher.cs @@ -0,0 +1,69 @@ +using System.Reflection; +using HarmonyLib; +using JeTeeS.MemoryOptimizer.Helper; +using UnityEngine; + +namespace JeTeeS.MemoryOptimizer.Patcher +{ + public static class FinalValidationServicePatch + { + public static bool PatchedCheckParams() + { + Debug.Log("[MemoryOptimizer] MemoryOptimizerVRCFuryPatcher hello from patched CheckParams!"); + // skip original method + return false; + } + } + + internal static class MemoryOptimizerVRCFuryPatcher + { + private const string HarmonyId = "dev.jetees.memoryoptimizer.Patcher"; + private static bool _refIsPatched = false; + + public static void FindAndPatch() + { +#if MemoryOptimizer_VRCFury_IsInstalled + // check that we are patched + if (!Harmony.HasAnyPatches(HarmonyId)) + { + _refIsPatched = false; + } + + // if we are patched, just skip + if (_refIsPatched) + { + return; + } + + var harmony = new Harmony(HarmonyId); + + // make sure we unpatch everything from us first, just in case + harmony.UnpatchAll(HarmonyId); + + if (ReflectionHelper.FindTypeInAssemblies("VF.Service.FinalValidationService") is not { } vfService) + { + Debug.LogError("[MemoryOptimizer] MemoryOptimizerVRCFuryPatcher failed: did not find type VF.Service.FinalValidationService"); + return; + } + + if (vfService.GetMethod("CheckParams", BindingFlags.NonPublic | BindingFlags.Instance) is not { } original) + { + Debug.LogError("[MemoryOptimizer] MemoryOptimizerVRCFuryPatcher failed: did not find method VF.Service.FinalValidationService.CheckParams"); + return; + } + + if (typeof(FinalValidationServicePatch).GetMethod("PatchedCheckParams", BindingFlags.Public | BindingFlags.Static) is not { } patch) + { + Debug.LogError("[MemoryOptimizer] MemoryOptimizerVRCFuryPatcher failed: did not find method JeTeeS.MemoryOptimizer.PatcherFinalValidationService.PatchedCheckParams"); + return; + } + + harmony.Patch(original, new HarmonyMethod(patch)); + + _refIsPatched = true; + + Debug.Log($"[MemoryOptimizer] MemoryOptimizerVRCFuryPatcher harmony patched {patch} to {original}"); +#endif + } + } +} diff --git a/Editor/Patcher/MemoryOptimizerVRCFuryPatcher.cs.meta b/Editor/Patcher/MemoryOptimizerVRCFuryPatcher.cs.meta new file mode 100644 index 0000000..ee53d84 --- /dev/null +++ b/Editor/Patcher/MemoryOptimizerVRCFuryPatcher.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 630e356928644fc18a7f55eea03a394b +timeCreated: 1727551628 \ No newline at end of file diff --git a/Editor/Pipeline.meta b/Editor/Pipeline.meta new file mode 100644 index 0000000..c8b611a --- /dev/null +++ b/Editor/Pipeline.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 17e44102ea5cc1a4097b348580379389 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Pipeline/MemoryOptimizerIsActuallyUploading.cs b/Editor/Pipeline/MemoryOptimizerIsActuallyUploading.cs new file mode 100644 index 0000000..5ef917c --- /dev/null +++ b/Editor/Pipeline/MemoryOptimizerIsActuallyUploading.cs @@ -0,0 +1,64 @@ +using System.Diagnostics; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.SDKBase.Editor.BuildPipeline; + +namespace JeTeeS.MemoryOptimizer.Pipeline +{ + internal class MemoryOptimizerIsActuallyUploading : IVRCSDKPreprocessAvatarCallback + { + private static bool _isActuallyUploading = false; + + public int callbackOrder => int.MinValue; + + public bool OnPreprocessAvatar(GameObject avatarGameObject) + { + EditorApplication.delayCall += () => _isActuallyUploading = false; + + _isActuallyUploading = IsActuallyUploadingCheck(); + + return true; + } + + private static bool IsActuallyUploadingCheck() + { + if (Application.isPlaying) + { + return false; + } + + var stack = new StackTrace().GetFrames(); + + if (stack is null) + { + return true; + } + + var preprocessFrame = stack + .Select((frame, i) => (frame, i)) + .Where(f => f.frame.GetMethod().Name == "OnPreprocessAvatar" && (f.frame.GetMethod().DeclaringType?.FullName ?? "").StartsWith("VRC.")) + .Select(pair => pair.i) + .DefaultIfEmpty(-1) + .Last(); + + if (preprocessFrame < 0) + { + return false; + } + + if (preprocessFrame >= stack.Length - 1) + { + return true; + } + + var callingClass = stack[preprocessFrame + 1].GetMethod().DeclaringType?.FullName; + return callingClass is null || callingClass.StartsWith("VRC."); + } + + public static bool Check() + { + return _isActuallyUploading; + } + } +} \ No newline at end of file diff --git a/Editor/Pipeline/MemoryOptimizerIsActuallyUploading.cs.meta b/Editor/Pipeline/MemoryOptimizerIsActuallyUploading.cs.meta new file mode 100644 index 0000000..6434a12 --- /dev/null +++ b/Editor/Pipeline/MemoryOptimizerIsActuallyUploading.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9b190dc30d604109a7b1f93277d55f7a +timeCreated: 1728153899 \ No newline at end of file diff --git a/Editor/Pipeline/MemoryOptimizerUploadPipeline.cs b/Editor/Pipeline/MemoryOptimizerUploadPipeline.cs new file mode 100644 index 0000000..d947f65 --- /dev/null +++ b/Editor/Pipeline/MemoryOptimizerUploadPipeline.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using JeTeeS.MemoryOptimizer.Helper; +using JeTeeS.MemoryOptimizer.Shared; +using UnityEditor.Animations; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; +using VRC.SDKBase.Editor.BuildPipeline; +using static JeTeeS.MemoryOptimizer.Shared.MemoryOptimizerConstants; +using static JeTeeS.TES.HelperFunctions.TESHelperFunctions; + +namespace JeTeeS.MemoryOptimizer.Pipeline +{ + internal class MemoryOptimizerUploadPipeline : IVRCSDKPreprocessAvatarCallback + { + // VRCFury runs at -10000 + // VRChat strips components at -1024 + // So we preprocess in between but not too close to the stripping of components + public int callbackOrder => -2048; + + public bool OnPreprocessAvatar(GameObject avatarGameObject) + { + var vrcAvatarDescriptor = avatarGameObject.GetComponent(); + var memoryOptimizer = avatarGameObject.GetComponent(); + + // if we don't have a component don't process + if (memoryOptimizer is null) + { + return PreprocessFinalValidation(vrcAvatarDescriptor); + } + + // generate temporary fx layer if we don't find one, as parameters can be used in other layers as well + // but need to be synced on FX + var fxLayer = FindFXLayer(vrcAvatarDescriptor) ?? GenerateTemporaryFXLayer(vrcAvatarDescriptor); + var avatarParameters = vrcAvatarDescriptor.expressionParameters; + var parameters = Array.Empty(); + + // skip if: + // - no parameters (null or empty) + // - parameters are within budget, optimizing should only be done if we actually need it + // - the system is already installed + // - no configurations + { + var skipReasons = new List(); + + if (avatarParameters is null || avatarParameters?.parameters.Length <= 0 || (avatarParameters?.IsWithinBudget() ?? true)) + { + skipReasons.Add(" - No parameters to optimize."); + } + + if (MemoryOptimizerHelper.IsSystemInstalled(fxLayer)) + { + skipReasons.Add(" - System is already installed."); + } + + // if none are selected, skip + if (memoryOptimizer.parameterConfigs.All(x => !x.selected)) + { + skipReasons.Add(" - No Parameters were selected to be optimized."); + } + + if (skipReasons.Any()) + { + Debug.LogWarning($"[MemoryOptimizer] System was not installed:\n{string.Join("\n", skipReasons)}"); + return PreprocessFinalValidation(vrcAvatarDescriptor); + } + } + + // make the avatar upload clone reference copies to not modify original assets + { + // setup copies + var pathBase = $"Packages/dev.jetees.memoryoptimizer/Temp/Generated_{avatarGameObject.name.SanitizeFileName()}/"; + var (copiedFxLayerSuccess, copiedFxLayer) = MakeCopyOf(fxLayer, pathBase, $"{prefix}_GeneratedFxLayer"); + var (copiedExpressionParametersSuccess, copiedExpressionParameters) = MakeCopyOf(vrcAvatarDescriptor.expressionParameters, pathBase, $"{prefix}_GeneratedExpressionParameters"); + + // throw error when copy fails + if (!copiedFxLayerSuccess || !copiedExpressionParametersSuccess) + { + return false; + } + + fxLayer = copiedFxLayer; + parameters = copiedExpressionParameters.parameters; + + vrcAvatarDescriptor.expressionParameters = copiedExpressionParameters; + for (int i = 0, l = vrcAvatarDescriptor.baseAnimationLayers.Length; i < l; i++) + { + // find the fx layer and replace it + if (vrcAvatarDescriptor.baseAnimationLayers[i] is { type: VRCAvatarDescriptor.AnimLayerType.FX, animatorController: not null }) + { + var layer = vrcAvatarDescriptor.baseAnimationLayers[i]; + + layer.animatorController = fxLayer; + + vrcAvatarDescriptor.baseAnimationLayers[i] = layer; + + break; + } + } + } + + Debug.Log($"[MemoryOptimizer] OnPreprocessAvatar running on {avatarGameObject.name} with {parameters.Length} parameters - loaded configuration: {memoryOptimizer.parameterConfigs.Where(p => p.selected && p.willBeOptimized).ToList().Count}"); + + var parametersBoolToOptimize = new List(memoryOptimizer.parameterConfigs.Count); + var parametersIntNFloatToOptimize = new List(memoryOptimizer.parameterConfigs.Count); + + int countBoolShould = 0, countBoolIs = 0, countIntNFloatShould = 0, countIntNFloatIs = 0; + + foreach (var parameterConfig in memoryOptimizer.parameterConfigs) + { + // ignore orphans + if (parameterConfig.isOrphanParameter) + { + continue; + } + + // find actual parameter + VRCExpressionParameters.Parameter parameter = null; + if (parameterConfig.param.name.StartsWith("VF##_")) + { + parameter = parameters.FirstOrDefault(p => + { + if (p.networkSynced) + { + // match VRCFury parameters by regex replacing VF\d+_ and replacing VF##_ and then matching name and type + if (p.name.StartsWith("VF") && new Regex("^VF\\d+_").Replace(p.name, string.Empty).Equals(parameterConfig.param.name.Replace("VF##_", string.Empty)) && p.valueType == parameterConfig.param.valueType) + { + return true; + } + } + + return false; + }); + } + else + { + parameter = parameters.FirstOrDefault(p => + { + if (p.networkSynced) + { + // match parameters by name and type + if (p.name.Equals(parameterConfig.param.name) && p.valueType == parameterConfig.param.valueType) + { + return true; + } + } + + return false; + }); + } + + switch (parameterConfig.param.valueType) + { + case VRCExpressionParameters.ValueType.Bool: + countBoolShould++; + break; + case VRCExpressionParameters.ValueType.Int: + case VRCExpressionParameters.ValueType.Float: + countIntNFloatShould++; + break; + } + + // didn't find parameter, skip + if (parameter is null) + { + continue; + } + + // add the parameter to the fx layer if it's missing + if (fxLayer.parameters.All(p => !p.name.Equals(parameter.name))) + { + var type = AnimatorControllerParameterType.Float; + switch (parameter.valueType) + { + case VRCExpressionParameters.ValueType.Int: + type = AnimatorControllerParameterType.Int; + break; + case VRCExpressionParameters.ValueType.Bool: + type = AnimatorControllerParameterType.Bool; + break; + default: + case VRCExpressionParameters.ValueType.Float: + // since we already assign float at the beginning, skip + break; + } + + fxLayer.AddUniqueParam(parameter.name, type); + } + + if (parameterConfig.selected && parameterConfig.willBeOptimized) + { + // create copy to reference the proper parameter + var baseCopy = parameterConfig.CopyBase(parameter); + + switch (parameterConfig.param.valueType) + { + case VRCExpressionParameters.ValueType.Bool: + countBoolIs++; + parametersBoolToOptimize.Add(baseCopy); + break; + case VRCExpressionParameters.ValueType.Int: + case VRCExpressionParameters.ValueType.Float: + countIntNFloatIs++; + parametersIntNFloatToOptimize.Add(baseCopy); + break; + } + } + } + + // ensure the bool parameters that are left still match what we can optimize + if (countBoolIs < countBoolShould) + { + var newBoolCount = parametersBoolToOptimize.Count - (parametersBoolToOptimize.Count % memoryOptimizer.syncSteps); + Debug.LogWarning($"[MemoryOptimizer] Bool count did not match, expected: {countBoolShould} got: {countBoolIs} - now taking: {newBoolCount}"); + parametersBoolToOptimize = parametersBoolToOptimize.Take(newBoolCount).ToList(); + } + + // ensure the int and float parameters that are left still match what we can optimize + if (countIntNFloatIs < countIntNFloatShould) + { + var newIntNFloatCount = parametersIntNFloatToOptimize.Count - (parametersIntNFloatToOptimize.Count % memoryOptimizer.syncSteps); + Debug.LogWarning($"[MemoryOptimizer] IntNFloat count did not match, expected: {countIntNFloatShould} got: {countIntNFloatIs} - now taking: {newIntNFloatCount}"); + parametersIntNFloatToOptimize = parametersIntNFloatToOptimize.Take(newIntNFloatCount).ToList(); + } + + if (parametersBoolToOptimize.Any() || parametersIntNFloatToOptimize.Any()) + { + MemoryOptimizerMain.InstallMemOpt(vrcAvatarDescriptor, fxLayer, vrcAvatarDescriptor.expressionParameters, parametersBoolToOptimize, parametersIntNFloatToOptimize, memoryOptimizer.syncSteps, memoryOptimizer.stepDelay, memoryOptimizer.changeDetection, memoryOptimizer.wdOption, defaultSavePath); + + Debug.Log($"[MemoryOptimizer] OnPreprocessAvatar optimized:\n- Bools:\n{string.Join("\n", parametersBoolToOptimize.Select(p => $" > {p.param.name}"))}\n- IntNFloats:\n{string.Join("\n", parametersIntNFloatToOptimize.Select(p => $" > {p.param.name}"))}"); + + return PreprocessFinalValidation(vrcAvatarDescriptor); + } + + Debug.LogWarning("[MemoryOptimizer] System was not installed as there were no parameters to optimize."); + + return PreprocessFinalValidation(vrcAvatarDescriptor); + } + + private static bool PreprocessFinalValidation(VRCAvatarDescriptor vrcAvatarDescriptor) + { + var shouldFail = false; + if (vrcAvatarDescriptor.expressionParameters.parameters.Length > 8192) + { + Debug.LogError($"[MemoryOptimizer] Exceeded maximum expression parameter count of 8192."); + + shouldFail = true; + } + + if (!vrcAvatarDescriptor.expressionParameters.IsWithinBudget()) + { + Debug.LogError($"[MemoryOptimizer] Exceeded maximum synced expression parameter bit count: {vrcAvatarDescriptor.expressionParameters.CalcTotalCost()}/{VRCExpressionParameters.MAX_PARAMETER_COST}."); + + shouldFail = true; + } + + // only return the actual fail status if we actually are uploading + if (MemoryOptimizerIsActuallyUploading.Check()) + { + // invert since false -> failed + return !shouldFail; + } + + if (shouldFail) + { + Debug.LogWarning("[MemoryOptimizer] Pipeline passed build even though there were errors, as we are not uploading."); + } + + return true; + } + } +} diff --git a/Editor/Pipeline/MemoryOptimizerUploadPipeline.cs.meta b/Editor/Pipeline/MemoryOptimizerUploadPipeline.cs.meta new file mode 100644 index 0000000..a7bf31d --- /dev/null +++ b/Editor/Pipeline/MemoryOptimizerUploadPipeline.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a61b6e17169c4e93880735fc7e88c45c +timeCreated: 1727383253 \ No newline at end of file diff --git a/Editor/Pipeline/MemoryOptimizerWrapVRCFuryUploadPipeline.cs b/Editor/Pipeline/MemoryOptimizerWrapVRCFuryUploadPipeline.cs new file mode 100644 index 0000000..02c03f2 --- /dev/null +++ b/Editor/Pipeline/MemoryOptimizerWrapVRCFuryUploadPipeline.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Reflection; +using JeTeeS.MemoryOptimizer.Helper; +using JeTeeS.MemoryOptimizer.Patcher; +using UnityEditor.Callbacks; +using UnityEngine; +using VRC.SDKBase.Editor.BuildPipeline; + +namespace JeTeeS.MemoryOptimizer.Pipeline +{ + internal static class MemoryOptimizerWrapVRCFuryUploadPipeline + { + [DidReloadScripts] + public static void FindAndWrap() + { +#if MemoryOptimizer_VRCFury_IsInstalled + var pipelineCallbacks = ReflectionHelper.FindTypeInAssemblies("VRC.SDKBase.Editor.BuildPipeline.VRCBuildPipelineCallbacks"); + var pipelineCallbacksField = pipelineCallbacks?.GetField("_preprocessAvatarCallbacks", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + var pipelineCallbacksValues = pipelineCallbacksField?.GetValue(null); + if (pipelineCallbacksValues is List callbacks) + { + foreach (var callback in callbacks.ToArray()) + { + var callbackType = callback.GetType(); + var callbackTypeName = callbackType.Name; + var callbackAssembly = string.IsNullOrEmpty(callbackType.AssemblyQualifiedName) ? "" : callbackType.AssemblyQualifiedName; + + if (callbackTypeName.Equals("VrcPreuploadHook") && callbackAssembly.Contains("VF.Hook")) + { + var wrap = new MemoryOptimizerVRCFuryUploadPipelineWrapper(callback); + callbacks.Remove(callback); + callbacks.Add(wrap); + + Debug.Log($"[MemoryOptimizer] wrapped the VRCFury Upload Pipeline! ({callbackAssembly} -> {wrap.GetType().AssemblyQualifiedName})"); + + break; + } + } + + pipelineCallbacksField.SetValue(null, callbacks); + } +#endif + } + + internal class MemoryOptimizerVRCFuryUploadPipelineWrapper : IVRCSDKPreprocessAvatarCallback + { + private readonly IVRCSDKPreprocessAvatarCallback _wrapped; + + public int callbackOrder => _wrapped?.callbackOrder ?? 0; + + public MemoryOptimizerVRCFuryUploadPipelineWrapper() + { + _wrapped = null; + } + + public MemoryOptimizerVRCFuryUploadPipelineWrapper(IVRCSDKPreprocessAvatarCallback wrap) + { + _wrapped = wrap; + } + + public bool OnPreprocessAvatar(GameObject avatarGameObject) + { + if (_wrapped is null) return true; + + MemoryOptimizerVRCFuryPatcher.FindAndPatch(); + + return _wrapped.OnPreprocessAvatar(avatarGameObject); + } + } + } +} diff --git a/Editor/Pipeline/MemoryOptimizerWrapVRCFuryUploadPipeline.cs.meta b/Editor/Pipeline/MemoryOptimizerWrapVRCFuryUploadPipeline.cs.meta new file mode 100644 index 0000000..6842524 --- /dev/null +++ b/Editor/Pipeline/MemoryOptimizerWrapVRCFuryUploadPipeline.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 00516ee5bfbb467a89829d5be0fb7c13 +timeCreated: 1727475942 \ No newline at end of file diff --git a/Editor/_InternalsVisibleTo.cs b/Editor/_InternalsVisibleTo.cs new file mode 100644 index 0000000..4c0313a --- /dev/null +++ b/Editor/_InternalsVisibleTo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer")] +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer.Component")] diff --git a/Editor/_InternalsVisibleTo.cs.meta b/Editor/_InternalsVisibleTo.cs.meta new file mode 100644 index 0000000..4a42be7 --- /dev/null +++ b/Editor/_InternalsVisibleTo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 96ff128552454cc6bff47bc3ac60fbd0 +timeCreated: 1727468128 \ No newline at end of file diff --git a/Editor/dev.jetees.memoryoptimizer.Editor.asmdef b/Editor/dev.jetees.memoryoptimizer.Editor.asmdef index cdb2a66..8dc6e9a 100644 --- a/Editor/dev.jetees.memoryoptimizer.Editor.asmdef +++ b/Editor/dev.jetees.memoryoptimizer.Editor.asmdef @@ -1,20 +1,30 @@ { - "name": "dev.jetees.memoryoptimizer.Editor", - "references": [ - "VRC.SDK3A", - "VRC.SDK3A.Editor", - "VRC.SDKBase", - "VRC.SDKBase.Editor", - "dev.jetees.memoryoptimizer" - ], - "includePlatforms": [ - "Editor" - ], - "excludePlatforms": [], - "allowUnsafeCode": false, - "autoReferenced": true, - "overrideReferences": false, - "precompiledReferences": [], - "defineConstraints": [], - "optionalUnityReferences": [] + "name": "dev.jetees.memoryoptimizer.Editor", + "rootNamespace": "", + "references": [ + "VRC.SDK3A", + "VRC.SDK3A.Editor", + "VRC.SDKBase", + "VRC.SDKBase.Editor", + "dev.jetees.memoryoptimizer.Component", + "dev.jetees.memoryoptimizer.Helper", + "dev.jetees.memoryoptimizer.Shared" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.vrcfury.vrcfury", + "expression": "0.0.0", + "define": "MemoryOptimizer_VRCFury_IsInstalled" + } + ], + "noEngineReferences": false } \ No newline at end of file diff --git a/Helper.meta b/Helper.meta new file mode 100644 index 0000000..6ff60cc --- /dev/null +++ b/Helper.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1b590918719f41eaa7339ee978744394 +timeCreated: 1727380569 \ No newline at end of file diff --git a/Helper/MemoryOptimizerHelper.cs b/Helper/MemoryOptimizerHelper.cs new file mode 100644 index 0000000..1e39f78 --- /dev/null +++ b/Helper/MemoryOptimizerHelper.cs @@ -0,0 +1,39 @@ +#if UNITY_EDITOR + +using JeTeeS.TES.HelperFunctions; +using UnityEditor.Animations; +using VRC.SDK3.Avatars.Components; +using static JeTeeS.MemoryOptimizer.Shared.MemoryOptimizerConstants; + +namespace JeTeeS.MemoryOptimizer.Helper +{ + internal static class MemoryOptimizerHelper + { + public static bool IsSystemInstalled(VRCAvatarDescriptor descriptor) + { + return IsSystemInstalled(TESHelperFunctions.FindFXLayer(descriptor)); + } + + public static bool IsSystemInstalled(AnimatorController controller) + { + if (controller is null) + { + return false; + } + + if (controller.FindHiddenIdentifier(syncingLayerIdentifier).Count == 1) + { + return true; + } + + if (controller.FindHiddenIdentifier(mainBlendTreeIdentifier).Count == 1) + { + return true; + } + + return false; + } + } +} + +#endif diff --git a/Helper/MemoryOptimizerHelper.cs.meta b/Helper/MemoryOptimizerHelper.cs.meta new file mode 100644 index 0000000..a0a38b6 --- /dev/null +++ b/Helper/MemoryOptimizerHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 89c06548ea0d4983bb65e6b12f3cca0a +timeCreated: 1727874331 \ No newline at end of file diff --git a/Helper/ReadOnlyPropertyHelper.cs b/Helper/ReadOnlyPropertyHelper.cs new file mode 100644 index 0000000..0c3351e --- /dev/null +++ b/Helper/ReadOnlyPropertyHelper.cs @@ -0,0 +1,31 @@ +using System; +using UnityEditor; +using UnityEngine; + +namespace JeTeeS.MemoryOptimizer.Helper +{ + internal static class ReadOnlyPropertyHelper + { + [AttributeUsage(AttributeTargets.Field)] + internal class ReadOnlyAttribute : PropertyAttribute { } + +#if UNITY_EDITOR + [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] + internal class ReadOnlyAttributeDrawer : PropertyDrawer + { + public override void OnGUI(Rect rect, SerializedProperty prop, GUIContent label) + { + bool wasEnabled = GUI.enabled; + GUI.enabled = false; + EditorGUI.PropertyField(rect, prop); + GUI.enabled = wasEnabled; + } + + public override float GetPropertyHeight(SerializedProperty prop, GUIContent label) + { + return EditorGUI.GetPropertyHeight(prop, label, true); + } + } +#endif + } +} \ No newline at end of file diff --git a/Helper/ReadOnlyPropertyHelper.cs.meta b/Helper/ReadOnlyPropertyHelper.cs.meta new file mode 100644 index 0000000..91b82d7 --- /dev/null +++ b/Helper/ReadOnlyPropertyHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b8c926cde99d4af39f3d5b562ccfe40b +timeCreated: 1727380609 \ No newline at end of file diff --git a/Helper/ReflectionHelper.cs b/Helper/ReflectionHelper.cs new file mode 100644 index 0000000..345592a --- /dev/null +++ b/Helper/ReflectionHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; + +namespace JeTeeS.MemoryOptimizer.Helper +{ + internal static class ReflectionHelper + { + internal static object GetFieldValue(this object obj, string field) + { + return obj.GetType().GetField(field).GetValue(obj); + } + + internal static Type FindTypeInAssemblies(string type) + { + return AppDomain.CurrentDomain.GetAssemblies().Select(assembly => assembly.GetType(type)).FirstOrDefault(t => t is not null); + } + } +} \ No newline at end of file diff --git a/Helper/ReflectionHelper.cs.meta b/Helper/ReflectionHelper.cs.meta new file mode 100644 index 0000000..049c219 --- /dev/null +++ b/Helper/ReflectionHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f78219981c3c42d4bbfa12a7a65547a2 +timeCreated: 1727460083 \ No newline at end of file diff --git a/Editor/TESHelperFunctions.cs b/Helper/TESHelperFunctions.cs similarity index 52% rename from Editor/TESHelperFunctions.cs rename to Helper/TESHelperFunctions.cs index fbb3716..7d6a85e 100644 --- a/Editor/TESHelperFunctions.cs +++ b/Helper/TESHelperFunctions.cs @@ -1,121 +1,188 @@ +#if UNITY_EDITOR + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using UnityEngine; using UnityEditor; using UnityEditor.Animations; using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; +using Debug = UnityEngine.Debug; namespace JeTeeS.TES.HelperFunctions { - public static class TESHelperFunctions + internal static class TESHelperFunctions { public static string SanitizeFileName(this string fileName) { - return String.Join("_", fileName.Split(System.IO.Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)); + return string.Join("_", fileName.Split(System.IO.Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)); } + public static void MakeBackupOf(List things, string saveTo) { ReadyPath(saveTo); - foreach (UnityEngine.Object obj in things) + foreach (var obj in things) { - int i = 0; - List splitAssetname = GetAssetName(obj).Split('.').ToList(); - string assetName = ""; - foreach (string strng in splitAssetname.Take(splitAssetname.Count - 1)) + var i = 0; + var splitAssetname = GetAssetName(obj).Split('.').ToList(); + var assetName = ""; + foreach (var strng in splitAssetname.Take(splitAssetname.Count - 1)) + { assetName += strng; + } while (System.IO.File.Exists(saveTo + assetName + $" ({i})." + splitAssetname.Last())) + { i++; + } + if (!AssetDatabase.CopyAsset(AssetDatabase.GetAssetPath(obj), saveTo + assetName + $" ({i})." + splitAssetname.Last())) + { Debug.LogWarning($"Failed to copy '{AssetDatabase.GetAssetPath(obj)}' to path: {saveTo + assetName + $" ({i})." + splitAssetname.Last()}"); + } + } + } + + /// + /// Makes a copy and returns the copy with a success status + /// + /// + /// + /// + /// + public static (bool, T) MakeCopyOf(UnityEngine.Object thing, string saveTo, string newThingName = null) where T : UnityEngine.Object + { + bool success = false; + UnityEngine.Object copiedThing = null; + + ReadyPath(saveTo); + + string assetName = GetAssetName(thing); + string assetFileType = GetFileType(thing); + + string copiedAssetPath = saveTo + (string.IsNullOrEmpty(newThingName) ? assetName[..^$".{assetFileType}".Length] : newThingName) + "." + GetFileType(thing); + + if (!AssetDatabase.CopyAsset(AssetDatabase.GetAssetPath(thing), copiedAssetPath)) + { + Debug.LogWarning($"Failed to copy '{AssetDatabase.GetAssetPath(thing)}' to path: '{copiedAssetPath}'"); + } + else + { + success = true; + copiedThing = AssetDatabase.LoadAssetAtPath(copiedAssetPath); } + + return (success, (T)copiedThing); } + public static string GetFileType(UnityEngine.Object obj) { - string fileName = GetAssetName(obj); - if (!string.IsNullOrEmpty(fileName)) - return fileName.Split('.').ToList().Last(); - return null; + var fileName = GetAssetName(obj); + return !string.IsNullOrEmpty(fileName) ? fileName.Split('.')[^1] : null; } + public static string GetAssetName(string path) { - if (!string.IsNullOrEmpty(path)) - return path.Split(@"\/".ToCharArray()).ToList().Last(); - return null; + return !string.IsNullOrEmpty(path) ? path.Split(@"\/".ToCharArray())[^1] : null; } + public static string GetAssetName(UnityEngine.Object thing) { - string path = AssetDatabase.GetAssetPath(thing); + var path = AssetDatabase.GetAssetPath(thing); return GetAssetName(path); } + public static int UninstallErrorDialogWithDiscordLink(string title, string mainMessage, string discordLink) { - int option = EditorUtility.DisplayDialogComplex(title, mainMessage, "Continue uninstall anyways (not recommended)", "Cancel uninstall", "Join the discord"); - switch (option) - { - case 0: - return 0; - case 1: - return 1; - case 2: - Application.OpenURL(discordLink); - return 2; - default: - Debug.LogError("Unrecognized option."); - return -1; - } + var option = EditorUtility.DisplayDialogComplex(title, mainMessage, "Continue uninstall anyways (not recommended)", "Cancel uninstall", "Join the discord"); + switch (option) + { + case 0: + return 0; + case 1: + return 1; + case 2: + Application.OpenURL(discordLink); + return 2; + default: + Debug.LogError("Unrecognized option."); + return -1; + } } + public static AnimatorController FindFXLayer(VRCAvatarDescriptor descriptor) { - return (AnimatorController)descriptor.baseAnimationLayers.FirstOrDefault(x => x.type == VRCAvatarDescriptor.AnimLayerType.FX && x.animatorController != null).animatorController; + var controller = descriptor.baseAnimationLayers.FirstOrDefault(x => x is { type: VRCAvatarDescriptor.AnimLayerType.FX, animatorController: not null }).animatorController; + + if (controller is AnimatorController fxController) + { + return fxController; + } + + return null; } + public static AnimatorController GenerateTemporaryFXLayer(VRCAvatarDescriptor descriptor) + { + var pathBase = $"Packages/dev.jetees.memoryoptimizer/Temp/Generated_{descriptor.gameObject.name.SanitizeFileName()}/MemOpt_Temp_GeneratedFXLayer.controller"; + + var temp = new AnimatorController() + { + name = "MemOpt_Temp_GeneratedFXLayer" + }; + + AssetDatabase.CreateAsset(temp, pathBase); + AssetDatabase.SaveAssets(); + AssetDatabase.ImportAsset(pathBase, ImportAssetOptions.ForceUpdate); + + return temp; + } + public static VRCExpressionParameters FindExpressionParams(VRCAvatarDescriptor descriptor) { - if (descriptor == null) - return null; - return descriptor.expressionParameters; + return descriptor?.expressionParameters; } public static AnimatorControllerParameterType ValueTypeToParamType(this VRCExpressionParameters.ValueType valueType) { - switch (valueType) + return valueType switch { - case VRCExpressionParameters.ValueType.Float: return AnimatorControllerParameterType.Float; - case VRCExpressionParameters.ValueType.Int: return AnimatorControllerParameterType.Int; - case VRCExpressionParameters.ValueType.Bool: return AnimatorControllerParameterType.Bool; - - default: return AnimatorControllerParameterType.Float; - } + VRCExpressionParameters.ValueType.Float => AnimatorControllerParameterType.Float, + VRCExpressionParameters.ValueType.Int => AnimatorControllerParameterType.Int, + VRCExpressionParameters.ValueType.Bool => AnimatorControllerParameterType.Bool, + _ => AnimatorControllerParameterType.Float + }; } public static VRCExpressionParameters.ValueType ParamTypeToValueType(this AnimatorControllerParameterType paramType) { - switch (paramType) + return paramType switch { - case AnimatorControllerParameterType.Float: return VRCExpressionParameters.ValueType.Float; - case AnimatorControllerParameterType.Int: return VRCExpressionParameters.ValueType.Int; - case AnimatorControllerParameterType.Bool: return VRCExpressionParameters.ValueType.Bool; - - default: return VRCExpressionParameters.ValueType.Float; - } + AnimatorControllerParameterType.Float => VRCExpressionParameters.ValueType.Float, + AnimatorControllerParameterType.Int => VRCExpressionParameters.ValueType.Int, + AnimatorControllerParameterType.Bool => VRCExpressionParameters.ValueType.Bool, + _ => VRCExpressionParameters.ValueType.Float + }; } public static string GetControllerParentFolder(AnimatorController controller) { - List subPaths = controller.GetAssetPath().Split(@"\/".ToCharArray()).ToList(); + var subPaths = controller.GetAssetPath().Split(@"\/".ToCharArray()).ToList(); subPaths.RemoveAt(subPaths.Count - 1); - string returnString = ""; - foreach (string subPath in subPaths) + var returnString = ""; + foreach (var subPath in subPaths) + { returnString += subPath + "/"; + } + return returnString; } public static Vector3 AngleRadiusToPos(float angle, float radius, Vector3 offset) { - Vector3 result = new Vector3((float)Math.Sin(angle) * radius, (float)Math.Cos(angle) * radius, 0); + Vector3 result = new((float)Math.Sin(angle) * radius, (float)Math.Cos(angle) * radius, 0); result += offset; return result; @@ -130,16 +197,20 @@ public static void LabelWithHelpBox(string text) public static int FindLayerIndex(this AnimatorController controller, AnimatorControllerLayer layer) { - for (int i = 0; i < controller.layers.Count(); i++) + for (var i = 0; i < controller.layers.Count(); i++) + { if (controller.layers[i].stateMachine == layer.stateMachine) + { return i; + } + } return -1; } public static void RemoveLayer(this AnimatorController controller, AnimatorControllerLayer layer) { - int i = controller.FindLayerIndex(layer); + var i = controller.FindLayerIndex(layer); if (i == -1) { Debug.LogError("Layer " + layer.name + "was not found in " + controller.name); @@ -151,17 +222,22 @@ public static void RemoveLayer(this AnimatorController controller, AnimatorContr public static List FindAllStatesInLayer(this AnimatorControllerLayer layer) { - List returnList = new List(); + List returnList = new(); - Queue stateMachines = new Queue(); + Queue stateMachines = new(); stateMachines.Enqueue(layer.stateMachine); while (stateMachines.Count > 0) { - AnimatorStateMachine currentStateMachine = stateMachines.Dequeue(); - foreach (ChildAnimatorState state in currentStateMachine.states) + var currentStateMachine = stateMachines.Dequeue(); + foreach (var state in currentStateMachine.states) + { returnList.Add(state); - foreach (ChildAnimatorStateMachine stateMachine in currentStateMachine.stateMachines) + } + + foreach (var stateMachine in currentStateMachine.stateMachines) + { stateMachines.Enqueue(stateMachine.stateMachine); + } } return returnList; @@ -169,20 +245,27 @@ public static List FindAllStatesInLayer(this AnimatorControl public static int FindWDInLayer(this AnimatorControllerLayer layer) { - Queue stateQueue = new Queue(); + Queue stateQueue = new(); foreach (var state in layer.FindAllStatesInLayer()) + { stateQueue.Enqueue(state); + } if (stateQueue.Count == 0) + { return -2; - ChildAnimatorState currentState = stateQueue.Dequeue(); - bool firstWD = currentState.state.writeDefaultValues; + } + + var currentState = stateQueue.Dequeue(); + var firstWD = currentState.state.writeDefaultValues; while (stateQueue.Count > 1) { currentState = stateQueue.Dequeue(); if (currentState.state.writeDefaultValues != firstWD && !currentState.state.name.Contains("WD On")) + { return -1; + } } return Convert.ToInt32(firstWD); @@ -190,15 +273,21 @@ public static int FindWDInLayer(this AnimatorControllerLayer layer) public static int FindWDInController(this AnimatorController controller) { - Queue layerQueue = new Queue(); - AnimatorControllerLayer currentLayer = new AnimatorControllerLayer(); - int firstWD = -2; + Queue layerQueue = new(); + AnimatorControllerLayer currentLayer = new(); + var firstWD = -2; foreach (var layer in controller.layers) + { layerQueue.Enqueue(layer); + } while ((currentLayer.IsBlendTreeLayer() || firstWD == -2) && layerQueue.Count > 1) { - if (firstWD == -1) return -1; + if (firstWD == -1) + { + return -1; + } + currentLayer = layerQueue.Dequeue(); firstWD = currentLayer.FindWDInLayer(); } @@ -207,7 +296,9 @@ public static int FindWDInController(this AnimatorController controller) { currentLayer = layerQueue.Dequeue(); if (currentLayer.FindWDInLayer() != firstWD) + { return -1; + } } return Convert.ToInt32(firstWD); @@ -215,57 +306,105 @@ public static int FindWDInController(this AnimatorController controller) public static bool IsBlendTreeLayer(this AnimatorControllerLayer layer) { - if (layer == null || layer.stateMachine == null) + if (layer?.stateMachine is null) + { return false; + } + foreach (var state in layer.stateMachine.states) + { if (!state.state.name.Contains("WD On")) + { return false; + } + } return true; } public static AnimatorControllerParameter AddUniqueParam(this AnimatorController controller, string paramName, AnimatorControllerParameterType paramType = AnimatorControllerParameterType.Float, float defaultValue = 0) { - foreach (AnimatorControllerParameter param in controller.parameters) + foreach (var param in controller.parameters) { if (param.name == paramName) { if (param.type != paramType) + { Debug.LogError("Parameter " + param.name + " is of type: " + param.type.ToString() + " not " + paramType.ToString() + "!"); + } + return param; } } - AnimatorControllerParameter controllerParam = new AnimatorControllerParameter(); + AnimatorControllerParameter controllerParam = new(); if (paramType == AnimatorControllerParameterType.Float) - controllerParam = new AnimatorControllerParameter() { name = paramName, type = paramType, defaultFloat = defaultValue }; + { + controllerParam = new() + { + name = paramName, + type = paramType, + defaultFloat = defaultValue + }; + } else if (paramType == AnimatorControllerParameterType.Int) - controllerParam = new AnimatorControllerParameter() { name = paramName, type = paramType, defaultInt = ((int)defaultValue) }; + { + controllerParam = new() + { + name = paramName, + type = paramType, + defaultInt = ((int)defaultValue) + }; + } else if (paramType == AnimatorControllerParameterType.Bool) - controllerParam = defaultValue > 0 ? new AnimatorControllerParameter() { name = paramName, type = paramType, defaultBool = true } : new AnimatorControllerParameter() { name = paramName, type = paramType, defaultBool = false }; + { + controllerParam = new() + { + name = paramName, + type = paramType, + defaultBool = defaultValue > 0 + }; + } else + { Debug.LogError("Parameter " + paramName + " is not a float, int or bool??"); + } controller.AddParameter(controllerParam); + return controller.parameters.Last(x => x.name == paramName && x.type == paramType); } public static void AddUniqueSyncedParam(this VRCExpressionParameters vrcExpressionParameters, string name, VRCExpressionParameters.ValueType valueType, bool isNetworkSynced = true, bool isSaved = true, float defaultValue = 0) { - foreach (VRCExpressionParameters.Parameter param in vrcExpressionParameters.parameters) + foreach (var param in vrcExpressionParameters.parameters) { if (param.name == name) { if (param.valueType != valueType) + { Debug.LogError("Parameter " + param.name + " is not of type: " + param.valueType.ToString() + "!"); + } + return; } } - VRCExpressionParameters.Parameter[] newList = new VRCExpressionParameters.Parameter[vrcExpressionParameters.parameters.Length + 1]; - for (int i = 0; i < vrcExpressionParameters.parameters.Length; i++) + var newList = new VRCExpressionParameters.Parameter[vrcExpressionParameters.parameters.Length + 1]; + for (var i = 0; i < vrcExpressionParameters.parameters.Length; i++) + { newList[i] = vrcExpressionParameters.parameters[i]; - newList[newList.Length - 1] = new VRCExpressionParameters.Parameter() { name = name, valueType = valueType, networkSynced = isNetworkSynced, saved = isSaved, defaultValue = defaultValue }; + } + + newList[^1] = new() + { + name = name, + valueType = valueType, + networkSynced = isNetworkSynced, + saved = isSaved, + defaultValue = defaultValue + }; + vrcExpressionParameters.parameters = newList; } @@ -277,19 +416,21 @@ public static void AddUniqueSyncedParamToController(string name, AnimatorControl public static AnimatorControllerLayer AddLayer(this AnimatorController controller, string name, float defaultWeight = 1) { - AnimatorControllerLayer layer = new AnimatorControllerLayer() + AnimatorControllerLayer layer = new() { name = name, defaultWeight = defaultWeight, - stateMachine = new AnimatorStateMachine() { hideFlags = HideFlags.HideInHierarchy }, + stateMachine = new() { hideFlags = HideFlags.HideInHierarchy }, }; + controller.AddLayer(layer); + return layer; } public static void AddHiddenIdentifier(this AnimatorStateMachine animatorStateMachine, string identifierString) { - AnimatorStateTransition identifier = animatorStateMachine.AddAnyStateTransition((AnimatorStateMachine)null); + var identifier = animatorStateMachine.AddAnyStateTransition((AnimatorStateMachine)null); identifier.canTransitionToSelf = false; identifier.mute = true; identifier.isExit = true; @@ -298,14 +439,23 @@ public static void AddHiddenIdentifier(this AnimatorStateMachine animatorStateMa public static List FindHiddenIdentifier(this AnimatorController animatorController, string identifierString) { - if (animatorController == null) + if (animatorController is null) + { return null; - List returnList = new List(); + } + + List returnList = new(); - foreach (AnimatorControllerLayer layer in animatorController.layers) - foreach (AnimatorStateTransition anyStateTransition in layer.stateMachine.anyStateTransitions) + foreach (var layer in animatorController.layers) + { + foreach (var anyStateTransition in layer.stateMachine.anyStateTransitions) + { if (anyStateTransition.isExit && anyStateTransition.mute && anyStateTransition.name == identifierString) + { returnList.Add(layer); + } + } + } return returnList; } @@ -317,26 +467,6 @@ public static void ReadyPath(string path) System.IO.Directory.CreateDirectory(path); AssetDatabase.ImportAsset(path); } - /* - string[] subPaths = path.Split(@"\/".ToCharArray()); - foreach (string subPath in subPaths) Debug.Log(subPath); - string tempPath = ""; - - if (subPaths[subPaths.Length - 1].Contains('.')) subPaths = subPaths.Take(subPaths.Length - 1).ToArray(); - for (int i = 0; i < subPaths.Length; i++) - { - if (i == 0) tempPath += subPaths[i]; - else - { - if (!AssetDatabase.IsValidFolder(tempPath + "/" + subPaths[i])) - { - Debug.Log("Creating Folder " + subPaths[i] + " at " + tempPath); - AssetDatabase.CreateFolder(tempPath, subPaths[i]); - } - tempPath += "/" + subPaths[i]; - } - } - */ } public static AnimationClip MakeAAP(string paramName, string saveAssetsTo, float value = 0, float animLength = 1, string assetName = "") => MakeAAP(new[] { paramName }, saveAssetsTo, value, animLength, assetName); @@ -344,127 +474,171 @@ public static void ReadyPath(string path) public static AnimationClip MakeAAP(string[] paramNames, string saveAssetsTo, float value = 0, float animLength = 1, string assetName = "") { if (paramNames.Length == 0) + { Debug.LogError("param list is empty!"); + } + if (assetName == "") + { assetName = paramNames[0] + "_AAP " + value; - string saveName = assetName.Replace('/', '_').SanitizeFileName(); - AnimationClip animClip = (AnimationClip)AssetDatabase.LoadAssetAtPath(saveAssetsTo + saveName + ".anim", typeof(AnimationClip)); - if (animClip != null) + } + + var saveName = assetName.Replace('/', '_').SanitizeFileName(); + var animClip = (AnimationClip)AssetDatabase.LoadAssetAtPath(saveAssetsTo + saveName + ".anim", typeof(AnimationClip)); + if (animClip is not null) + { return animClip; + } ReadyPath(saveAssetsTo); animLength /= 60f; - animClip = new AnimationClip + animClip = new() { name = assetName, wrapMode = WrapMode.Clamp, }; - foreach (string paramName in paramNames) + + foreach (var paramName in paramNames) { - AnimationCurve curve = new AnimationCurve(); + AnimationCurve curve = new(); curve.AddKey(0, value); curve.AddKey(animLength, value); + animClip.SetCurve("", typeof(Animator), paramName, curve); } AssetDatabase.CreateAsset(animClip, saveAssetsTo + saveName + ".anim"); + return animClip; } public static string GetAssetPath(this UnityEngine.Object item) => AssetDatabase.GetAssetPath(item); + public static void SaveToAsset(this UnityEngine.Object item, UnityEngine.Object saveTo) => AssetDatabase.AddObjectToAsset(item, AssetDatabase.GetAssetPath(saveTo)); public static void SaveUnsavedAssetsToController(this AnimatorController controller) { - Queue childStateMachines = new Queue(); - List states = new List(); - List transitions = new List(); + Queue childStateMachines = new(); + List states = new(); + List transitions = new(); - foreach (AnimatorControllerLayer layer in controller.layers) + foreach (var layer in controller.layers) { if (GetAssetPath(layer.stateMachine) == "") + { layer.stateMachine.SaveToAsset(controller); - foreach (var state in layer.stateMachine.states) - states.Add(state); + } + + states.AddRange(layer.stateMachine.states); - foreach (ChildAnimatorStateMachine tempChildStateMachine in layer.stateMachine.stateMachines) + foreach (var tempChildStateMachine in layer.stateMachine.stateMachines) { childStateMachines.Enqueue(tempChildStateMachine); - foreach (ChildAnimatorState state in tempChildStateMachine.stateMachine.states) - states.Add(state); + states.AddRange(tempChildStateMachine.stateMachine.states); } while (childStateMachines.Count > 0) { - ChildAnimatorStateMachine childStateMachine = childStateMachines.Dequeue(); - foreach (ChildAnimatorStateMachine tempChildStateMachine in childStateMachine.stateMachine.stateMachines) + var childStateMachine = childStateMachines.Dequeue(); + foreach (var tempChildStateMachine in childStateMachine.stateMachine.stateMachines) { childStateMachines.Enqueue(tempChildStateMachine); - foreach (ChildAnimatorState state in tempChildStateMachine.stateMachine.states) - states.Add(state); + states.AddRange(tempChildStateMachine.stateMachine.states); } - if (GetAssetPath(childStateMachine.stateMachine) == "") + if (string.IsNullOrEmpty(GetAssetPath(childStateMachine.stateMachine))) + { childStateMachine.stateMachine.SaveToAsset(controller); + } } - foreach (AnimatorStateTransition anyState in layer.stateMachine.anyStateTransitions) - transitions.Add(anyState); + transitions.AddRange(layer.stateMachine.anyStateTransitions); } - foreach (ChildAnimatorState state in states) + foreach (var state in states) { - foreach (AnimatorStateTransition transition in state.state.transitions) - transitions.Add(transition); - if (GetAssetPath(state.state) == "") + transitions.AddRange(state.state.transitions); + + if (string.IsNullOrEmpty(GetAssetPath(state.state))) + { state.state.SaveToAsset(controller); + } + if (state.state.motion is BlendTree tree) + { SaveUnsavedBlendtreesToController(tree, controller); + } } - foreach (AnimatorStateTransition transition in transitions) - if (GetAssetPath(transition) == "") transition.SaveToAsset(controller); + foreach (var transition in transitions.Where(transition => string.IsNullOrEmpty(GetAssetPath(transition)))) + { + transition.SaveToAsset(controller); + } } public static void SaveUnsavedBlendtreesToController(BlendTree blendTree, UnityEngine.Object saveTo) { - Queue blendTrees = new Queue(); + Queue blendTrees = new(); blendTrees.Enqueue(blendTree); while (blendTrees.Count > 0) { - BlendTree subBlendTree = blendTrees.Dequeue(); - if (GetAssetPath(subBlendTree) == "") subBlendTree.SaveToAsset(saveTo); - foreach (ChildMotion child in subBlendTree.children) - if (child.motion is BlendTree tree) blendTrees.Enqueue(tree); + var subBlendTree = blendTrees.Dequeue(); + if (string.IsNullOrEmpty(GetAssetPath(subBlendTree))) + { + subBlendTree.SaveToAsset(saveTo); + } + + foreach (var child in subBlendTree.children) + { + if (child.motion is BlendTree tree) + { + blendTrees.Enqueue(tree); + } + } } } public static void DeleteBlendTreeFromAsset(BlendTree blendTree) { - Queue blendTrees = new Queue(); + Queue blendTrees = new(); blendTrees.Enqueue(blendTree); while (blendTrees.Count > 0) { - BlendTree subBlendTree = blendTrees.Dequeue(); + var subBlendTree = blendTrees.Dequeue(); AssetDatabase.RemoveObjectFromAsset(subBlendTree); - foreach (ChildMotion child in subBlendTree.children) - if (child.motion is BlendTree tree) blendTrees.Enqueue(tree); + foreach (var child in subBlendTree.children) + { + if (child.motion is BlendTree tree) + { + blendTrees.Enqueue(tree); + } + } } } public static AnimatorControllerParameter AddSmoothedVer(this AnimatorControllerParameter param, float minValue, float maxValue, AnimatorController controller, string smoothedParamName, string saveTo, string smoothingAmountParamName = "SmoothingAmount", string mainBlendTreeIdentifier = "MainBlendTree", string mainBlendTreeLayerName = "MainBlendTree", string smoothingParentTreeName = "SmoothingParentTree", string constantOneName = "ConstantOne") { - BlendTree smoothingParentTree = GetOrGenerateChildTree(controller, smoothingParentTreeName, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); - AnimationClip smoothingAnimMin = MakeAAP(smoothedParamName, saveTo, minValue, 1, smoothedParamName + minValue); - AnimationClip smoothingAnimMax = MakeAAP(smoothedParamName, saveTo, maxValue, 1, smoothedParamName + maxValue); + var smoothingParentTree = GetOrGenerateChildTree(controller, smoothingParentTreeName, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); + + var smoothingAnimMin = MakeAAP(smoothedParamName, saveTo, minValue, 1, smoothedParamName + minValue); + var smoothingAnimMax = MakeAAP(smoothedParamName, saveTo, maxValue, 1, smoothedParamName + maxValue); + controller.AddUniqueParam(smoothingAmountParamName, AnimatorControllerParameterType.Float, 0.1f); - AnimatorControllerParameter constantOneParam = controller.AddUniqueParam(constantOneName, AnimatorControllerParameterType.Float, 1); - AnimatorControllerParameter smoothedParam = controller.AddUniqueParam(smoothedParamName); + + var constantOneParam = controller.AddUniqueParam(constantOneName, AnimatorControllerParameterType.Float, 1); + var smoothedParam = controller.AddUniqueParam(smoothedParamName); - BlendTree smoothedValue = new BlendTree() { blendType = BlendTreeType.Simple1D, blendParameter = smoothedParamName, name = smoothedParamName, useAutomaticThresholds = false, hideFlags = HideFlags.HideInHierarchy }; + BlendTree smoothedValue = new() + { + blendType = BlendTreeType.Simple1D, + blendParameter = smoothedParamName, + name = smoothedParamName, + useAutomaticThresholds = false, + hideFlags = HideFlags.HideInHierarchy + }; - ChildMotion[] tempChildren = new ChildMotion[2]; + var tempChildren = new ChildMotion[2]; tempChildren[0].motion = smoothingAnimMin; tempChildren[0].timeScale = 1; tempChildren[0].threshold = minValue; @@ -475,7 +649,15 @@ public static AnimatorControllerParameter AddSmoothedVer(this AnimatorController smoothedValue.children = tempChildren; - BlendTree originalValue = new BlendTree() { blendType = BlendTreeType.Simple1D, blendParameter = param.name, name = param.name + "_Original", useAutomaticThresholds = false, hideFlags = HideFlags.HideInHierarchy }; + BlendTree originalValue = new() + { + blendType = BlendTreeType.Simple1D, + blendParameter = param.name, + name = param.name + "_Original", + useAutomaticThresholds = false, + hideFlags = HideFlags.HideInHierarchy + }; + tempChildren = new ChildMotion[2]; tempChildren[0].motion = smoothingAnimMin; tempChildren[0].timeScale = 1; @@ -485,7 +667,14 @@ public static AnimatorControllerParameter AddSmoothedVer(this AnimatorController tempChildren[1].threshold = maxValue; originalValue.children = tempChildren; - BlendTree smoother = new BlendTree() { blendType = BlendTreeType.Simple1D, blendParameter = smoothingAmountParamName, name = param.name + " Smoothing Tree", hideFlags = HideFlags.HideInHierarchy }; + BlendTree smoother = new() + { + blendType = BlendTreeType.Simple1D, + blendParameter = smoothingAmountParamName, + name = param.name + " Smoothing Tree", + hideFlags = HideFlags.HideInHierarchy + }; + smoother.AddChild(smoothedValue); smoother.AddChild(originalValue); smoother.useAutomaticThresholds = false; @@ -494,48 +683,52 @@ public static AnimatorControllerParameter AddSmoothedVer(this AnimatorController smoothingParentTree.AddChild(smoother); tempChildren = smoothingParentTree.children; - tempChildren[tempChildren.Length - 1].directBlendParameter = constantOneParam.name; + tempChildren[^1].directBlendParameter = constantOneParam.name; smoothingParentTree.children = tempChildren; return smoothedParam; } public static AnimatorControllerParameter AddParamDifferential(AnimatorControllerParameter param1, AnimatorControllerParameter param2, AnimatorController controller, string saveTo, float minValue = -1, float maxValue = 1, string differentialParamName = "", string mainBlendTreeIdentifier = "MainBlendTree", string mainBlendTreeLayerName = "MainBlendTree", string differentialParentTreeName = "DifferentialParentTree", string constantOneName = "ConstantOne") { - BlendTree differentialParentTree = GetOrGenerateChildTree(controller, differentialParentTreeName, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); - AnimatorControllerParameter constantOneParam = controller.AddUniqueParam(constantOneName, AnimatorControllerParameterType.Float, 1); + var differentialParentTree = GetOrGenerateChildTree(controller, differentialParentTreeName, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); + var constantOneParam = controller.AddUniqueParam(constantOneName, AnimatorControllerParameterType.Float, 1); if (differentialParamName == "") { differentialParamName = param1.name + "_Minus_" + param2.name; - for (int i = 0; i < differentialParamName.Length; i++) + for (var i = 0; i < differentialParamName.Length; i++) + { if (differentialParamName[i] == '/' || differentialParamName[i] == '\\') + { differentialParamName.Remove(i); + } + } } - AnimatorControllerParameter differentialParam = controller.AddUniqueParam(differentialParamName); + var differentialParam = controller.AddUniqueParam(differentialParamName); if (minValue >= 0 && maxValue >= 0) { - AnimationClip animationClipNegative = MakeAAP(differentialParamName, saveTo, -1); - AnimationClip animationClipPositive = MakeAAP(differentialParamName, saveTo, 1); + var animationClipNegative = MakeAAP(differentialParamName, saveTo, -1); + var animationClipPositive = MakeAAP(differentialParamName, saveTo, 1); differentialParentTree.AddChild(animationClipPositive); differentialParentTree.AddChild(animationClipNegative); - ChildMotion[] tempChildren = differentialParentTree.children; - tempChildren[tempChildren.Length - 2].directBlendParameter = param1.name; - tempChildren[tempChildren.Length - 1].directBlendParameter = param2.name; + var tempChildren = differentialParentTree.children; + tempChildren[^2].directBlendParameter = param1.name; + tempChildren[^1].directBlendParameter = param2.name; differentialParentTree.children = tempChildren; } else { - AnimationClip animationClipMin = MakeAAP(differentialParamName, saveTo, minValue); - AnimationClip animationClipMax = MakeAAP(differentialParamName, saveTo, maxValue); + var animationClipMin = MakeAAP(differentialParamName, saveTo, minValue); + var animationClipMax = MakeAAP(differentialParamName, saveTo, maxValue); controller.AddUniqueParam(differentialParamName); - BlendTree param1Tree = new BlendTree() { blendType = BlendTreeType.Simple1D, blendParameter = param1.name, name = param1.name + "Tree", useAutomaticThresholds = false, hideFlags = HideFlags.HideInHierarchy }; - BlendTree param2Tree = new BlendTree() { blendType = BlendTreeType.Simple1D, blendParameter = param2.name, name = param2.name + "Tree", useAutomaticThresholds = false, hideFlags = HideFlags.HideInHierarchy }; + BlendTree param1Tree = new() { blendType = BlendTreeType.Simple1D, blendParameter = param1.name, name = param1.name + "Tree", useAutomaticThresholds = false, hideFlags = HideFlags.HideInHierarchy }; + BlendTree param2Tree = new() { blendType = BlendTreeType.Simple1D, blendParameter = param2.name, name = param2.name + "Tree", useAutomaticThresholds = false, hideFlags = HideFlags.HideInHierarchy }; - ChildMotion[] tempChildren = new ChildMotion[2]; + var tempChildren = new ChildMotion[2]; tempChildren[0].motion = animationClipMin; tempChildren[0].threshold = -1; tempChildren[0].timeScale = 1; @@ -557,23 +750,24 @@ public static AnimatorControllerParameter AddParamDifferential(AnimatorControlle differentialParentTree.AddChild(param2Tree); tempChildren = differentialParentTree.children; - tempChildren[tempChildren.Length - 2].directBlendParameter = constantOneParam.name; - tempChildren[tempChildren.Length - 1].directBlendParameter = constantOneParam.name; + tempChildren[^2].directBlendParameter = constantOneParam.name; + tempChildren[^1].directBlendParameter = constantOneParam.name; differentialParentTree.children = tempChildren; } return differentialParam; } - public static BlendTree GetOrGenerateMainBlendTree(AnimatorController fxLayer, string mainBlendTreeIdentifier, string layerName, string constantOneName) - => GetMainBlendTree(fxLayer, mainBlendTreeIdentifier) ?? GenerateMainBlendTree(fxLayer, mainBlendTreeIdentifier, layerName, constantOneName); + public static BlendTree GetOrGenerateMainBlendTree(AnimatorController fxLayer, string mainBlendTreeIdentifier, string layerName, string constantOneName) => GetMainBlendTree(fxLayer, mainBlendTreeIdentifier) ?? GenerateMainBlendTree(fxLayer, mainBlendTreeIdentifier, layerName, constantOneName); private static BlendTree GetMainBlendTree(AnimatorController fxLayer, string mainBlendTreeIdentifier) { - List mainBlendTrees = FindHiddenIdentifier(fxLayer, mainBlendTreeIdentifier); + var mainBlendTrees = FindHiddenIdentifier(fxLayer, mainBlendTreeIdentifier); if (mainBlendTrees.Count > 0 && mainBlendTrees[0].stateMachine.states.Length > 0 && mainBlendTrees[0].stateMachine.states[0].state.motion is BlendTree tree) + { return tree; + } return null; } @@ -582,44 +776,49 @@ private static BlendTree GenerateMainBlendTree(AnimatorController fxLayer, strin { fxLayer.AddUniqueParam(constantOneName, AnimatorControllerParameterType.Float, 1); - AnimatorControllerLayer mainBlendTreeLayer = AddLayer(fxLayer, layerName); + var mainBlendTreeLayer = AddLayer(fxLayer, layerName); mainBlendTreeLayer.stateMachine.name = layerName; - mainBlendTreeLayer.stateMachine.anyStatePosition = new Vector3(20, 20, 0); - mainBlendTreeLayer.stateMachine.entryPosition = new Vector3(20, 50, 0); - AnimatorState state = mainBlendTreeLayer.stateMachine.AddState("MainBlendTree (WD On)", new Vector3(0, 100, 0)); + mainBlendTreeLayer.stateMachine.anyStatePosition = new(20, 20, 0); + mainBlendTreeLayer.stateMachine.entryPosition = new(20, 50, 0); + + var state = mainBlendTreeLayer.stateMachine.AddState("MainBlendTree (WD On)", new(0, 100, 0)); state.hideFlags = HideFlags.HideInHierarchy; - BlendTree mainBlendTree = new BlendTree() + + BlendTree mainBlendTree = new() { hideFlags = HideFlags.HideInHierarchy, blendType = BlendTreeType.Direct, blendParameter = constantOneName, name = "MainBlendTree", }; + state.motion = mainBlendTree; state.writeDefaultValues = true; + mainBlendTreeLayer.stateMachine.AddHiddenIdentifier(mainBlendTreeIdentifier); + return (BlendTree)state.motion; } - public static BlendTree GetOrGenerateChildTree(AnimatorController fxLayer, string name, string mainBlendTreeIdentifier, string mainBlendTreeLayerName, string constantOneName) - => GetChildTree(fxLayer, name, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName) ?? GenerateChildTree(fxLayer, name, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); + public static BlendTree GetOrGenerateChildTree(AnimatorController fxLayer, string name, string mainBlendTreeIdentifier, string mainBlendTreeLayerName, string constantOneName) => GetChildTree(fxLayer, name, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName) ?? GenerateChildTree(fxLayer, name, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); private static BlendTree GenerateChildTree(AnimatorController controller, string name, string mainBlendTreeIdentifier, string mainBlendTreeLayerName, string constantOneName) { - BlendTree mainBlendTree = GetOrGenerateMainBlendTree(controller, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); + var mainBlendTree = GetOrGenerateMainBlendTree(controller, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); - BlendTree smoothedParentTree = new BlendTree() + BlendTree smoothedParentTree = new() { hideFlags = HideFlags.HideInHierarchy, blendType = BlendTreeType.Direct, blendParameter = constantOneName, name = name, }; + mainBlendTree.AddChild(smoothedParentTree); var tempChildren = mainBlendTree.children; - tempChildren[tempChildren.Length - 1].directBlendParameter = constantOneName; + tempChildren[^1].directBlendParameter = constantOneName; mainBlendTree.children = tempChildren; return (BlendTree)mainBlendTree.children.Last().motion; @@ -627,11 +826,15 @@ private static BlendTree GenerateChildTree(AnimatorController controller, string private static BlendTree GetChildTree(AnimatorController controller, string name, string mainBlendTreeIdentifier, string mainBlendTreeLayerName, string constantOneName) { - BlendTree mainBlendTree = GetOrGenerateMainBlendTree(controller, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); + var mainBlendTree = GetOrGenerateMainBlendTree(controller, mainBlendTreeIdentifier, mainBlendTreeLayerName, constantOneName); - foreach (ChildMotion child in mainBlendTree.children) + foreach (var child in mainBlendTree.children) + { if (child.motion.name == name) + { return (BlendTree)child.motion; + } + } return null; } @@ -639,13 +842,58 @@ private static BlendTree GetChildTree(AnimatorController controller, string name public static int DecimalToBinary(this int i) { if (i <= 0) + { return 0; + } - string result = ""; - for (int j = i; j > 0; j /= 2) + var result = ""; + for (var j = i; j > 0; j /= 2) + { result = (j % 2).ToString() + result; + } - return Int32.Parse(result); + return int.Parse(result); + } + + public static int GetVRCExpressionParameterCost(this VRCExpressionParameters.Parameter parameter) + { + return parameter.networkSynced ? 0 : parameter.valueType == VRCExpressionParameters.ValueType.Bool ? 1 : 8; + } + + internal class TESPerformanceLogger : IDisposable + { + private readonly string _message; + private readonly UnityEngine.Object _context; + private readonly Stopwatch _w; + + public TESPerformanceLogger(string message = null, UnityEngine.Object context = null) + { + _message = string.IsNullOrEmpty(message) ? "TESPerformanceLogger finished in {0}" : message; + + _context = context; + + _w = new Stopwatch(); + _w.Start(); + } + + public void Dispose() + { + _w.Stop(); + + var elapsed = _w.Elapsed; + var message = string.Format(_message, $"{elapsed.TotalSeconds:#0}s-{elapsed.TotalMilliseconds:##0}ms"); + + if (_context is not null) + { + Debug.LogWarning(message, _context); + } + else + { + Debug.LogWarning(message); + } + } } } } + +#endif diff --git a/Editor/TESHelperFunctions.cs.meta b/Helper/TESHelperFunctions.cs.meta similarity index 100% rename from Editor/TESHelperFunctions.cs.meta rename to Helper/TESHelperFunctions.cs.meta diff --git a/Helper/_InternalsVisibleTo.cs b/Helper/_InternalsVisibleTo.cs new file mode 100644 index 0000000..320c255 --- /dev/null +++ b/Helper/_InternalsVisibleTo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer.Component")] +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer.Editor")] diff --git a/Helper/_InternalsVisibleTo.cs.meta b/Helper/_InternalsVisibleTo.cs.meta new file mode 100644 index 0000000..a75a7d2 --- /dev/null +++ b/Helper/_InternalsVisibleTo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 641e92ea267d43d88a00e8f39eeb7721 +timeCreated: 1727468042 \ No newline at end of file diff --git a/Helper/dev.jetees.memoryoptimizer.Helper.asmdef b/Helper/dev.jetees.memoryoptimizer.Helper.asmdef new file mode 100644 index 0000000..21884ec --- /dev/null +++ b/Helper/dev.jetees.memoryoptimizer.Helper.asmdef @@ -0,0 +1,20 @@ +{ + "name": "dev.jetees.memoryoptimizer.Helper", + "rootNamespace": "", + "references": [ + "VRC.SDK3A", + "VRC.SDK3A.Editor", + "VRC.SDKBase", + "VRC.SDKBase.Editor", + "dev.jetees.memoryoptimizer.Shared" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Helper/dev.jetees.memoryoptimizer.Helper.asmdef.meta b/Helper/dev.jetees.memoryoptimizer.Helper.asmdef.meta new file mode 100644 index 0000000..6367080 --- /dev/null +++ b/Helper/dev.jetees.memoryoptimizer.Helper.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f049bd9f33464d14bdb09b1782cb118c +timeCreated: 1727380590 \ No newline at end of file diff --git a/Media/preview-component.gif b/Media/preview-component.gif new file mode 100644 index 0000000..a5be2cb Binary files /dev/null and b/Media/preview-component.gif differ diff --git a/Media/preview-component.gif.meta b/Media/preview-component.gif.meta new file mode 100644 index 0000000..ade656f --- /dev/null +++ b/Media/preview-component.gif.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: 6575ebf504093874d8c391a7d45f1f44 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Media/use-component.gif b/Media/use-component.gif new file mode 100644 index 0000000..f29c244 Binary files /dev/null and b/Media/use-component.gif differ diff --git a/Media/use-component.gif.meta b/Media/use-component.gif.meta new file mode 100644 index 0000000..2db8201 --- /dev/null +++ b/Media/use-component.gif.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: 0e28b6ba44fe04143a3efd7e872bd95b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 41a7506..04b6701 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,34 @@ It is generally recommended to have as few sync steps as possible, as the more s > [!TIP] > By default the sync steps slider is limited to 4 steps, but this can be unlocked in the settings. + +## How to use with VRCFury + +> [!NOTE] +> VRCFury does offer a component "Parameter Compressor" (formerly "Unlimited Parameters") though **MemoryOptimizer** offers more fine control of which parameters you would like to optimize. + +If you wish to use this with VRCFury, you will need to add the MemoryOptimizerComponent to your avatar. + +> [!WARNING] +> The component needs to be placed on the same object where the `VRC Avatar Descriptor` is present. + +MemoryOptimizer Component + +> [!TIP] +> This component is not exclusive to be used with VRCFury, it works without it as well. + +From there just click `Configure` and you have the same workflow as above ([**How to use**](https://github.com/JeTeeS/MemoryOptimizer#parameters-selection)), but with a major difference: We resolve VRCFury parameters (to the best of our ability) so you can configure those without needing to create a build of the Avatar! + +MemoryOptimizer Component Usage + +> [!NOTE] +> Whenever you are done, remember to press `Save` at the bottom to actually save your optimizer configuration. + +### How does it work in comparison? + +The core difference from this to the normal workflow is that the MemoryOptimizer gets installed **during** the upload rather than before, which means whenever you upload, it will process the optimization configuration and apply it! + +> [!NOTE] +> The component upload pipeline is non-destructive! we create a copy of your parameters and fx-layer to avoid touching the originals! + +When used with VRCFury, it runs after VRCFury is done with all its magic, and then collects and maps the parameters so the configured ones get optimized. \ No newline at end of file diff --git a/Shared.meta b/Shared.meta new file mode 100644 index 0000000..7f8a9b6 --- /dev/null +++ b/Shared.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4fdcd2527c784d5abc7b281ac13604fe +timeCreated: 1727704132 \ No newline at end of file diff --git a/Shared/MemoryOptimizerConstants.cs b/Shared/MemoryOptimizerConstants.cs new file mode 100644 index 0000000..ca89093 --- /dev/null +++ b/Shared/MemoryOptimizerConstants.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; + +namespace JeTeeS.MemoryOptimizer.Shared +{ + internal static class MemoryOptimizerConstants + { + internal const string menuPath = "Tools/TES/MemoryOptimizer"; + internal const string defaultSavePath = "Packages/dev.jetees.memoryoptimizer/Temp/Save"; + internal const string prefKey = "Mem_Opt_Pref_"; + internal const string unlockSyncStepsEPKey = prefKey + "UnlockSyncSteps"; + internal const string backUpModeEPKey = prefKey + "BackUpMode"; + internal const string savePathPPKey = prefKey + "SavePath"; + internal const int maxUnsyncedParams = 8192; + + internal const string discordLink = "https://discord.gg/N7snuJhzkd"; + internal const string prefix = "MemOpt_"; + internal const string syncingLayerName = prefix + "Syncing Layer"; + internal const string syncingLayerIdentifier = prefix + "Syncer"; + internal const string mainBlendTreeIdentifier = prefix + "MainBlendTree"; + internal const string mainBlendTreeLayerName = prefix + "Main BlendTree"; + internal const string smoothingAmountParamName = prefix + "ParamSmoothing"; + internal const string smoothedVerSuffix = "_S"; + internal const string SmoothingTreeName = "SmoothingParentTree"; + internal const string DifferentialTreeName = "DifferentialParentTree"; + internal const string DifferentialSuffix = "_Delta"; + internal const string constantOneName = prefix + "ConstantOne"; + internal const string indexerParamName = prefix + "Indexer "; + internal const string boolSyncerParamName = prefix + "BoolSyncer "; + internal const string intNFloatSyncerParamName = prefix + "IntNFloatSyncer "; + internal const string oneFrameBufferAnimName = prefix + "OneFrameBuffer"; + internal const string oneSecBufferAnimName = prefix + "OneSecBuffer"; + internal const float changeSensitivity = 0.05f; + + internal const string EditorKeyInspectComponent = "dev.jetees.memoryoptimizer_inspectcomponent"; + internal const string EditorKeyInspectParameters = "dev.jetees.memoryoptimizer_inspectparameters"; + + internal static readonly string[] wdOptions = { "Auto-Detect", "Off", "On" }; + internal static readonly string[] backupModes = { "On", "Off", "Ask" }; + internal static readonly string[] paramTypes = { "Int", "Float", "Bool" }; + internal static readonly string[] animatorParamTypes = { "", /* 1 */ "Float", "", /* 3 */ "Int", /* 4 */ "Bool", "", "", "", "", /* 9 */ "Trigger" }; + + // exclude certain names, like VRC Animator Parameters, we don't want to optimize those + internal static readonly List AnimatorExclusions = new() + { + "IsLocal", + "Viseme", + "Voice", + "GestureLeft", + "GestureRight", + "GestureLeftWeight", + "GestureRightWeight", + "AngularY", + "VelocityX", + "VelocityY", + "VelocityZ", + "VelocityMagnitude", + "Upright", + "Grounded", + "Seated", + "AFK", + "Expression1", + "Expression2", + "Expression3", + "Expression4", + "Expression5", + "Expression6", + "Expression7", + "Expression8", + "Expression9", + "Expression10", + "Expression11", + "Expression12", + "Expression13", + "Expression14", + "Expression15", + "Expression16", + "TrackingType", + "VRMode", + "MuteSelf", + "InStation", + "Earmuffs", + "IsOnFriendsList", + "AvatarVersion", + "ScaleModified", + "ScaleFactor", + "ScaleFactorInverse", + "EyeHeightAsMeters", + "EyeHeightAsPercent" + }; + } +} \ No newline at end of file diff --git a/Shared/MemoryOptimizerConstants.cs.meta b/Shared/MemoryOptimizerConstants.cs.meta new file mode 100644 index 0000000..c5c21eb --- /dev/null +++ b/Shared/MemoryOptimizerConstants.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 314562f99108491983248671d6f063ba +timeCreated: 1727874408 \ No newline at end of file diff --git a/Shared/MemoryOptimizerListData.cs b/Shared/MemoryOptimizerListData.cs new file mode 100644 index 0000000..e437f13 --- /dev/null +++ b/Shared/MemoryOptimizerListData.cs @@ -0,0 +1,24 @@ +#if UNITY_EDITOR + +using System; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace JeTeeS.MemoryOptimizer.Shared +{ + [Serializable] + internal class MemoryOptimizerListData + { + public VRCExpressionParameters.Parameter param; + public bool selected = false; + public bool willBeOptimized = false; + + public MemoryOptimizerListData(VRCExpressionParameters.Parameter parameter, bool isSelected, bool willOptimize) + { + param = parameter; + selected = isSelected; + willBeOptimized = willOptimize; + } + } +} + +#endif diff --git a/Shared/MemoryOptimizerListData.cs.meta b/Shared/MemoryOptimizerListData.cs.meta new file mode 100644 index 0000000..f89836c --- /dev/null +++ b/Shared/MemoryOptimizerListData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6113fc8e95544740ad0c045a7d6d1f5d +timeCreated: 1727704142 \ No newline at end of file diff --git a/Shared/_InternalsVisibleTo.cs b/Shared/_InternalsVisibleTo.cs new file mode 100644 index 0000000..65c9cf9 --- /dev/null +++ b/Shared/_InternalsVisibleTo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer.Component")] +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer.Editor")] +[assembly: InternalsVisibleTo("dev.jetees.memoryoptimizer.Helper")] diff --git a/Shared/_InternalsVisibleTo.cs.meta b/Shared/_InternalsVisibleTo.cs.meta new file mode 100644 index 0000000..7b2543a --- /dev/null +++ b/Shared/_InternalsVisibleTo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 424e12f812594ccaa598d6f273345e28 +timeCreated: 1727704351 \ No newline at end of file diff --git a/Shared/dev.jetees.memoryoptimizer.Shared.asmdef b/Shared/dev.jetees.memoryoptimizer.Shared.asmdef new file mode 100644 index 0000000..5aa136c --- /dev/null +++ b/Shared/dev.jetees.memoryoptimizer.Shared.asmdef @@ -0,0 +1,19 @@ +{ + "name": "dev.jetees.memoryoptimizer.Shared", + "rootNamespace": "", + "references": [ + "VRC.SDK3A", + "VRC.SDK3A.Editor", + "VRC.SDKBase", + "VRC.SDKBase.Editor" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Shared/dev.jetees.memoryoptimizer.Shared.asmdef.meta b/Shared/dev.jetees.memoryoptimizer.Shared.asmdef.meta new file mode 100644 index 0000000..ae1fecb --- /dev/null +++ b/Shared/dev.jetees.memoryoptimizer.Shared.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cc6717a052474614acb1a2409d552580 +timeCreated: 1727704192 \ No newline at end of file