diff --git a/Editor/MaterialAdvancedSettings.cs b/Editor/MaterialAdvancedSettings.cs index 6c0076d..89ee7b8 100644 --- a/Editor/MaterialAdvancedSettings.cs +++ b/Editor/MaterialAdvancedSettings.cs @@ -38,7 +38,7 @@ private string Lang(string word) { if (protector == null) return ""; - return lang.GetLang(protector.lang, word); + return lang.GetLang(protector.Language, word); } private void OnGUI() @@ -48,21 +48,21 @@ private void OnGUI() redStyle.wordWrap = true; scroll = GUILayout.BeginScrollView(scroll); - foreach (var option in protector.matOptions) + foreach (var option in protector.MaterialOptions) { Texture2D mainTex = option.Key.mainTexture as Texture2D; GUILayout.BeginHorizontal(); - option.Value.active = GUILayout.Toggle(option.Value.active, ""); + option.Value.Active = GUILayout.Toggle(option.Value.Active, ""); EditorGUILayout.ObjectField(option.Key, typeof(Material), true); EditorGUILayout.ObjectField(mainTex, typeof(Texture2D), true); - if (option.Value.filter == -1) - option.Value.filter = protector.GetDefaultFilter(); - option.Value.filter = EditorGUILayout.Popup(option.Value.filter, ShellProtector.filterStrings, GUILayout.Width(100)); + if (option.Value.Filter == -1) + option.Value.Filter = protector.GetDefaultFilter(); + option.Value.Filter = EditorGUILayout.Popup(option.Value.Filter, ShellProtector.FilterStrings, GUILayout.Width(100)); - if (option.Value.fallback == -1) - option.Value.fallback = protector.GetDefaultFallback(); - option.Value.fallback = EditorGUILayout.Popup(option.Value.fallback, ShellProtector.fallbackStrings, GUILayout.Width(100)); + if (option.Value.Fallback == -1) + option.Value.Fallback = protector.GetDefaultFallback(); + option.Value.Fallback = EditorGUILayout.Popup(option.Value.Fallback, ShellProtector.FallbackStrings, GUILayout.Width(100)); if (AssetManager.GetInstance().IsPoiyomi(option.Key.shader)) { @@ -71,6 +71,7 @@ private void OnGUI() else GUILayout.Label(Lang("New shader")); } + //option.Value.emissionEnc = GUILayout.Toggle(option.Value.emissionEnc, "Encrypt emission()"); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); @@ -84,17 +85,16 @@ private void OnGUI() { GUILayout.Label(Lang("The main texture is empty."), redStyle); } - else if ((mainTex is Texture2D) == false) + else if (!TextureEncryptManager.IsSupportedTexture(mainTex)) { - GUILayout.Label(Lang("The main texture is not Texture2D."), redStyle); - } - else if ( - mainTex.format != TextureFormat.DXT1 && - mainTex.format != TextureFormat.DXT5 && - mainTex.format != TextureFormat.RGB24 && - mainTex.format != TextureFormat.RGBA32) - { - GUILayout.Label(Lang("The main texture is not supported format."), redStyle); + if (!(mainTex is Texture2D)) + { + GUILayout.Label(Lang("The main texture is not Texture2D."), redStyle); + } + else + { + GUILayout.Label(Lang("The main texture is not supported format."), redStyle); + } } } } @@ -118,16 +118,16 @@ private void Init() foreach (var mat in mats) { matSets.Add(mat); - if (!protector.matOptions.ContainsKey(mat)) + if (!protector.MaterialOptions.ContainsKey(mat)) { var option = new ShellProtector.MatOption(); - option.active = true; - protector.matOptions.Add(mat, option); + option.Active = true; + protector.MaterialOptions.Add(mat, option); } } List removed = new List(); - foreach (var pair in protector.matOptions) + foreach (var pair in protector.MaterialOptions) { if (!matSets.Contains(pair.Key)) { @@ -135,7 +135,7 @@ private void Init() } } foreach (var mat in removed) - protector.matOptions.Remove(mat); + protector.MaterialOptions.Remove(mat); } } } diff --git a/Editor/ShellProtectorEditor.cs b/Editor/ShellProtectorEditor.cs index 8ef1650..993ac74 100644 --- a/Editor/ShellProtectorEditor.cs +++ b/Editor/ShellProtectorEditor.cs @@ -1,13 +1,10 @@ #if UNITY_EDITOR -using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEditorInternal; -using System.Text; using System; using System.IO; -using System.Text.RegularExpressions; using VRC.SDK3.Avatars.Components; namespace Shell.Protector @@ -19,83 +16,73 @@ public class ShellProtectorEditor : Editor ShellProtector root = null; readonly LanguageManager lang = LanguageManager.GetInstance(); - ReorderableList game_object_list; - ReorderableList material_list; - ReorderableList texture_list; + ReorderableList gameobjectList; + ReorderableList materialList; + ReorderableList textureList; ReorderableList obfuscationList; SerializedProperty rounds; SerializedProperty filter; SerializedProperty fallback; SerializedProperty algorithm; - SerializedProperty key_size; - SerializedProperty key_size_idx; - SerializedProperty animation_speed; - SerializedProperty delete_folders; - SerializedProperty parameter_multiplexing; + SerializedProperty keySize; + SerializedProperty keySizeIdx; + SerializedProperty syncSize; + SerializedProperty deleteFolders; SerializedProperty bUseSmallMipTexture; SerializedProperty bPreserveMMD; - SerializedProperty fallbackTime; SerializedProperty turnOnAllSafetyFallback; + ShellProtectorEditorViewModel viewModel; bool debug = false; bool option = true; - bool ObfuscatorOption = true; + bool obfuscatorOption = true; bool forceProgress = false; bool fallbackOption = true; readonly string[] languages = new string[3]; - readonly string[] enc_funcs = new string[2]; - readonly string[] key_lengths = new string[5]; + readonly string[] encryptFunctions = new string[2]; + readonly string[] keyLengthLabels = new string[5]; List shaders = new List(); + readonly List debugTextures = new List(); - bool show_pwd = false; - - Texture2D tex; - - string github_version; + bool showPassword = false; private string Lang(string word) { if (root == null) return ""; - return lang.GetLang(root.lang, word); + return lang.GetLang(root.Language, word); } void OnEnable() { root = target as ShellProtector; - MonoScript monoScript = MonoScript.FromMonoBehaviour(root); - string script_path = AssetDatabase.GetAssetPath(monoScript); - - root.asset_dir = Path.GetDirectoryName(Path.GetDirectoryName(script_path)); - - game_object_list = new ReorderableList(serializedObject, serializedObject.FindProperty("gameobject_list"), true, true, true, true); - game_object_list.drawHeaderCallback = rect => EditorGUI.LabelField(rect, Lang("Object list")); - game_object_list.drawElementCallback = (rect, index, is_active, is_focused) => + gameobjectList = new ReorderableList(serializedObject, serializedObject.FindProperty("_gameObjectList"), true, true, true, true); + gameobjectList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, Lang("Object list")); + gameobjectList.drawElementCallback = (rect, index, is_active, is_focused) => { - SerializedProperty element = game_object_list.serializedProperty.GetArrayElementAtIndex(index); + SerializedProperty element = gameobjectList.serializedProperty.GetArrayElementAtIndex(index); EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element, GUIContent.none); }; - material_list = new ReorderableList(serializedObject, serializedObject.FindProperty("material_list"), true, true, true, true); - material_list.drawHeaderCallback = rect => EditorGUI.LabelField(rect, Lang("Material List")); - material_list.drawElementCallback = (rect, index, is_active, is_focused) => + materialList = new ReorderableList(serializedObject, serializedObject.FindProperty("_materialList"), true, true, true, true); + materialList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, Lang("Material List")); + materialList.drawElementCallback = (rect, index, is_active, is_focused) => { - SerializedProperty element = material_list.serializedProperty.GetArrayElementAtIndex(index); + SerializedProperty element = materialList.serializedProperty.GetArrayElementAtIndex(index); EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element, GUIContent.none); }; - texture_list = new ReorderableList(serializedObject, serializedObject.FindProperty("texture_list"), true, true, true, true); - texture_list.drawHeaderCallback = rect => EditorGUI.LabelField(rect, Lang("Texture List")); - texture_list.drawElementCallback = (rect, index, is_active, is_focused) => + textureList = new ReorderableList(debugTextures, typeof(Texture2D), true, true, true, true); + textureList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, Lang("Texture List")); + textureList.drawElementCallback = (rect, index, is_active, is_focused) => { - SerializedProperty element = texture_list.serializedProperty.GetArrayElementAtIndex(index); - EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element, GUIContent.none); + debugTextures[index] = EditorGUI.ObjectField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), debugTextures[index], typeof(Texture2D), false) as Texture2D; }; - obfuscationList = new ReorderableList(serializedObject, serializedObject.FindProperty("obfuscationRenderers"), true, true, true, true); + obfuscationList = new ReorderableList(serializedObject, serializedObject.FindProperty("_obfuscationRenderers"), true, true, true, true); obfuscationList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, Lang("BlendShape obfuscation")); obfuscationList.drawElementCallback = (rect, index, is_active, is_focused) => { @@ -104,33 +91,32 @@ void OnEnable() }; #region SerializedObject - rounds = serializedObject.FindProperty("rounds"); - filter = serializedObject.FindProperty("filter"); - fallback = serializedObject.FindProperty("fallback"); - algorithm = serializedObject.FindProperty("algorithm"); - key_size = serializedObject.FindProperty("key_size"); - key_size_idx = serializedObject.FindProperty("key_size_idx"); - animation_speed = serializedObject.FindProperty("animation_speed"); - delete_folders = serializedObject.FindProperty("delete_folders"); - parameter_multiplexing = serializedObject.FindProperty("parameter_multiplexing"); - bUseSmallMipTexture = serializedObject.FindProperty("bUseSmallMipTexture"); - bPreserveMMD = serializedObject.FindProperty("bPreserveMMD"); - fallbackTime = serializedObject.FindProperty("fallbackTime"); - turnOnAllSafetyFallback = serializedObject.FindProperty("turnOnAllSafetyFallback"); + rounds = serializedObject.FindProperty("_rounds"); + filter = serializedObject.FindProperty("_filter"); + fallback = serializedObject.FindProperty("_fallback"); + algorithm = serializedObject.FindProperty("_algorithm"); + keySize = serializedObject.FindProperty("_keySize"); + keySizeIdx = serializedObject.FindProperty("_keySizeIndex"); + syncSize = serializedObject.FindProperty("_syncSize"); + deleteFolders = serializedObject.FindProperty("_deleteFolders"); + bUseSmallMipTexture = serializedObject.FindProperty("_useSmallMipTexture"); + bPreserveMMD = serializedObject.FindProperty("_preserveMmd"); + turnOnAllSafetyFallback = serializedObject.FindProperty("_turnOnAllSafetyFallback"); #endregion + viewModel = new ShellProtectorEditorViewModel(root, keySize, syncSize, gameobjectList, materialList); - enc_funcs[0] = "XXTEA"; - enc_funcs[1] = "Chacha8"; + encryptFunctions[0] = "XXTEA"; + encryptFunctions[1] = "Chacha8"; languages[0] = "English"; languages[1] = "한국어"; languages[2] = "日本語"; - key_lengths[0] = Lang("0 (Minimal security)"); - key_lengths[1] = Lang("4 (Low security)"); - key_lengths[2] = Lang("8 (Middle security)"); - key_lengths[3] = Lang("12 (Hight security)"); - key_lengths[4] = Lang("16 (Unbreakable security)"); + keyLengthLabels[0] = Lang("0 (Minimal security)"); + keyLengthLabels[1] = Lang("4 (Low security)"); + keyLengthLabels[2] = Lang("8 (Middle security)"); + keyLengthLabels[3] = Lang("12 (Hight security)"); + keyLengthLabels[4] = Lang("16 (Unbreakable security)"); VersionManager.GetInstance().Refresh(); @@ -142,7 +128,7 @@ public override void OnInspectorGUI() { root = target as ShellProtector; - root.descriptor = EditorGUILayout.ObjectField(root.descriptor, typeof(VRCAvatarDescriptor)) as VRCAvatarDescriptor; + root.Descriptor = EditorGUILayout.ObjectField(root.Descriptor, typeof(VRCAvatarDescriptor), true) as VRCAvatarDescriptor; GUILayout.BeginHorizontal(); GUILayout.Label(Lang("Current version: ") + VersionManager.GetInstance().GetVersion()); GUILayout.FlexibleSpace(); @@ -154,27 +140,27 @@ public override void OnInspectorGUI() GUILayout.Label(Lang("Languages: ")); GUILayout.FlexibleSpace(); - root.lang_idx = EditorGUILayout.Popup(root.lang_idx, languages, GUILayout.Width(100)); + root.LanguageIndex = EditorGUILayout.Popup(root.LanguageIndex, languages, GUILayout.Width(100)); - key_lengths[0] = Lang("0 (Minimal security)"); - key_lengths[1] = Lang("4 (Low security)"); - key_lengths[2] = Lang("8 (Middle security)"); - key_lengths[3] = Lang("12 (Hight security)"); - key_lengths[4] = Lang("16 (Unbreakable security)"); + keyLengthLabels[0] = Lang("0 (Minimal security)"); + keyLengthLabels[1] = Lang("4 (Low security)"); + keyLengthLabels[2] = Lang("8 (Middle security)"); + keyLengthLabels[3] = Lang("12 (Hight security)"); + keyLengthLabels[4] = Lang("16 (Unbreakable security)"); - switch (root.lang_idx) + switch (root.LanguageIndex) { case 0: - root.lang = "eng"; + root.Language = "eng"; break; case 1: - root.lang = "kor"; + root.Language = "kor"; break; case 2: - root.lang = "jp"; + root.Language = "jp"; break; default: - root.lang = "eng"; + root.Language = "eng"; break; } @@ -190,69 +176,47 @@ public override void OnInspectorGUI() GUILayout.Label(Lang("Password"), EditorStyles.boldLabel); - if (key_size.intValue < 16) + if (keySize.intValue < 16) { - int length = 16 - key_size.intValue; + int length = 16 - keySize.intValue; GUILayout.BeginHorizontal(); - root.pwd = GUILayout.TextField(root.pwd, length, GUILayout.Width(100)); + root.FixedPassword = GUILayout.TextField(root.FixedPassword, length, GUILayout.Width(100)); if (GUILayout.Button(Lang("Generate"))) - root.pwd = KeyGenerator.GenerateRandomString(length); + root.FixedPassword = KeyGenerator.GenerateRandomString(length); GUILayout.FlexibleSpace(); GUILayout.Label(Lang("A password that you don't need to memorize. (max:") + length + ")", EditorStyles.wordWrappedLabel); GUILayout.EndHorizontal(); } - if (key_size.intValue > 0) + if (keySize.intValue > 0) { GUILayout.BeginHorizontal(); - if(!show_pwd) - root.pwd2 = GUILayout.PasswordField(root.pwd2, '*', key_size.intValue, GUILayout.Width(100)); + if(!showPassword) + root.UserPassword = GUILayout.PasswordField(root.UserPassword, '*', keySize.intValue, GUILayout.Width(100)); else - root.pwd2 = GUILayout.TextField(root.pwd2, key_size.intValue, GUILayout.Width(100)); + root.UserPassword = GUILayout.TextField(root.UserPassword, keySize.intValue, GUILayout.Width(100)); if (GUILayout.Button(Lang("Show"))) - show_pwd = !show_pwd; + showPassword = !showPassword; GUILayout.FlexibleSpace(); - GUILayout.Label(Lang("This password should be memorized. (max:") + key_size.intValue + ")", EditorStyles.wordWrappedLabel); + GUILayout.Label(Lang("This password should be memorized. (max:") + keySize.intValue + ")", EditorStyles.wordWrappedLabel); GUILayout.EndHorizontal(); } - int free_parameter = -1; + serializedObject.Update(); + viewModel.Refresh(); - GUIStyle red_style = new GUIStyle(GUI.skin.label); - red_style.normal.textColor = Color.red; - red_style.wordWrap = true; + GUIStyle redStyle = new GUIStyle(GUI.skin.label); + redStyle.normal.textColor = Color.red; + redStyle.wordWrap = true; - var parameters = root.GetParameter(); - if (parameters == null) - GUILayout.Label(Lang("Cannot find VRCExpressionParameters in your avatar!"), red_style); + if (!viewModel.HasParameterAsset) + GUILayout.Label(Lang("Cannot find VRCExpressionParameters in your avatar!"), redStyle); else { - free_parameter = 256 - parameters.CalcTotalCost(); - GUILayout.Label(Lang("Free parameter:") + free_parameter, EditorStyles.wordWrappedLabel); - } - int using_parameter = (key_size.intValue * 8); - if(parameter_multiplexing.boolValue == true) - { - int keys = key_size.intValue; - switch(keys) - { - case 4: - using_parameter = 8 + 3; - break; - case 8: - using_parameter = 8 + 4; - break; - case 12: - using_parameter = 8 + 5; - break; - case 16: - using_parameter = 8 + 5; - break; - } + GUILayout.Label(Lang("Free parameter:") + viewModel.FreeParameter, EditorStyles.wordWrappedLabel); } - GUILayout.Label(Lang("Parameters to be used:") + using_parameter, EditorStyles.wordWrappedLabel); + GUILayout.Label(Lang("Parameters to be used:") + viewModel.UsedParameter, EditorStyles.wordWrappedLabel); - serializedObject.Update(); - game_object_list.DoLayoutList(); - material_list.DoLayoutList(); + gameobjectList.DoLayoutList(); + materialList.DoLayoutList(); GUILayout.Label(Lang("Encrypting too many objects can cause lag when loading avatars in-game.")); if(GUILayout.Button(Lang("Material advanced settings"))) { @@ -264,29 +228,50 @@ public override void OnInspectorGUI() if(option) { GUILayout.Label(Lang("Max password length"), EditorStyles.boldLabel); - key_size_idx.intValue = EditorGUILayout.Popup(key_size_idx.intValue, key_lengths, GUILayout.Width(150)); + keySizeIdx.intValue = EditorGUILayout.Popup(keySizeIdx.intValue, keyLengthLabels, GUILayout.Width(150)); + GUILayout.Space(10); - switch (key_size_idx.intValue) + switch (keySizeIdx.intValue) { case 0: - key_size.intValue = 0; + keySize.intValue = 0; break; case 1: - key_size.intValue = 4; + keySize.intValue = 4; break; case 2: - key_size.intValue = 8; + keySize.intValue = 8; break; case 3: - key_size.intValue = 12; + keySize.intValue = 12; break; case 4: - key_size.intValue = 16; + keySize.intValue = 16; break; } + var syncSize_value = syncSize.intValue; + int syncSize_index = 0; + //int[] syncSizeCandidates = { 1, 2, 4}; + //string[] selectableValues = { "1", "2", "4" }; + int[] syncSizeCandidates = { 1 }; + string[] selectableValues = { "1" }; + for (int i = 0; i < syncSizeCandidates.Length; i++) + if (syncSizeCandidates[i] == syncSize_value) + syncSize_index = i; + + if(keySize.intValue > 0) + { + GUILayout.Label(Lang("Sync speed"), EditorStyles.boldLabel); + syncSize_index = EditorGUILayout.Popup(syncSize_index, selectableValues, GUILayout.Width(100)); + syncSize.intValue = syncSizeCandidates[syncSize_index]; + GUILayout.Label(Lang("Under development."), EditorStyles.boldLabel); + //GUILayout.Label(Lang("When the Sync speed is 2 or higher, OSC1.7 or higher must be used."), EditorStyles.boldLabel); + GUILayout.Space(10); + } + GUILayout.Label(Lang("Encrytion algorithm"), EditorStyles.boldLabel); - algorithm.intValue = EditorGUILayout.Popup(algorithm.intValue, enc_funcs, GUILayout.Width(120)); + algorithm.intValue = EditorGUILayout.Popup(algorithm.intValue, encryptFunctions, GUILayout.Width(120)); if (algorithm.intValue == 0) { @@ -308,34 +293,25 @@ public override void OnInspectorGUI() GUILayout.Space(10); GUILayout.Label(Lang("Default texture filter"), EditorStyles.boldLabel); - filter.intValue = EditorGUILayout.Popup(filter.intValue, ShellProtector.filterStrings, GUILayout.Width(100)); + filter.intValue = EditorGUILayout.Popup(filter.intValue, ShellProtector.FilterStrings, GUILayout.Width(100)); GUILayout.Label(Lang("Setting it to 'Point' may result in aliasing, but performance is better."), EditorStyles.wordWrappedLabel); - GUILayout.Label(Lang("Initial animation speed"), EditorStyles.boldLabel); - GUILayout.BeginHorizontal(); - animation_speed.floatValue = GUILayout.HorizontalSlider(animation_speed.floatValue, 2.0f, 5.0f, GUILayout.Width(100)); - animation_speed.floatValue = EditorGUILayout.FloatField("", animation_speed.floatValue, GUILayout.Width(50)); - animation_speed.floatValue = Math.Clamp(animation_speed.floatValue, 2.0f, 5.0f); - GUILayout.FlexibleSpace(); - GUILayout.Label(Lang("Avatar first load animation speed"), EditorStyles.wordWrappedLabel); - GUILayout.EndHorizontal(); + //GUILayout.Label(Lang("Initial animation speed"), EditorStyles.boldLabel); + //GUILayout.BeginHorizontal(); + //animation_speed.floatValue = GUILayout.HorizontalSlider(animation_speed.floatValue, 2.0f, 5.0f, GUILayout.Width(100)); + //animation_speed.floatValue = EditorGUILayout.FloatField("", animation_speed.floatValue, GUILayout.Width(50)); + //animation_speed.floatValue = Math.Clamp(animation_speed.floatValue, 2.0f, 5.0f); + //GUILayout.FlexibleSpace(); + //GUILayout.Label(Lang("Avatar first load animation speed"), EditorStyles.wordWrappedLabel); + //GUILayout.EndHorizontal(); GUILayout.Space(10); GUILayout.BeginHorizontal(); GUILayout.Label(Lang("Delete folders that already exists when at creation time"), EditorStyles.boldLabel); - delete_folders.boolValue = EditorGUILayout.Toggle(delete_folders.boolValue); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - - GUILayout.BeginHorizontal(); - GUILayout.Label(Lang("parameter-multiplexing"), EditorStyles.boldLabel); - parameter_multiplexing.boolValue = EditorGUILayout.Toggle(parameter_multiplexing.boolValue); + deleteFolders.boolValue = EditorGUILayout.Toggle(deleteFolders.boolValue); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); - GUILayout.Label(Lang("The OSC program must always be on, but it consumes fewer parameters."), EditorStyles.wordWrappedLabel); GUILayout.Space(10); @@ -349,8 +325,8 @@ public override void OnInspectorGUI() GUILayout.Space(10); } - ObfuscatorOption = EditorGUILayout.Foldout(ObfuscatorOption, Lang("Obfustactor Options")); - if(ObfuscatorOption) + obfuscatorOption = EditorGUILayout.Foldout(obfuscatorOption, Lang("Obfustactor Options")); + if(obfuscatorOption) { obfuscationList.DoLayoutList(); @@ -368,20 +344,6 @@ public override void OnInspectorGUI() { GUILayout.Label(Lang("Opponents with Safety option turned on will see degraded textures instead of noise.")); - GUILayout.Label(Lang("Fallback wait time"), EditorStyles.boldLabel); - GUILayout.BeginHorizontal(); - fallbackTime.floatValue = GUILayout.HorizontalSlider(fallbackTime.floatValue, 0.0f, 10.0f, GUILayout.Width(100)); - fallbackTime.floatValue = EditorGUILayout.FloatField("", fallbackTime.floatValue, GUILayout.Width(50)); - fallbackTime.floatValue = Mathf.Clamp(fallbackTime.floatValue, 0.0f, 10.0f); -#if UNITY_2022 - fallbackTime.floatValue = MathF.Round(fallbackTime.floatValue, 1); -#endif - GUILayout.FlexibleSpace(); - GUILayout.Label(Lang("After this time, the fallback is turned off. (Only who is Safety OFF)"), EditorStyles.wordWrappedLabel); - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - GUILayout.BeginHorizontal(); GUILayout.Label(Lang("Change all Safety Fallback settings of shader to Unlit."), EditorStyles.boldLabel); turnOnAllSafetyFallback.boolValue = EditorGUILayout.Toggle(turnOnAllSafetyFallback.boolValue); @@ -389,13 +351,14 @@ public override void OnInspectorGUI() GUILayout.EndHorizontal(); GUILayout.Label(Lang("Default fallback texture"), EditorStyles.boldLabel); - fallback.intValue = EditorGUILayout.Popup(fallback.intValue, ShellProtector.fallbackStrings, GUILayout.Width(100)); + fallback.intValue = EditorGUILayout.Popup(fallback.intValue, ShellProtector.FallbackStrings, GUILayout.Width(100)); } #endregion - if (free_parameter - using_parameter < 0) + viewModel.Refresh(); + if (!viewModel.HasEnoughParameterSpace) { - GUILayout.Label(Lang("Not enough parameter space!"), red_style); + GUILayout.Label(Lang("Not enough parameter space!"), redStyle); GUILayout.BeginHorizontal(); GUILayout.Label(Lang("Force progress")); forceProgress = EditorGUILayout.Toggle(forceProgress); @@ -403,12 +366,12 @@ public override void OnInspectorGUI() GUILayout.EndHorizontal(); GUI.enabled = forceProgress; } - if (game_object_list.count == 0 && material_list.count == 0) + if (!viewModel.HasTargets) GUI.enabled = false; - + #if MODULAR - if (GUILayout.Button(Lang("Manual Encrypt!"))) + if (GUILayout.Button(Lang("Manual Encrypt! (for testing)"))) #else if (GUILayout.Button(Lang("Encrypt!"))) #endif @@ -422,7 +385,7 @@ public override void OnInspectorGUI() GUILayout.Label(Lang("Modular avatars exist. It is automatically encrypted on upload."), modularStyle); #endif - if (GUILayout.Button(Lang("Delete previously encrypted files") + String.Format("({0})", root.GetEncyryptedFoldersCount()))) + if (GUILayout.Button(Lang("Delete previously encrypted files") + String.Format("({0})", root.GetEncryptedFoldersCount()))) { root.CleanEncrypted(); } @@ -432,36 +395,40 @@ public override void OnInspectorGUI() { GUILayout.Space(10); if (GUILayout.Button(Lang("XXTEA test"))) - Test.XXTEATest(root.pwd, root.pwd2, root.GetKeySize()); + Test.XXTEATest(root.FixedPassword, root.UserPassword, root.GetKeySize()); if (GUILayout.Button(Lang("Chacha8 test"))) - Test.ChachaTest(root.pwd, root.pwd2, root.GetKeySize()); + Test.ChachaTest(root.FixedPassword, root.UserPassword, root.GetKeySize()); GUILayout.Space(10); - texture_list.DoLayoutList(); + textureList.DoLayoutList(); GUILayout.BeginHorizontal(); if (GUILayout.Button(Lang("Encrypt"))) { Texture2D last = null; - for (int i = 0; i < texture_list.count; i++) + for (int i = 0; i < debugTextures.Count; i++) { - SerializedProperty element = texture_list.serializedProperty.GetArrayElementAtIndex(i); - Texture2D texture = element.objectReferenceValue as Texture2D; + Texture2D texture = debugTextures[i]; + if (texture == null) + continue; TextureSettings.SetRWEnableTexture(texture); - Texture2D[] encrypted_texture = root.GetEncryptTexture().TextureEncrypt(texture, KeyGenerator.MakeKeyBytes(root.pwd, root.pwd2, key_size.intValue), new XXTEA()); + var result = TextureEncryptManager.EncryptTexture(texture, KeyGenerator.MakeKeyBytes(root.FixedPassword, root.UserPassword, keySize.intValue), new XXTEA()); + if (result.Texture1 == null) + continue; - last = encrypted_texture[0]; + last = result.Texture1; - if (!AssetDatabase.IsValidFolder(root.asset_dir + '/' + root.descriptor.gameObject.name)) - AssetDatabase.CreateFolder(root.asset_dir, root.descriptor.gameObject.name); - if (!AssetDatabase.IsValidFolder(root.asset_dir + '/' + root.descriptor.gameObject.name + "/mat")) - AssetDatabase.CreateFolder(root.asset_dir + '/' + root.descriptor.gameObject.name, "mat"); + var writer = new AssetWriter(); + var outputPaths = new OutputPaths(root.AssetDir, root.Descriptor.gameObject); + outputPaths.PrepareFolders(writer, false); - AssetDatabase.CreateAsset(encrypted_texture[0], root.asset_dir + '/' + root.descriptor.gameObject.name + '/' + texture.name + "_encrypt.asset"); - File.WriteAllBytes(root.asset_dir + '/' + root.descriptor.gameObject.name + '/' + texture.name + "_encrypt.png", encrypted_texture[1].EncodeToPNG()); - if (encrypted_texture[1] != null) - AssetDatabase.CreateAsset(encrypted_texture[1], root.asset_dir + '/' + root.descriptor.gameObject.name + '/' + texture.name + "_encrypt2.asset"); + writer.CreateAssetInFolder(result.Texture1, outputPaths.Folders.TexGuid, outputPaths.EncryptedTextureName(texture, 0)); + if (result.Texture2 != null) + { + File.WriteAllBytes(writer.UniquePathInFolder(outputPaths.Folders.TexGuid, OutputPaths.Sanitize(texture.name) + "_encrypt.png"), result.Texture2.EncodeToPNG()); + writer.CreateAssetInFolder(result.Texture2, outputPaths.Folders.TexGuid, outputPaths.EncryptedTextureName(texture, 2)); + } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); @@ -470,35 +437,6 @@ public override void OnInspectorGUI() Selection.activeObject = last; } - /*if (GUILayout.Button("Decrypt")) - { - Texture2D last = null; - for (int i = 0; i < texture_list.count; i++) - { - SerializedProperty textureProperty = texture_list.serializedProperty.GetArrayElementAtIndex(i); - Texture2D texture = textureProperty.objectReferenceValue as Texture2D; - - root.SetRWEnableTexture(texture); - - Texture2D tmp = root.GetEncryptTexture().TextureDecryptXXTEA(texture, root.MakeKeyBytes(root.pwd)); - - if (root.asset_dir[root.asset_dir.Length - 1] == '/') - root.asset_dir = root.asset_dir.Remove(root.asset_dir.Length - 1); - - if (!AssetDatabase.IsValidFolder(root.asset_dir + '/' + root.gameObject.name)) - AssetDatabase.CreateFolder(root.asset_dir, root.gameObject.name); - if (!AssetDatabase.IsValidFolder(root.asset_dir + '/' + root.gameObject.name + "/mat")) - AssetDatabase.CreateFolder(root.asset_dir + '/' + root.gameObject.name, "mat"); - - System.IO.File.WriteAllBytes(root.asset_dir + '/' + root.gameObject.name + '/' + texture.name + "_decrypt.png", tmp.EncodeToPNG()); - last = (Texture2D)AssetDatabase.LoadAssetAtPath(root.asset_dir + '/' + root.gameObject.name + '/' + texture.name + "_decrypt.png", typeof(Texture2D)); - - AssetDatabase.Refresh(); - } - if (last != null) - Selection.activeObject = last; - }*/ - GUILayout.EndHorizontal(); } serializedObject.ApplyModifiedProperties(); @@ -522,7 +460,7 @@ static void AddShellProtector() obj.transform.parent = gameobject.transform; var shellProtector = obj.AddComponent(); - shellProtector.descriptor = av3; + shellProtector.Descriptor = av3; shellProtector.Init(); Selection.activeObject = obj; @@ -558,4 +496,4 @@ private void OnGUI() } } } -#endif \ No newline at end of file +#endif diff --git a/Editor/ShellProtectorEditorViewModel.cs b/Editor/ShellProtectorEditorViewModel.cs new file mode 100644 index 0000000..00a6206 --- /dev/null +++ b/Editor/ShellProtectorEditorViewModel.cs @@ -0,0 +1,48 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEditorInternal; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace Shell.Protector +{ + internal sealed class ShellProtectorEditorViewModel + { + readonly ShellProtector protector; + readonly SerializedProperty keySize; + readonly SerializedProperty syncSize; + readonly ReorderableList gameobjectList; + readonly ReorderableList materialList; + + public ShellProtectorEditorViewModel( + ShellProtector protector, + SerializedProperty keySize, + SerializedProperty syncSize, + ReorderableList gameobjectList, + ReorderableList materialList) + { + this.protector = protector; + this.keySize = keySize; + this.syncSize = syncSize; + this.gameobjectList = gameobjectList; + this.materialList = materialList; + } + + public bool HasParameterAsset { get; private set; } + public int FreeParameter { get; private set; } + public int UsedParameter { get; private set; } + public bool HasEnoughParameterSpace => HasParameterAsset && FreeParameter >= UsedParameter; + public bool HasTargets => gameobjectList.count > 0 || materialList.count > 0; + + public void Refresh() + { + VRCExpressionParameters parameters = protector.GetParameter(); + HasParameterAsset = parameters != null; + FreeParameter = HasParameterAsset ? 256 - parameters.CalcTotalCost() : -1; + + int lockSize = 1; + int switchCount = ShellProtector.GetRequiredSwitchCount(keySize.intValue, syncSize.intValue); + UsedParameter = switchCount + lockSize + syncSize.intValue * 8; + } + } +} +#endif diff --git a/Runtime/Scripts/EncryptTexture.cs.meta b/Editor/ShellProtectorEditorViewModel.cs.meta similarity index 83% rename from Runtime/Scripts/EncryptTexture.cs.meta rename to Editor/ShellProtectorEditorViewModel.cs.meta index bc6fe90..2dbe6ad 100644 --- a/Runtime/Scripts/EncryptTexture.cs.meta +++ b/Editor/ShellProtectorEditorViewModel.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 66bc90ffbdb30c34694f898f5f1a2842 +guid: 22ea150d6dc34805a3c1fb82273a77a0 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Editor/TesterEditor.cs b/Editor/TesterEditor.cs index 972931e..81766b0 100644 --- a/Editor/TesterEditor.cs +++ b/Editor/TesterEditor.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using System.Collections; using System.Collections.Generic; using UnityEditor; @@ -18,7 +18,7 @@ private string Lang(string word) { if (root == null) return ""; - return lang.GetLang(root.lang, word); + return lang.GetLang(root.Language, word); } public void OnEnable() { @@ -29,14 +29,14 @@ public void OnEnable() } public override void OnInspectorGUI() { - root.protector = EditorGUILayout.ObjectField(root.protector, typeof(ShellProtector), GUILayout.Width(200)) as ShellProtector; + root.Protector = EditorGUILayout.ObjectField(root.Protector, typeof(ShellProtector), true, GUILayout.Width(200)) as ShellProtector; GUILayout.Space(10); GUILayout.BeginHorizontal(); GUILayout.Label(Lang("Max password length")); GUILayout.FlexibleSpace(); - root.userKeyLength = EditorGUILayout.IntField(root.userKeyLength, GUILayout.Width(50)); + root.UserKeyLength = EditorGUILayout.IntField(root.UserKeyLength, GUILayout.Width(50)); GUILayout.EndHorizontal(); GUILayout.Space(10); @@ -47,39 +47,24 @@ public override void OnInspectorGUI() GUILayout.Space(10); - root.lang_idx = EditorGUILayout.Popup(root.lang_idx, languages, GUILayout.Width(100)); - switch (root.lang_idx) + root.LanguageIndex = EditorGUILayout.Popup(root.LanguageIndex, languages, GUILayout.Width(100)); + switch (root.LanguageIndex) { case 0: - root.lang = "eng"; + root.Language = "eng"; break; case 1: - root.lang = "kor"; + root.Language = "kor"; break; default: - root.lang = "eng"; + root.Language = "eng"; break; } GUILayout.EndHorizontal(); GUILayout.Space(10); - GUILayout.BeginHorizontal(); - if (GUILayout.Button(Lang("Off fallback"))) - { - root.ResetEncryption(); - root.protector.SetMaterialFallbackValue(root.transform.root.gameObject, false); - } - if (GUILayout.Button(Lang("On fallback"))) - { - root.ResetEncryption(); - root.protector.SetMaterialFallbackValue(root.transform.root.gameObject, true); - } - GUILayout.EndHorizontal(); - - GUILayout.Space(10); - - if (root.userKeyLength == 0) + if (root.UserKeyLength == 0) { GUILayout.Label(Lang("It's okay for the 0-digit password to be the same as the original.")); } @@ -99,4 +84,4 @@ public override void OnInspectorGUI() } } } -#endif \ No newline at end of file +#endif diff --git a/Editor/liltoonCustom/CustomInspector.cs b/Editor/liltoonCustom/CustomInspector.cs index 9829dea..9c4c7ed 100644 --- a/Editor/liltoonCustom/CustomInspector.cs +++ b/Editor/liltoonCustom/CustomInspector.cs @@ -1,4 +1,6 @@ #if UNITY_EDITOR +using System.Linq; +using Shell.Protector; using UnityEditor; using UnityEngine; @@ -8,14 +10,15 @@ namespace lilToon public class ShellProtectorInspector : lilToonInspector { // Custom properties - MaterialProperty mip_tex; - MaterialProperty encrypted_tex0; - MaterialProperty encrypted_tex1; - MaterialProperty[] key = new MaterialProperty[15]; - MaterialProperty fallback; + MaterialProperty mipTexture; + MaterialProperty encryptedTexture0; + MaterialProperty encryptedTexture1; + MaterialProperty[] key = new MaterialProperty[16]; + MaterialProperty passwordHash; + MaterialProperty hashMagic; private static bool isShowCustomProperties; - private static bool show_pwd = false; + private static bool showPassword = false; private const string shaderName = "ShellProtector"; protected override void LoadCustomProperties(MaterialProperty[] props, Material material) @@ -30,13 +33,14 @@ protected override void LoadCustomProperties(MaterialProperty[] props, Material //isShowRenderMode = false; //LoadCustomLanguage(""); - mip_tex = FindProperty("_MipTex", props); - encrypted_tex0 = FindProperty("_EncryptTex0", props); - encrypted_tex1 = FindProperty("_EncryptTex1", props); - fallback = FindProperty("_fallback", props); + mipTexture = FindProperty(ShaderProperties.MipTexture, props); + encryptedTexture0 = FindProperty(ShaderProperties.EncryptTexture0, props); + encryptedTexture1 = FindProperty(ShaderProperties.EncryptTexture1, props); + passwordHash = FindProperty(ShaderProperties.PasswordHash, props); + hashMagic = FindProperty(ShaderProperties.HashMagic, props); for (int i = 0; i < key.Length; ++i) - key[i] = FindProperty("_Key" + i, props); + key[i] = FindProperty(ShaderProperties.KeyPrefix + i, props); } protected override void DrawCustomProperties(Material material) @@ -52,24 +56,25 @@ protected override void DrawCustomProperties(Material material) isShowCustomProperties = Foldout("ShellProtector", "ShellProtector", isShowCustomProperties); if(isShowCustomProperties) { - m_MaterialEditor.ShaderProperty(fallback, "fallback"); - EditorGUILayout.BeginVertical(boxOuter); EditorGUILayout.LabelField(GetLoc("ShellProtector"), customToggleFont); EditorGUILayout.BeginVertical(boxInnerHalf); - if (mip_tex != null) - m_MaterialEditor.ShaderProperty(mip_tex, "Mip reference texture"); - if(encrypted_tex0 != null) - m_MaterialEditor.ShaderProperty(encrypted_tex0, "Encrypted texture0"); - if (encrypted_tex1 != null) - m_MaterialEditor.ShaderProperty(encrypted_tex1, "Encrypted texture1"); + if (mipTexture != null) + m_MaterialEditor.ShaderProperty(mipTexture, "Mip reference texture"); + if(encryptedTexture0 != null) + m_MaterialEditor.ShaderProperty(encryptedTexture0, "Encrypted texture0"); + if (encryptedTexture1 != null) + m_MaterialEditor.ShaderProperty(encryptedTexture1, "Encrypted texture1"); EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical(); - show_pwd = Foldout("Keys", "Keys", show_pwd); - if(show_pwd) + m_MaterialEditor.ShaderProperty(passwordHash, "Password hash"); + m_MaterialEditor.ShaderProperty(hashMagic, "Hash Salt"); + + showPassword = Foldout("Keys", "Keys", showPassword); + if(showPassword) { for(int i = 0; i < key.Length; ++i) { @@ -78,6 +83,9 @@ protected override void DrawCustomProperties(Material material) m_MaterialEditor.ShaderProperty(key[i], "Key" + i); } } + + var hash = KeyGenerator.SimpleHash(key.Select(k => (byte)Mathf.RoundToInt(k.floatValue)).ToArray(), (uint)hashMagic.intValue); + EditorGUILayout.LabelField("Password hash", hash.ToString()); } } @@ -165,4 +173,4 @@ private static void ConvertMaterialToCustomShaderMenu() } } #endif -#endif \ No newline at end of file +#endif diff --git a/README.ENG.md b/README.ENG.md index c1dbbe4..b23c937 100644 --- a/README.ENG.md +++ b/README.ENG.md @@ -19,26 +19,28 @@ You can easily enter the password with the OSC program. Source code of OSC: https://github.com/Shell4026/ShellProtectorOSC ## Supported shaders -- Poiyomi 7.3(Unstable), 8.0, 8.1, 8.2, 9.0, 9.1, 9.2(pro) PCSS(Need to more testing) -- lilToon (1.3.8 ~ 1.7.3)(VCC) - +- Poiyomi 8.x, 9.x, 10.x(pro), PCSS(Not recommended) +- lilToon (1.3.8 ~ 2.3.4)(VCC) + ## Supported texture formats - RGB24, RGBA32 - DXT1, DXT5 - The Crunch Compression format will auto-convert to DXT1 or DXT5. +- **BC7 is not supported** + - For textures without transparency, halving the size while keeping the RGB24 format will result in similar quality and texture size. ## Features - Texture Encryption - OSC programs for descryption - Blendshape obfuscation -- Fallback: the ability to make non-friends see a 16x16 texture instead of encryption noise +- Fallback: the ability to make non-friends see a small texture instead of encryption noise ## Usage 1. Right-click on your avatar and select "Shell Protector" to add the component. 2. Set a password and specify the Material or GameObject to encrypt. -(The steps below are not required when using Modular avatar) +(The steps(3,4) below are not required when using Modular avatar) 3. Click the Encrypt button. 4. Check the encryption via the Testor component in the new avatar and press the Done button. @@ -47,13 +49,16 @@ Source code of OSC: https://github.com/Shell4026/ShellProtectorOSC ### If your password is more than 4 digits (OSC) 1. Download ShellProtectorOSC.zip from the release, unzip it, and run ShellProtectorOSC.exe. (You only need to run it once the first time. If you use a reset avatar, keep it on.) 2. Replace your uploaded avatar and enter your user password in the OSC program. -3. If changing the password doesn't change the appearance of your avatar, try going to the Action menu - Options - OSC - Reset Config in VRChat. -4. If you're still having trouble, try clearing the C:\Users\UserName\AppData\LocalLow\VRChat\VRChat\OSC folder. +3. Starting with version 2.5.0, you must enable 'Parameter-multiplexing'. +4. If changing the password doesn't change the appearance of your avatar, try going to the Action menu - Options - OSC - Reset Config in VRChat. +5. If you're still having trouble, try clearing the C:\Users\UserName\AppData\LocalLow\VRChat\VRChat\OSC folder. ### Parameter-multiplexing +**Supported by default starting with version 2.5.0.** + Detailed principle:https://github.com/seanedwards/vrc-worldobject/blob/main/docs/parameter-multiplexing.md -This is a parameter-saving technique. After checking, OSC must always be turned on and Parameter-multiplexing must be checked in the OSC program. +This is a parameter-saving technique. After checking, OSC must always be **turned on and Parameter-multiplexing** must be checked in the OSC program. This will slightly increase the time it takes to get back to your original avatar appearance in-game. @@ -63,8 +68,11 @@ When using parameter multiplexing, depending on the server or network conditions In this case, try increasing the refresh rate slightly, which was added in OSC 1.5.0. +It is applied by default from version 2.5.0beta. + ### Avatar fallback -A feature that allows anyone with Safety On when encryption is in place to appear as a degraded version of themselves when viewing your avatar. +A feature that allows anyone with Safety On when encryption is in place to appear as a degraded version of themselves when viewing your avatar.
+It appears as a fallback texture even when the encryption hasn't been decrypted. ![fallback](https://github.com/user-attachments/assets/d3ca69b0-ff08-4793-a4e4-73269bc8efd3) ## Troubleshooting @@ -72,7 +80,7 @@ If you find the issue, please raise it in Issues. **[is not supported texture format! Error]** -Select the texture and change the compression format to either DXT1 or DXT5 in the Inspector. (DXT5 for textures with transparency) +Select the texture and change the compression format to either DXT1/RGB24 or DXT5/RGBA32 in the Inspector. (DXT5 for textures with transparency) ![texture](https://github.com/Shell4026/ShellProtector/assets/104874910/872f9d15-7b89-4381-b940-00514bd60638) @@ -98,6 +106,8 @@ When using parameter multiplexing, depending on the server or network conditions In this case, try increasing the refresh rate slightly, which was added in OSC 1.5.0. +This can also occur when there are parameter-compressed assets. + **[When a Texture that was present in a Material is missing]** When using a texture such as a main color/texture, it will be stripped for security reasons. @@ -130,6 +140,8 @@ Bilinear filtering: 0.35ms This may not seem like a huge difference, but for performance reasons, I recommend only encrypting textures that are absolutely necessary. +Encrypting too many parts may cause lag during the decryption process. + ## How secure is it? By default, it has 16 bytes of keys, split between keys stored inside the shader and keys that the user can enter using VRC parameters. (I'll call these user keys.) diff --git a/README.JP.md b/README.JP.md index 2bb2a18..4a533b2 100644 --- a/README.JP.md +++ b/README.JP.md @@ -21,19 +21,21 @@ OSCプログラムで簡単にパスワードを入力することができま OSCのソースコード: https://github.com/Shell4026/ShellProtectorOSC ## 対応シェーダー -- Poiyomi 8.0, 8.1, 8.2, 9.0, 9.1, 9.2(pro), PCSS(より多くのテストが必要) -- lilToon (1.3.8 ~ 1.7.3)(VCC) +- Poiyomi 8.x, 9.x, 10.x(pro), PCSS(推奨しません) +- lilToon (1.3.8 ~ 2.3.4)(VCC) ## 対応テクスチャ形式 - RGB24, RGBA32 - DXT1, DXT5 - Crunch Compressionフォーマットは自動的にDXT1またはDXT5に変換されます。 +- **BC7はサポートしていません** + - 透明度のないテクスチャの場合、RGB24でサイズを半分に縮小すると、同等の品質とテクスチャサイズが得られます。 ## サポートする機能 - テクスチャ暗号化 - 暗号を解読するためのOSCプログラム - Blendshape難読化 -- フォールバック: 友達以外のユーザーには、暗号化ノイズの代わりに16x16テクスチャで見えるようにする機能。 +- フォールバック: 友達以外のユーザーには、暗号化ノイズの代わりに小さなテクスチャで見えるようにする機能。 ## 使用方法 1. アバターを右クリックして「Shell Protector」を押してコンポーネントを追加します。 @@ -48,13 +50,16 @@ OSCのソースコード: https://github.com/Shell4026/ShellProtectorOSC ### 自分のパスワードが4桁以上の場合 (OSC) 1. ReleaseにあるShellProtectorOSC.zipをダウンロードし、解凍してShellProtectorOSC.exeを実行します。 (最初に一度だけ実行するだけです。リセットアバターやパラメータ多重化を使用する場合は、継続してオンにしてください。) 2. アップロードしたアバターに変更した後、OSCプログラムでユーザーパスワードを入力します。 -3. もしパスワードが変わってもアバターの外観に変化がなければ、VRChatでアクションメニュー - Options - OSC - Reset Configを押してください。 -4. それでも問題がある場合は、C:\Users\ユーザー\AppData\LocalLow\VRChat\VRChat\OSCフォルダを消去してください。 +3. バージョン2.5.0以降では、「Parameter-multiplexing」に必ずチェックを入れる必要があります。 +4. もしパスワードが変わってもアバターの外観に変化がなければ、VRChatでアクションメニュー - Options - OSC - Reset Configを押してください。 +5. それでも問題がある場合は、C:\Users\ユーザー\AppData\LocalLow\VRChat\VRChat\OSCフォルダを消去してください。 ### パラメータマルチプレックス +**バージョン2.5.0から標準でサポートされています。** + 詳細原理:https://github.com/seanedwards/vrc-worldobject/blob/main/docs/parameter-multiplexing.md -パラメータ節約技術です。チェック後、OSCを常にオンにしておく必要があり、OSCプログラムにもParameter-multiplexingをチェックする必要があります。 +パラメータ節約技術です。チェック後、OSCを常にオンにしておく必要があり、OSCプログラムにも**Parameter-multiplexingをチェック**する必要があります。 ゲーム内で元の姿に戻るまでの時間が若干増加します。 @@ -64,8 +69,11 @@ OSCのソースコード: https://github.com/Shell4026/ShellProtectorOSC この場合、OSC 1.5.0で追加されたRefresh rateを少し上げてみてください。 +2.5.0betaからデフォルトで適用されます。 + ### アバターフォールバック -暗号化がかかっているときにSafetyがオンになっている人は、アバターを見るときに劣化したバージョンに見えるようにする機能です。 +暗号化がかかっているときにSafetyがオンになっている人は、アバターを見るときに劣化したバージョンに見えるようにする機能です。
+暗号化が解除されていない場合でもフォールバックテクスチャとして表示されます。 ![fallback](https://github.com/user-attachments/assets/d3ca69b0-ff08-4793-a4e4-73269bc8efd3) ## 問題解決 @@ -73,7 +81,7 @@ OSCのソースコード: https://github.com/Shell4026/ShellProtectorOSC **** -テクスチャを選択後、Inspectorで圧縮フォーマットをDXT1またはDXT5のいずれかに変更してください。 (透明度のあるテクスチャはDXT5) +テクスチャを選択後、Inspectorで圧縮フォーマットをDXT1/RGB24またはDXT5/RGBA32のいずれかに変更してください。 (透明度のあるテクスチャはDXT5) ![texture](https://github.com/Shell4026/ShellProtector/assets/104874910/872f9d15-7b89-4381-b940-00514bd60638) @@ -99,6 +107,8 @@ lilToonのバグなので、無視してアップロードするか、3つの方 この場合、OSC 1.5.0で追加されたRefresh rateを少し上げてみてください。 +パラメータ圧縮されたアセットがある場合にも発生する可能性があります。 + **<マテリアルにあったテクスチャが消える場合>** メインカラーと同じテクスチャを使用する場合、セキュリティ上の理由でそのテクスチャは削除されます。 @@ -132,6 +142,8 @@ Bilinear:0.35ms それほど大きな差はないようですが、パフォーマンスを考えると、必要なテクスチャだけを暗号化することをお勧めします。 +暗号化する部分が多すぎると、復号処理中に遅延が発生する可能性があります。 + ## どの程度安全ですか 基本的に16バイトのキーを持ち、シェーダー内部に保存されるキーとユーザーがVRCパラメータを利用して入力できるキーに分かれています(ユーザーキーと呼びます)。 diff --git a/README.md b/README.md index 97a654b..3596671 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FShell4026%2FShellProtector&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) # VCC : https://shell4026.github.io/VCC/ +347042998-b6e62237-67fe-4a4b-a415-12080ca64f01 +https://Shell4026.github.io/VCC/index.json + + 권장 유니티 버전 : 2022 ## | 한국어 | [English](./README.ENG.md) | [日本語](./README.JP.md) | @@ -19,25 +23,27 @@ OSC 프로그램으로 간편하게 비밀번호를 입력할 수 있습니다. OSC 소스 코드: https://github.com/Shell4026/ShellProtectorOSC ## 지원 셰이더 -- Poiyomi 7.3(불안정), 8.0, 8.1, 8.2, 9.0, 9.1, 9.2(pro), PCSS(테스트 더 필요) -- lilToon (1.3.8 ~ 1.7.3)(VCC) +- Poiyomi 8.x, 9.x, 10.x(pro) PCSS(권장하지 않음) +- lilToon (1.3.8 ~ 2.3.4)(VCC) ## 지원 텍스쳐 형식 - RGB24, RGBA32 - DXT1, DXT5 - Crunch Compression 포멧은 자동으로 DXT1이나 DXT5로 변환 됩니다. +- **BC7은 지원하지 않습니다** + - 투명도가 없는 텍스쳐의 경우 RGB24에 사이즈를 반으로 줄이면 비슷한 품질과 텍스쳐 사이즈를 보입니다. ## 지원하는 기능 - 텍스쳐 암호화 - 암호화를 풀기 위한 OSC 프로그램 - 쉐이프키 난독화 -- 폴백: 친구가 아닌 유저에게는 암호화 노이즈 대신 16x16 텍스쳐로 보이게 하는 기능 +- 폴백: 친구가 아닌 유저에게는 암호화 노이즈 대신 작은 텍스쳐로 보이게 하는 기능 ## 사용법 1. 아바타를 우클릭해 'Shell Protector'를 눌러 컴포넌트를 추가합니다. 2. 비밀번호를 설정해주고 암호화 할 메테리얼이나 게임오브젝트를 지정해줍니다. -(모듈러 사용시 아래 과정은 필요 없습니다.) +(모듈러 사용시 아래 과정(3,4)은 필요 없습니다.) 3. Encrypt 버튼을 누르세요. 4. 새로 생긴 아바타에 들어간 Testor컴포넌트를 통해 암호화 여부를 확인하고 완료 버튼을 누르세요. @@ -46,10 +52,13 @@ OSC 소스 코드: https://github.com/Shell4026/ShellProtectorOSC ### 자신의 비밀번호가 4자리 이상인 경우 (OSC) 1. Release에 있는 ShellProtectorOSC.zip을 다운 후 압축을 풀고 ShellProtectorOSC.exe를 실행시킵니다. (최초 한 번만 실행하면 됩니다. 리셋 아바타나 파라미터 멀티플렉싱을 사용한다면 계속 켜두세요.) 2. 업로드 한 아바타로 바꾼 후 OSC프로그램에서 사용자 비밀번호를 입력합니다. -3. 만약 비밀번호가 바뀌어도 아바타의 외형에 변화가 없다면 VRChat에서 액션 메뉴 - Options - OSC - Reset Config를 눌러보세요. -4. 그래도 문제가 있다면 C:\Users\유저\AppData\LocalLow\VRChat\VRChat\OSC 폴더를 지워보세요. +3. 2.5.0이후부터 Parameter-multiplexing를 필수로 체크해야 합니다. +4. 만약 비밀번호가 바뀌어도 아바타의 외형에 변화가 없다면 VRChat에서 액션 메뉴 - Options - OSC - Reset Config를 눌러보세요. +5. 그래도 문제가 있다면 C:\Users\유저\AppData\LocalLow\VRChat\VRChat\OSC 폴더를 지워보세요. ### 파라미터 멀티플렉싱 +**2.5.0부터 기본적으로 지원합니다.** + 세부 원리:https://github.com/seanedwards/vrc-worldobject/blob/main/docs/parameter-multiplexing.md 파라미터 절약 기술입니다. 체크 후 OSC를 항상 켜둬야하며 OSC프로그램에도 **Parameter-multiplexing을 체크** 해야 합니다. @@ -65,7 +74,8 @@ OSC 소스 코드: https://github.com/Shell4026/ShellProtectorOSC 2.5.0beta부터 기본 적용 됩니다. ### 아바타 폴백 -암호화가 걸려있을 때 세이프티가 켜져있는 사람은 아바타를 볼 때 열화된 버전으로 보이게 하는 기능입니다. +암호화가 걸려있을 때 세이프티가 켜져있는 사람은 아바타를 볼 때 열화된 버전으로 보이게 하는 기능입니다.
+암호화가 풀리지 않았을 때도 폴백 텍스쳐로 보입니다. ![fallback](https://github.com/user-attachments/assets/d3ca69b0-ff08-4793-a4e4-73269bc8efd3) ## 문제해결 @@ -73,7 +83,7 @@ OSC 소스 코드: https://github.com/Shell4026/ShellProtectorOSC **** -텍스쳐를 선택 후 Inspector에서 압축 포멧을 DXT1이나 DXT5중 하나로 바꿔주세요. (투명도가 있는 텍스쳐는 DXT5) +텍스쳐를 선택 후 Inspector에서 압축 포멧을 DXT1/RGB24나 DXT5/RGBA32중 하나로 바꿔주세요. (투명도가 있는 텍스쳐는 DXT5) ![texture](https://github.com/Shell4026/ShellProtector/assets/104874910/872f9d15-7b89-4381-b940-00514bd60638) @@ -99,6 +109,8 @@ OSC 소스 코드: https://github.com/Shell4026/ShellProtectorOSC 이 경우 OSC 1.5.0에 추가된 Refresh rate를 조금 올려보시길 바랍니다. +파라미터 압축 에셋이 있는 경우에도 발생할 수 있습니다. + **<메테리얼에서 있던 텍스쳐가 빠지는 경우>** 메인 컬러와 같은 텍스쳐 사용시 보안상의 이유로 해당 텍스쳐는 빠집니다. @@ -133,6 +145,8 @@ Bilinear 필터링: 0.35ms 그렇게 큰 차이는 아닌 것으로 보이나, 성능을 생각하면 꼭 필요한 텍스쳐만 암호화 하는 것을 추천합니다. +너무 많은 부위를 암호화 하면 복호화 과정에서 렉이 걸리는 현상이 있을 수 있습니다. + ## 얼마나 안전한가요? 기본적으로 16바이트의 키를 가지며, 셰이더 내부에 저장되는 키와 사용자가 VRC 파라미터를 이용하여 입력할 수 있는 키로 나누어져 있습니다. (사용자 키라고 부르겠습니다.) diff --git a/Runtime/Chacha.cginc b/Runtime/Chacha.cginc deleted file mode 100644 index 3aba812..0000000 --- a/Runtime/Chacha.cginc +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once -static const uint CHACHA20_ROUNDS = 8; -uint _Nonce0; -uint _Nonce1; -uint _Nonce2; - -uint Rotl32(uint x, int n) -{ - return x << n | (x >> (32 - n)); -} - -void Chacha20QuarterRound(inout uint state[16], int a, int b, int c, int d) -{ - state[a] += state[b]; state[d] = Rotl32(state[d] ^ state[a], 16); - state[c] += state[d]; state[b] = Rotl32(state[b] ^ state[c], 12); - state[a] += state[b]; state[d] = Rotl32(state[d] ^ state[a], 8); - state[c] += state[d]; state[b] = Rotl32(state[b] ^ state[c], 7); -} - -void Chacha20XOR(inout uint data[2], const uint key[4]) -{ - uint4 state[4]; - state[0] = uint4(0x61707865, 0x3320646e, 0x79622d32, 0x6b206574); - state[1] = uint4(key[0], key[1], key[2], key[3]); - state[2] = state[1]; - state[3] = uint4(1, _Nonce0, _Nonce1, _Nonce2); - - uint block[16]; - //block - [unroll] - int i = 0; - for(i = 0; i < 4; ++i) - { - block[i * 4 + 0] = state[i].x; - block[i * 4 + 1] = state[i].y; - block[i * 4 + 2] = state[i].z; - block[i * 4 + 3] = state[i].w; - } - [unroll] - for (i = 0; i < CHACHA20_ROUNDS; i += 2) - { - Chacha20QuarterRound(block, 0, 4, 8, 12); - Chacha20QuarterRound(block, 1, 5, 9, 13); - Chacha20QuarterRound(block, 2, 6, 10, 14); - Chacha20QuarterRound(block, 3, 7, 11, 15); - Chacha20QuarterRound(block, 0, 5, 10, 15); - Chacha20QuarterRound(block, 1, 6, 11, 12); - Chacha20QuarterRound(block, 2, 7, 8, 13); - Chacha20QuarterRound(block, 3, 4, 9, 14); - } - [unroll] - for(i = 0; i < 4; ++i) - { - block[i * 4 + 0] += state[i].x; - block[i * 4 + 1] += state[i].y; - block[i * 4 + 2] += state[i].z; - block[i * 4 + 3] += state[i].w; - } - // - data[0] ^= block[0]; - data[1] ^= block[1]; -} - -void Chacha20XOR(inout uint data[3], const uint key[4]) -{ - uint4 state[4]; - state[0] = uint4(0x61707865, 0x3320646e, 0x79622d32, 0x6b206574); - state[1] = uint4(key[0], key[1], key[2], key[3]); - state[2] = state[1]; - state[3] = uint4(1, _Nonce0, _Nonce1, _Nonce2); - - uint block[16]; - //block - int i = 0; - [unroll] - for(i = 0; i < 4; ++i) - { - block[i * 4 + 0] = state[i].x; - block[i * 4 + 1] = state[i].y; - block[i * 4 + 2] = state[i].z; - block[i * 4 + 3] = state[i].w; - } - [unroll] - for (i = 0; i < CHACHA20_ROUNDS; i += 2) - { - Chacha20QuarterRound(block, 0, 4, 8, 12); - Chacha20QuarterRound(block, 1, 5, 9, 13); - Chacha20QuarterRound(block, 2, 6, 10, 14); - Chacha20QuarterRound(block, 3, 7, 11, 15); - Chacha20QuarterRound(block, 0, 5, 10, 15); - Chacha20QuarterRound(block, 1, 6, 11, 12); - Chacha20QuarterRound(block, 2, 7, 8, 13); - Chacha20QuarterRound(block, 3, 4, 9, 14); - } - [unroll] - for(i = 0; i < 4; ++i) - { - block[i * 4 + 0] += state[i].x; - block[i * 4 + 1] += state[i].y; - block[i * 4 + 2] += state[i].z; - block[i * 4 + 3] += state[i].w; - } - // - data[0] ^= block[0]; - data[1] ^= block[1]; - data[2] ^= block[2]; -} \ No newline at end of file diff --git a/Runtime/Decrypt.cginc b/Runtime/Decrypt.cginc deleted file mode 100644 index 7de41a0..0000000 --- a/Runtime/Decrypt.cginc +++ /dev/null @@ -1,166 +0,0 @@ -#pragma once -#include "XXTEA.cginc" - -static const uint mw[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; -static const uint mh[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; - -float _Key0, _Key1, _Key2, _Key3, _Key4, _Key5, _Key6, _Key7, _Key8, _Key9, _Key10, _Key11, _Key12, _Key13, _Key14, _Key15; - -uint _Woffset; -uint _Hoffset; - -half3 InverseGammaCorrection(half3 rgb) -{ - half3 result = pow(rgb, 0.454545); - return result; -} -half3 GammaCorrection(half3 rgb) -{ - //half3 result = pow(rgb, 2.2); - half3 result = rgb * rgb * (rgb * (half)0.2 + (half)0.8); //fast pow - return result; -} - -half2 GetUV(int idx, int m, int woffset = 0, int hoffset = 0) -{ - int w = idx % mw[m + woffset]; - int h = idx / mw[m + woffset]; - return half2((half)w/mw[m + woffset], (half)h/mh[m + hoffset]); -} - -half4 DecryptTexture(half2 uv, int m) -{ - half x = frac(uv.x); - half y = frac(uv.y); - - int idx = (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 2) << 2) - }; - - half3 pixels[4]; - - const int pos[4] = { 0, -1, -2, -3 }; - int offset = pos[idx % 4]; - pixels[0] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[2] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 2 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[3] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 3 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[3] = { 0, 0, 0 }; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[1].r * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].g * 255.0f) | ((uint)round(pixels[1].b * 255.0f) << 8) | ((uint)round(pixels[2].r * 255.0f) << 16) | ((uint)round(pixels[2].g * 255.0f) << 24)); - data[2] = ((uint)round(pixels[2].b * 255.0f) | ((uint)round(pixels[3].r * 255.0f) << 8) | ((uint)round(pixels[3].g * 255.0f) << 16) | ((uint)round(pixels[3].b * 255.0f) << 24)); - - XXTEADecrypt(data, key); - - half r[4] = { (data[0] & 0x000000FF)/255.0f, ((data[0] & 0xFF000000) >> 24)/255.0f, ((data[1] & 0x00FF0000) >> 16)/255.0f, ((data[2] & 0x0000FF00) >> 8)/255.0f }; - half g[4] = { ((data[0] & 0x0000FF00) >> 8)/255.0f, ((data[1] & 0x000000FF) >> 0)/255.0f, ((data[1] & 0xFF000000) >> 24)/255.0f, ((data[2] & 0x00FF0000) >> 16)/255.0f }; - half b[4] = { ((data[0] & 0x00FF0000) >> 16)/255.0f, ((data[1] & 0x0000FF00) >> 8)/255.0f, ((data[2] & 0x000000FF) >> 0)/255.0f, ((data[2] & 0xFF000000) >> 24)/255.0f }; - half3 decrypt = half3(r[idx % 4], g[idx % 4], b[idx % 4]); - - return half4(GammaCorrection(decrypt), 1.0); -} -half4 DecryptTextureRGBA(half2 uv, int m) -{ - half x = frac(uv.x); - half y = frac(uv.y); - - int idx = (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 1) << 1) - }; - - half4 pixels[2]; - - int offset = (idx & 1) == 0 ? 0 : -1; - pixels[0] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[2] = { 0, 0 }; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); - - XXTEADecrypt(data, key); - - half r = ((data[idx & 1] & 0x000000FF) >> 0)/255.0f; - half g = ((data[idx & 1] & 0x0000FF00) >> 8)/255.0f; - half b = ((data[idx & 1] & 0x00FF0000) >> 16)/255.0f; - half a = ((data[idx & 1] & 0xFF000000) >> 24)/255.0f; - half4 decrypt = half4(r, g, b, a); - - return half4(GammaCorrection(decrypt.rgb), decrypt.a); -} -half4 DecryptTextureDXT(half2 uv, int m) -{ - half2 frac_uv = frac(uv.xy); - half x = frac_uv.x; - half y = frac_uv.y; - - half4 col = _EncryptTex0.SampleLevel(sampler_EncryptTex0, uv, m); - - uint w = mw[m + (uint)_Woffset]; - uint h = mh[m + _Hoffset]; - - int idx = (w * floor(y * h)) + floor(x * w); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 1) << 1) - }; - - int offset = (idx & 1) == 0 ? 0 : -1; - - half4 pixels[2]; - pixels[0] = _EncryptTex1.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex1.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[2]; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); - - XXTEADecrypt(data, key); - - uint r = (data[idx & 1] & 0x000000FF) >> 0; - uint g = (data[idx & 1] & 0x0000FF00) >> 8; - uint b = (data[idx & 1] & 0x00FF0000) >> 16; - uint a = (data[idx & 1] & 0xFF000000) >> 24; - - uint color1 = (r | g << 8); - uint color2 = (b | a << 8); - - uint color1_r = (color1 & 0xF800) >> 11; - color1_r = color1_r << 3 | color1_r >> 2; - uint color1_g = (color1 & 0x7E0) >> 5; - color1_g = color1_g << 2 | color1_g >> 4; - uint color1_b= color1 & 0x1F; - color1_b = color1_b << 3 | color1_b >> 2; - - uint color2_r = (color2 & 0xF800) >> 11; - color2_r = color2_r << 3 | color2_r >> 2; - uint color2_g = (color2 & 0x7E0) >> 5; - color2_g = color2_g << 2 | color2_g >> 4; - uint color2_b= color2 & 0x1F; - color2_b = color2_b << 3 | color2_b >> 2; - - half3 col1 = half3(color1_r / (half)255, color1_g / (half)255, color1_b / (half)255); - half3 col2 = half3(color2_r / (half)255, color2_g / (half)255, color2_b / (half)255); - - half3 result; - result = lerp(col2, col1, color1 > color2 ? col.rgb : 0.5); - - return half4(GammaCorrection(result), col.a); -} \ No newline at end of file diff --git a/Runtime/DecryptChacha.cginc b/Runtime/DecryptChacha.cginc deleted file mode 100644 index 3264d5c..0000000 --- a/Runtime/DecryptChacha.cginc +++ /dev/null @@ -1,168 +0,0 @@ -#pragma once -#include "Chacha.cginc" - -static const uint mw[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; -static const uint mh[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; - -float _Key0, _Key1, _Key2, _Key3, _Key4, _Key5, _Key6, _Key7, _Key8, _Key9, _Key10, _Key11, _Key12, _Key13, _Key14, _Key15; - -uint _Woffset; -uint _Hoffset; - -half3 InverseGammaCorrection(half3 rgb) -{ - half3 result = pow(rgb, 0.454545); - return result; -} -half3 GammaCorrection(half3 rgb) -{ - //half3 result = pow(rgb, 2.2); - half3 result = rgb * rgb * (rgb * (half)0.2 + (half)0.8); //fast pow - return result; -} - -half2 GetUV(int idx, int m, int woffset = 0, int hoffset = 0) -{ - int w = idx % mw[m + woffset]; - int h = idx / mw[m + woffset]; - return half2((half)w/mw[m + woffset], (half)h/mh[m + hoffset]); -} - -half4 DecryptTexture(half2 uv, int m) -{ - half x = frac(uv.x); - half y = frac(uv.y); - - int idx = (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 2) << 2) - }; - - half3 pixels[4]; - - const int pos[4] = { 0, -1, -2, -3 }; - int offset = pos[idx % 4]; - pixels[0] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[2] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 2 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[3] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 3 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[3] = { 0, 0, 0 }; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[1].r * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].g * 255.0f) | ((uint)round(pixels[1].b * 255.0f) << 8) | ((uint)round(pixels[2].r * 255.0f) << 16) | ((uint)round(pixels[2].g * 255.0f) << 24)); - data[2] = ((uint)round(pixels[2].b * 255.0f) | ((uint)round(pixels[3].r * 255.0f) << 8) | ((uint)round(pixels[3].g * 255.0f) << 16) | ((uint)round(pixels[3].b * 255.0f) << 24)); - - Chacha20XOR(data, key); - - half r[4] = { (data[0] & 0x000000FF)/255.0f, ((data[0] & 0xFF000000) >> 24)/255.0f, ((data[1] & 0x00FF0000) >> 16)/255.0f, ((data[2] & 0x0000FF00) >> 8)/255.0f }; - half g[4] = { ((data[0] & 0x0000FF00) >> 8)/255.0f, ((data[1] & 0x000000FF) >> 0)/255.0f, ((data[1] & 0xFF000000) >> 24)/255.0f, ((data[2] & 0x00FF0000) >> 16)/255.0f }; - half b[4] = { ((data[0] & 0x00FF0000) >> 16)/255.0f, ((data[1] & 0x0000FF00) >> 8)/255.0f, ((data[2] & 0x000000FF) >> 0)/255.0f, ((data[2] & 0xFF000000) >> 24)/255.0f }; - half3 decrypt = half3(r[idx % 4], g[idx % 4], b[idx % 4]); - - return half4(GammaCorrection(decrypt), 1.0); -} -half4 DecryptTextureRGBA(half2 uv, int m) -{ - half x = frac(uv.x); - half y = frac(uv.y); - - int idx = (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 1) << 1) - }; - - half4 pixels[2]; - - int offset = (idx & 1) == 0 ? 0 : -1; - pixels[0] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[2] = { 0, 0 }; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); - - Chacha20XOR(data, key); - - half r = ((data[idx & 1] & 0x000000FF) >> 0)/255.0f; - half g = ((data[idx & 1] & 0x0000FF00) >> 8)/255.0f; - half b = ((data[idx & 1] & 0x00FF0000) >> 16)/255.0f; - half a = ((data[idx & 1] & 0xFF000000) >> 24)/255.0f; - half4 decrypt = half4(r, g, b, a); - - return half4(GammaCorrection(decrypt.rgb), decrypt.a); -} -half4 DecryptTextureDXT(half2 uv, int m) -{ - half2 frac_uv = frac(uv.xy); - half x = frac_uv.x; - half y = frac_uv.y; - - int miplv = 0; - - half4 col = _EncryptTex0.SampleLevel(sampler_EncryptTex0, uv, m); - - uint w = mw[m + (uint)_Woffset]; - uint h = mh[m + _Hoffset]; - - int idx = (w * floor(y * h)) + floor(x * w); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 1) << 1) - }; - - int offset = (idx & 1) == 0 ? 0 : -1; - - half4 pixels[2]; - pixels[0] = _EncryptTex1.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex1.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[2]; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); - - Chacha20XOR(data, key); - - uint r = (data[idx & 1] & 0x000000FF) >> 0; - uint g = (data[idx & 1] & 0x0000FF00) >> 8; - uint b = (data[idx & 1] & 0x00FF0000) >> 16; - uint a = (data[idx & 1] & 0xFF000000) >> 24; - - uint color1 = (r | g << 8); - uint color2 = (b | a << 8); - - uint color1_r = (color1 & 0xF800) >> 11; - color1_r = color1_r << 3 | color1_r >> 2; - uint color1_g = (color1 & 0x7E0) >> 5; - color1_g = color1_g << 2 | color1_g >> 4; - uint color1_b= color1 & 0x1F; - color1_b = color1_b << 3 | color1_b >> 2; - - uint color2_r = (color2 & 0xF800) >> 11; - color2_r = color2_r << 3 | color2_r >> 2; - uint color2_g = (color2 & 0x7E0) >> 5; - color2_g = color2_g << 2 | color2_g >> 4; - uint color2_b= color2 & 0x1F; - color2_b = color2_b << 3 | color2_b >> 2; - - half3 col1 = half3(color1_r / (half)255, color1_g / (half)255, color1_b / (half)255); - half3 col2 = half3(color2_r / (half)255, color2_g / (half)255, color2_b / (half)255); - - half3 result; - result = lerp(col2, col1, color1 > color2 ? col.rgb : 0.5); - - return half4(GammaCorrection(result), col.a); -} \ No newline at end of file diff --git a/Runtime/Scripts/Algorithm/Chacha20.cs b/Runtime/Scripts/Algorithm/Chacha20.cs index 4e7185e..4e9c029 100644 --- a/Runtime/Scripts/Algorithm/Chacha20.cs +++ b/Runtime/Scripts/Algorithm/Chacha20.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Runtime.CompilerServices; using UnityEngine; @@ -7,7 +7,8 @@ namespace Shell.Protector { public class Chacha20 : IEncryptor { - public byte[] nonce = new byte[12]; + public string Keyword => ShaderProperties.ChachaKeyword; + public byte[] Nonce { get; } = new byte[12]; [MethodImpl(MethodImplOptions.AggressiveInlining)] byte[] U32t8le(uint v) { @@ -55,12 +56,12 @@ byte[] Chacha20Serialize(uint[] input) } return output; } - byte[] Chacha20Block(uint[] input, int num_rounds) + byte[] Chacha20Block(uint[] input, int numRounds) { byte[] output = new byte[64]; uint[] x = new uint[16]; Array.Copy(input, 0, x, 0, input.Length); - for (int i = num_rounds; i > 0; i -= 2) + for (int i = numRounds; i > 0; i -= 2) { Chacha20QuarterRound(x, 0, 4, 8, 12); Chacha20QuarterRound(x, 1, 5, 9, 13); @@ -154,7 +155,7 @@ public uint[] Encrypt(uint[] data, uint[] key) dataBytes[i + 3] = (byte)(data[i / 4] >> 24 & 0xFF); } - byte[] resultBytes = ChaCha20XOR(keyBytes, 1, nonce, dataBytes); + byte[] resultBytes = ChaCha20XOR(keyBytes, 1, Nonce, dataBytes); uint[] result = new uint[data.Length]; for (int i = 0; i < data.Length; ++i) { @@ -173,11 +174,11 @@ public uint[] GetNonceUint3() byte[] nonceTmp = new byte[4]; for (int i = 0; i < 3; i++) { - Array.Copy(nonce, i * 4, nonceTmp, 0, nonceTmp.Length); + Array.Copy(Nonce, i * 4, nonceTmp, 0, nonceTmp.Length); result[i] = U8t32le(nonceTmp); } return result; } } -} \ No newline at end of file +} diff --git a/Runtime/Scripts/Algorithm/IEncryptor.cs b/Runtime/Scripts/Algorithm/IEncryptor.cs index c112ec7..d6b82d9 100644 --- a/Runtime/Scripts/Algorithm/IEncryptor.cs +++ b/Runtime/Scripts/Algorithm/IEncryptor.cs @@ -1,7 +1,9 @@ namespace Shell.Protector { - public interface IEncryptor + public interface ITextureEncryptor { + public string Keyword { get; } + #if UNITY_2022 public uint[] Encrypt(uint[] data, uint[] key); public uint[] Decrypt(uint[] data, uint[] key); @@ -10,4 +12,8 @@ public interface IEncryptor uint[] Decrypt(uint[] data, uint[] key); #endif } -} \ No newline at end of file + + public interface IEncryptor : ITextureEncryptor + { + } +} diff --git a/Runtime/Scripts/Algorithm/XXTEA.cs b/Runtime/Scripts/Algorithm/XXTEA.cs index fa91c22..4d38ba7 100644 --- a/Runtime/Scripts/Algorithm/XXTEA.cs +++ b/Runtime/Scripts/Algorithm/XXTEA.cs @@ -1,13 +1,14 @@ -using System; +using System; using UnityEngine; namespace Shell.Protector { public class XXTEA : IEncryptor { + public string Keyword => ShaderProperties.XXTEAKeyword; const uint Delta = 0x9E3779B9; - public uint m_rounds = 0; + public uint Rounds { get; set; } public uint[] Encrypt(uint[] data, uint[] key) { uint n = (uint)data.Length; @@ -27,10 +28,10 @@ public uint[] Encrypt(uint[] data, uint[] key) uint y, z, sum; uint p, rounds, e; - if (m_rounds == 0) + if (Rounds == 0) rounds = 6 + 52 / n; else - rounds = m_rounds; + rounds = Rounds; sum = 0; z = result[n - 1]; do @@ -62,10 +63,10 @@ public uint[] Decrypt(uint[] data, uint[] key) uint y, z, sum; uint p, rounds, e; - if (m_rounds == 0) + if (Rounds == 0) rounds = 6 + 52 / n; else - rounds = m_rounds; + rounds = Rounds; sum = rounds * Delta; y = result[0]; @@ -84,4 +85,4 @@ public uint[] Decrypt(uint[] data, uint[] key) return result; } } -} \ No newline at end of file +} diff --git a/Runtime/Scripts/AnimatorManager.cs b/Runtime/Scripts/AnimatorManager.cs index 90e2c5b..67a7ebc 100644 --- a/Runtime/Scripts/AnimatorManager.cs +++ b/Runtime/Scripts/AnimatorManager.cs @@ -1,12 +1,13 @@ -#if UNITY_EDITOR -using System.Collections; +#if UNITY_EDITOR using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using UnityEditor; using UnityEditor.Animations; using UnityEngine; using VRC.SDK3.Avatars.Components; +using VRC.SDKBase; namespace Shell.Protector { @@ -14,126 +15,52 @@ public class AnimatorManager : ScriptableObject { Dictionary encryptedClip = new Dictionary(); - static string curve1 = @" - - curve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: -128 - inSlope: -1919.995 - outSlope: 59.999996 - tangentMode: 69 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 2.1333334 - value: 0 - inSlope: 59.999996 - outSlope: -1919.995 - tangentMode: 69 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - attribute: material._Key0 - path: Body - classID: 137 - script: {fileID: 0}"; - - static string curve2 = @" - - curve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 128 - inSlope: 423.3333 - outSlope: 60.000004 - tangentMode: 69 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - - serializedVersion: 3 - time: 2.1333334 - value: 256 - inSlope: 59.999996 - outSlope: 60 - tangentMode: 69 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - attribute: material._Key0 - path: Body - classID: 137 - script: {fileID: 0}"; - static string fallbackOffCurve = @" - - serializedVersion: 2 - curve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 0 - inSlope: 0 - outSlope: 0 - tangentMode: 136 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - attribute: material._fallback - path: Body - classID: 137 - script: {fileID: 0} - flags: 16"; - static string fallbackOnCurve = @" - - serializedVersion: 2 - curve: - serializedVersion: 2 - m_Curve: - - serializedVersion: 3 - time: 0 - value: 1 - inSlope: 0 - outSlope: 0 - tangentMode: 136 - weightedMode: 0 - inWeight: 0.33333334 - outWeight: 0.33333334 - m_PreInfinity: 2 - m_PostInfinity: 2 - m_RotationOrder: 4 - attribute: material._fallback - path: Body - classID: 137 - script: {fileID: 0} - flags: 16"; - public static AnimatorController DuplicateAnimator(RuntimeAnimatorController anim, string new_dir) + public static AnimatorController DuplicateAnimator(RuntimeAnimatorController anim, OutputPaths paths, AssetWriter writer) { - string dir = AssetDatabase.GetAssetPath(anim); - string output = Path.Combine(new_dir, anim.name + anim.GetInstanceID().ToString() + "_encrypted.anim"); - if (!AssetDatabase.CopyAsset(dir, output)) + return DuplicateAnimators(new[] { anim }, paths, writer)[0]; + } + + public static AnimatorController[] DuplicateAnimators(RuntimeAnimatorController[] animators, OutputPaths paths, AssetWriter writer) + { + var sourcePaths = new string[animators.Length]; + var targetPaths = new Dictionary(); + + for (int i = 0; i < animators.Length; ++i) { - Debug.LogErrorFormat("Failed to copy a animator: {0}", anim.name); + RuntimeAnimatorController animator = animators[i]; + if (animator == null) + continue; + + string sourcePath = AssetDatabase.GetAssetPath(animator); + sourcePaths[i] = sourcePath; + if (targetPaths.ContainsKey(sourcePath)) + continue; + + if (!writer.CopyAssetToFolder(sourcePath, paths.Folders.AnimGuid, paths.ControllerName(animator), out string targetPath)) + { + Debug.LogErrorFormat("Failed to copy a animator: {0}", animator.name); + continue; + } + + targetPaths.Add(sourcePath, targetPath); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); - return AssetDatabase.LoadAssetAtPath(output, typeof(RuntimeAnimatorController)) as AnimatorController; + + var duplicatedAnimators = new AnimatorController[animators.Length]; + for (int i = 0; i < sourcePaths.Length; ++i) + { + if (!string.IsNullOrEmpty(sourcePaths[i]) && targetPaths.TryGetValue(sourcePaths[i], out string targetPath)) + duplicatedAnimators[i] = AssetDatabase.LoadAssetAtPath(targetPath); + } + + return duplicatedAnimators; } - public static void CreateKeyAniamtions(string animation_dir, string new_dir, GameObject[] objs) + public static void CreateKeyAnimations(string animationDir, OutputPaths paths, AssetWriter writer, GameObject[] objs) { - string[] files = Directory.GetFiles(animation_dir); + string[] files = Directory.GetFiles(animationDir); foreach (string file in files) { string filename = Path.GetFileName(file); @@ -141,12 +68,12 @@ public static void CreateKeyAniamtions(string animation_dir, string new_dir, Gam continue; if (filename.Contains("dummy")) continue; - if (filename.Contains("FallbackOff")) + + if (!writer.CopyAssetToFolder(file, paths.Folders.AnimGuid, filename, out string path)) + continue; + AnimationClip clip = AssetDatabase.LoadAssetAtPath(path); + if (clip == null) continue; - - string path = Path.Combine(new_dir, filename); - AssetDatabase.CopyAsset(file, path); - string anim = File.ReadAllText(path); Match match = Regex.Match(filename, "key(\\d+).*?\\.anim"); int n = 0; @@ -155,199 +82,162 @@ public static void CreateKeyAniamtions(string animation_dir, string new_dir, Gam foreach (var obj in objs) { - string hr_path = obj.transform.GetHierarchyPath(); - hr_path = Regex.Replace(hr_path, ".*?/(.*)", "'$1'"); - - string curve; - - if (!filename.Contains("_")) - curve = Regex.Replace(curve1, "attribute: material._Key\\d+", "attribute: material._Key" + n); - else - curve = Regex.Replace(curve2, "attribute: material._Key\\d+", "attribute: material._Key" + n); - curve = Regex.Replace(curve, "path: Body", "path: " + hr_path); - //SkinnedMeshRender classID:137 - //MeshRenderer classID:23 - if (obj.GetComponent() == null) - curve = Regex.Replace(curve, "classID: 137", "classID: 23"); - - anim = Regex.Replace(anim, "m_FloatCurves:", "m_FloatCurves:" + curve); - anim = Regex.Replace(anim, "m_EditorCurves:", "m_EditorCurves:" + curve); + AddKeyCurve(clip, obj, n, filename.Contains("_")); } - File.WriteAllText(path, anim); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } - public static AnimationClip CreateFallbackAniamtions(string animationDir, string newDir, GameObject[] objs, bool bOff = true) + static void AddKeyCurve(AnimationClip clip, GameObject obj, int keyIndex, bool secondKeyClip) { - string animName = Path.GetFileName(animationDir); - string newPath = Path.Combine(newDir, animName); - - AssetDatabase.CopyAsset(animationDir, newPath); - - string anim = File.ReadAllText(newPath); - - foreach (var obj in objs) + var binding = new EditorCurveBinding { - if (obj.name == "Body") - continue; - - string hrPath = obj.transform.GetHierarchyPath(); - hrPath = Regex.Replace(hrPath, ".*?/(.*)", "'$1'"); + path = GetAnimationPath(obj.transform), + propertyName = "material." + ShaderProperties.KeyPrefix + keyIndex, + type = obj.GetComponent() == null ? typeof(MeshRenderer) : typeof(SkinnedMeshRenderer) + }; - string curve = Regex.Replace(bOff ? fallbackOffCurve : fallbackOnCurve, "path: Body", "path: " + hrPath); - //SkinnedMeshRender classID:137 - //MeshRenderer classID:23 - if (obj.GetComponent() == null) - curve = Regex.Replace(curve, "classID: 137", "classID: 23"); + AnimationUtility.SetEditorCurve(clip, binding, CreateKeyCurve(secondKeyClip)); + } - anim = Regex.Replace(anim, "m_FloatCurves:", "m_FloatCurves:" + curve); - anim = Regex.Replace(anim, "m_EditorCurves:", "m_EditorCurves:" + curve); + static string GetAnimationPath(Transform transform) + { + var names = new List(); + Transform current = transform; + while (current != null && current.parent != null) + { + names.Add(current.name); + current = current.parent; } - File.WriteAllText(newPath, anim); + names.Reverse(); + return string.Join("/", names); + } - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); + static AnimationCurve CreateKeyCurve(bool secondKeyClip) + { + if (!secondKeyClip) + { + return new AnimationCurve( + new Keyframe(0, -128, -1919.995f, 59.999996f), + new Keyframe(2.1333334f, 0, 59.999996f, -1919.995f) + ); + } - return AssetDatabase.LoadAssetAtPath(newPath); + return new AnimationCurve( + new Keyframe(0, 128, 423.3333f, 60.000004f), + new Keyframe(2.1333334f, 256, 59.999996f, 60) + ); } - private static BlendTree[] CreateKeyTree(string animation_dir, int key_length, float speed) + private static BlendTree[] CreateKeyTree(string animationDir, int keyLength, float speed) { - BlendTree[] tree = new BlendTree[key_length]; - int offset = 16 - key_length; - for (int i = 0; i < key_length; ++i) + BlendTree[] tree = new BlendTree[keyLength]; + int offset = 16 - keyLength; + for (int i = 0; i < keyLength; ++i) { - BlendTree tree_key = new BlendTree(); - tree_key.name = "key" + i; - tree_key.blendType = BlendTreeType.Direct; - tree_key.blendParameter = "key_weight"; - tree_key.blendType = BlendTreeType.Simple1D; - tree_key.blendParameter = "pkey" + i; - tree_key.useAutomaticThresholds = false; + BlendTree keyTree = new BlendTree(); + keyTree.name = "key" + i; + keyTree.blendType = BlendTreeType.Simple1D; + keyTree.blendParameter = ParameterManager.GetKeyName(i); + keyTree.useAutomaticThresholds = false; - Motion motion0 = AssetDatabase.LoadAssetAtPath(Path.Combine(animation_dir, "key" + (i + offset) + ".anim"), typeof(AnimationClip)) as AnimationClip; - Motion motion1 = AssetDatabase.LoadAssetAtPath(Path.Combine(animation_dir, "key" + (i + offset) + "_2.anim"), typeof(AnimationClip)) as AnimationClip; + Motion motion0 = AssetDatabase.LoadAssetAtPath(Path.Combine(animationDir, "key" + (i + offset) + ".anim"), typeof(AnimationClip)) as AnimationClip; + Motion motion1 = AssetDatabase.LoadAssetAtPath(Path.Combine(animationDir, "key" + (i + offset) + "_2.anim"), typeof(AnimationClip)) as AnimationClip; - tree_key.AddChild(motion0, -1); - tree_key.AddChild(motion1, 1); + keyTree.AddChild(motion0, -1); + keyTree.AddChild(motion1, 1); - ChildMotion[] motions = tree_key.children; + ChildMotion[] motions = keyTree.children; for (int j = 0; j < motions.Length; ++j) motions[j].timeScale = speed; - tree_key.children = motions; + keyTree.children = motions; - tree[i] = tree_key; + tree[i] = keyTree; } return tree; } - private static void AddTransition(AnimatorStateTransition transition, int key_length, int idx) + private static AnimatorConditionMode[] GetSwitchConditions(int switchCount, int index) { - transition.AddCondition(AnimatorConditionMode.IfNot, 0, "encrypt_lock"); - if (key_length == 4) - { - int n = 2; - AnimatorConditionMode[] mode = new AnimatorConditionMode[n]; - for (int i = 0; i < n; ++i) - mode[i] = AnimatorConditionMode.IfNot; - if ((idx & 0b0001) == 1) - mode[0] = AnimatorConditionMode.If; - if ((idx & 0b0010) == 2) - mode[1] = AnimatorConditionMode.If; - for (int i = 0; i < n; ++i) - transition.AddCondition(mode[i], 0, "encrypt_switch" + i); - } - else if (key_length == 8) - { - int n = 3; - AnimatorConditionMode[] mode = new AnimatorConditionMode[n]; - for (int i = 0; i < n; ++i) - mode[i] = AnimatorConditionMode.IfNot; - if ((idx & 0b0001) == 1) - mode[0] = AnimatorConditionMode.If; - if ((idx & 0b0010) == 2) - mode[1] = AnimatorConditionMode.If; - if ((idx & 0b0100) == 4) - mode[2] = AnimatorConditionMode.If; - for (int i = 0; i < n; ++i) - transition.AddCondition(mode[i], 0, "encrypt_switch" + i); - } - else + AnimatorConditionMode[] mode = new AnimatorConditionMode[switchCount]; + for (int i = 0; i < switchCount; ++i) + mode[i] = AnimatorConditionMode.IfNot; + for (int i = 0; i < switchCount; ++i) { - int n = 4; - AnimatorConditionMode[] mode = new AnimatorConditionMode[n]; - for (int i = 0; i < n; ++i) - mode[i] = AnimatorConditionMode.IfNot; - if ((idx & 0b0001) == 1) - mode[0] = AnimatorConditionMode.If; - if ((idx & 0b0010) == 2) - mode[1] = AnimatorConditionMode.If; - if ((idx & 0b0100) == 4) - mode[2] = AnimatorConditionMode.If; - if ((idx & 0b1000) == 8) - mode[3] = AnimatorConditionMode.If; - for (int i = 0; i < n; ++i) - transition.AddCondition(mode[i], 0, "encrypt_switch" + i); + if ((index & (1 << i)) != 0) + mode[i] = AnimatorConditionMode.If; } + return mode; } - public static void AddParameter(AnimatorController anim, int key_length, bool optimize) + + private static void AddTransition(AnimatorStateTransition transition, int keyLength, int syncSize, int idx) { - var paramters = anim.parameters; - for (int i = 0; i < paramters.Length; ++i) - { - string name = paramters[i].name; - if (name == "key_weight") - return; - } - anim.AddParameter(new AnimatorControllerParameter() { defaultFloat = 1.0f, name = "key_weight", type = AnimatorControllerParameterType.Float }); + bool bLegacy = syncSize == 1; + transition.AddCondition(AnimatorConditionMode.IfNot, 0, ParameterManager.GetSyncLockName(bLegacy)); + AnimatorConditionMode[] switchConditions = GetSwitchConditions(ShellProtector.GetRequiredSwitchCount(keyLength, syncSize), idx); + for (int i = 0; i < switchConditions.Length; ++i) + transition.AddCondition(switchConditions[i], 0, ParameterManager.GetSyncSwitchName(i, bLegacy)); + } - for (int i = 0; i < key_length; ++i) - anim.AddParameter("pkey" + i, AnimatorControllerParameterType.Float); + private static void AddParameters(AnimatorController anim, int keyLength, int syncSize) + { + bool bLegacy = syncSize == 1; + AddParameterIfMissing(anim, new AnimatorControllerParameter + { + defaultFloat = 1.0f, + name = "key_weight", + type = AnimatorControllerParameterType.Float + }); - if (optimize) + AddParameterIfMissing(anim, new AnimatorControllerParameter { - anim.AddParameter("pkey", AnimatorControllerParameterType.Float); - anim.AddParameter("encrypt_lock", AnimatorControllerParameterType.Bool); - int switch_count = 1; - switch (key_length) - { - case 4: - switch_count = 2; - break; - case 8: - switch_count = 3; - break; - case 12: - case 16: - switch_count = 4; - break; - default: - Debug.LogErrorFormat("AnimatorManager-AddParameter: key_length = {} is wrong!", key_length); - return; - } + defaultBool = false, + name = ParameterManager.GetIsLocalName(), + type = AnimatorControllerParameterType.Bool + }); + + for (var i = 0; i < keyLength; ++i) + AddParameterIfMissing(anim, ParameterManager.GetKeyName(i), AnimatorControllerParameterType.Float); - for (int i = 0; i < switch_count; ++i) - anim.AddParameter("encrypt_switch" + i, AnimatorControllerParameterType.Bool); + AddParameterIfMissing(anim, ParameterManager.GetSyncLockName(bLegacy), AnimatorControllerParameterType.Bool); + var switchCount = ShellProtector.GetRequiredSwitchCount(keyLength, syncSize); + + if (!bLegacy) + { + for (var i = 0; i < keyLength; ++i) + AddParameterIfMissing(anim, ParameterManager.GetSavedKeyName(i), AnimatorControllerParameterType.Float); } + for (var i = 0; i < syncSize; ++i) + AddParameterIfMissing(anim, ParameterManager.GetSyncedKeyName(i, bLegacy), AnimatorControllerParameterType.Float); + for (var i = 0; i < switchCount; ++i) + AddParameterIfMissing(anim, ParameterManager.GetSyncSwitchName(i, bLegacy), AnimatorControllerParameterType.Bool); } - public static void AddKeyLayer(AnimatorController anim, string animation_dir, int key_length, float speed = 10.0f, bool optimize = false) + + private static void AddParameterIfMissing(AnimatorController anim, string name, AnimatorControllerParameterType type) { - AddParameter(anim, key_length, optimize); + if (anim.parameters.Any(p => p.name == name)) + return; - if (optimize) - { - AddKeyLayerMultiplexing(anim, animation_dir, key_length, speed); + anim.AddParameter(name, type); + } + + private static void AddParameterIfMissing(AnimatorController anim, AnimatorControllerParameter parameter) + { + if (anim.parameters.Any(p => p.name == parameter.name)) return; - } - var layers = anim.layers; - foreach (var _layer in layers) - { - if (_layer.name == "ShellProtector") - return; - } + anim.AddParameter(parameter); + } + + public static void AddKeyLayer(AnimatorController anim, string animationDir, int keyLength, int syncSize, float speed) + { + if (anim.layers.Any(l => l.name == "ShellProtector")) return; + + AddParameters(anim, keyLength, syncSize); + + AddMuxLayer(anim, keyLength, syncSize, 0.15f, 0.1f, 1f); // 10hz + AddDemuxLayer(anim, keyLength, syncSize); AnimatorStateMachine stateMachine = new AnimatorStateMachine { @@ -360,110 +250,190 @@ public static void AddKeyLayer(AnimatorController anim, string animation_dir, in var layer = anim.layers[anim.layers.Length - 1]; var state = layer.stateMachine.AddState("keys"); - BlendTree tree_root = new BlendTree + BlendTree rootTree = new BlendTree { name = "key_root", blendType = BlendTreeType.Direct, blendParameter = "key_weight" }; - AssetDatabase.AddObjectToAsset(tree_root, anim); - state.motion = tree_root; + AssetDatabase.AddObjectToAsset(rootTree, anim); + state.motion = rootTree; - var key_tree = CreateKeyTree(animation_dir, key_length, speed); - for (int i = 0; i < key_length; ++i) + var keyTrees = CreateKeyTree(animationDir, keyLength, speed); + for (int i = 0; i < keyLength; ++i) { - tree_root.AddChild(key_tree[i]); - AssetDatabase.AddObjectToAsset(key_tree[i], anim); + rootTree.AddChild(keyTrees[i]); + AssetDatabase.AddObjectToAsset(keyTrees[i], anim); } - ChildMotion[] children = tree_root.children; + ChildMotion[] children = rootTree.children; for (int i = 0; i < children.Length; ++i) children[i].directBlendParameter = "key_weight"; - tree_root.children = children; + rootTree.children = children; } - public static void AddKeyLayerMultiplexing(AnimatorController anim, string animation_dir, int key_length, float speed = 10.0f) + + private static void AddSyncEnabledCondition(AnimatorStateTransition transition) { - var layers = anim.layers; - foreach (var _layer in layers) - { - if (_layer.name == "ShellProtectorDriver") - return; - } + transition.AddCondition(AnimatorConditionMode.If, 0, ParameterManager.GetIsLocalName()); + } - AnimatorStateMachine stateMachine = new AnimatorStateMachine + private static void AddMuxLayer(AnimatorController anim, int keyLength, int syncSize, float unlockDelay, float interval, float delay) + { + if (anim.layers.Any(l => l.name == "ShellProtectorMux")) + return; + + bool bLegacy = syncSize == 1; + + if (bLegacy) + return; + + var stateMachine = new AnimatorStateMachine { - name = anim.MakeUniqueLayerName("ShellProtectorDriver"), + name = anim.MakeUniqueLayerName("ShellProtectorMux"), hideFlags = HideFlags.HideInHierarchy }; AssetDatabase.AddObjectToAsset(stateMachine, anim); anim.AddLayer(new AnimatorControllerLayer { name = stateMachine.name, defaultWeight = 1.0f, stateMachine = stateMachine }); - var layer = anim.layers[anim.layers.Length - 1]; - var state = layer.stateMachine.AddState("empty"); + var idle = layer.stateMachine.AddState("Idle", new Vector3(0, 0)); + + var steps = keyLength / syncSize; + var syncStates = new AnimatorState[steps]; + var lockStates = new AnimatorState[steps]; + var unlockStates = new AnimatorState[steps]; + const int x = 250; + const int y = 80; + layer.stateMachine.entryPosition = new Vector3(-x, 0); + layer.stateMachine.exitPosition = new Vector3(x * 4, y * (steps - 1)); + for (var step = 0; step < steps; step++) + { + var lockState = layer.stateMachine.AddState("mux" + step + "_lock", new Vector3(x * 1, y * step)); + var syncState = layer.stateMachine.AddState("mux" + step + "_sync", new Vector3(x * 2, y * step)); + var unlockState = layer.stateMachine.AddState("mux" + step + "_unlock", new Vector3(x * 3, y * step)); - var transition = layer.stateMachine.AddAnyStateTransition(state); - transition.canTransitionToSelf = false; - transition.exitTime = 0; - transition.duration = 0; - transition.hasExitTime = false; - transition.AddCondition(AnimatorConditionMode.If, 0, "encrypt_lock"); + var lockToSync = lockState.AddTransition(syncState); + lockToSync.hasExitTime = false; + lockToSync.duration = 0; + AddSyncEnabledCondition(lockToSync); - for (int i = 0; i < key_length; ++i) - { - var key_state = layer.stateMachine.AddState("key" + i); + var syncToUnlock = syncState.AddTransition(unlockState); + syncToUnlock.hasExitTime = false; + syncToUnlock.duration = unlockDelay; + AddSyncEnabledCondition(syncToUnlock); - var behaviour = key_state.AddStateMachineBehaviour(); - var behaviour_param = new VRCAvatarParameterDriver.Parameter + if (step == 0) // first step + { + var transition = idle.AddTransition(lockState); + transition.hasExitTime = false; + transition.duration = delay; + AddSyncEnabledCondition(transition); + } + else { - type = VRC.SDKBase.VRC_AvatarParameterDriver.ChangeType.Copy, - name = "pkey" + i, - source = "pkey", - }; - behaviour.parameters.Add(behaviour_param); + if (step == steps - 1) + { + var exit = unlockState.AddExitTransition(); // last step exit + exit.hasExitTime = false; + exit.duration = 0; + AddSyncEnabledCondition(exit); + } + var previousUnlock = unlockStates[step - 1]; + var transition = previousUnlock.AddTransition(lockState); + transition.hasExitTime = false; + transition.duration = interval; + AddSyncEnabledCondition(transition); + } - transition = layer.stateMachine.AddAnyStateTransition(key_state); - transition.canTransitionToSelf = false; - transition.exitTime = 0; - transition.duration = 0; - transition.hasExitTime = false; - AddTransition(transition, key_length, i); - } + var lockDriver = lockState.AddStateMachineBehaviour(); + var syncDriver = syncState.AddStateMachineBehaviour(); + var unlockDriver = unlockState.AddStateMachineBehaviour(); - AddKeyLayer(anim, animation_dir, key_length, speed, false); + lockDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter + { + type = VRC_AvatarParameterDriver.ChangeType.Set, + name = ParameterManager.GetSyncLockName(bLegacy), + value = 1 + }); + + for (var i = 0; i < syncSize; i++) + { + syncDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter + { + type = VRC_AvatarParameterDriver.ChangeType.Copy, + name = ParameterManager.GetKeyName(step * syncSize + i), + source = ParameterManager.GetSavedKeyName(step * syncSize + i) + }); + } + + for (var i = 0; i < ShellProtector.GetRequiredSwitchCount(keyLength, syncSize); i++) + { + syncDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter + { + type = VRC_AvatarParameterDriver.ChangeType.Set, + name = ParameterManager.GetSyncSwitchName(i, bLegacy), + value = (step & (1 << i)) != 0 ? 1 : 0 + }); + } + + unlockDriver.parameters.Add(new VRC_AvatarParameterDriver.Parameter + { + type = VRC_AvatarParameterDriver.ChangeType.Set, + name = ParameterManager.GetSyncLockName(bLegacy), + value = 0 + }); + + syncStates[step] = syncState; + lockStates[step] = lockState; + unlockStates[step] = unlockState; + } } - public static void AddFallbackLayer(AnimatorController anim, AnimationClip fallbackOnAnimation, AnimationClip fallbackOffAnimation, float time = 3) + private static void AddDemuxLayer(AnimatorController anim, int keyLength, int syncSize) { - var layers = anim.layers; - foreach (var _layer in layers) - { - if (_layer.name == "ShellProtectorFallback") - return; - } + bool bLegacy = syncSize == 1; + if (anim.layers.Any(l => l.name == "ShellProtectorDemux")) return; - AnimatorStateMachine stateMachine = new AnimatorStateMachine + var stateMachine = new AnimatorStateMachine { - name = anim.MakeUniqueLayerName("ShellProtectorFallback"), + name = anim.MakeUniqueLayerName("ShellProtectorDemux"), hideFlags = HideFlags.HideInHierarchy }; AssetDatabase.AddObjectToAsset(stateMachine, anim); anim.AddLayer(new AnimatorControllerLayer { name = stateMachine.name, defaultWeight = 1.0f, stateMachine = stateMachine }); var layer = anim.layers[anim.layers.Length - 1]; - var defaultState = layer.stateMachine.AddState("default"); - defaultState.writeDefaultValues = true; - defaultState.motion = fallbackOnAnimation; - - var fallbackState = layer.stateMachine.AddState("fallbackState"); + var state = layer.stateMachine.AddState("empty"); - var transition = defaultState.AddTransition(fallbackState); + var transition = layer.stateMachine.AddAnyStateTransition(state); transition.canTransitionToSelf = false; - transition.exitTime = time; + transition.exitTime = 0; transition.duration = 0; - transition.hasExitTime = true; + transition.hasExitTime = false; + transition.AddCondition(AnimatorConditionMode.If, 0, ParameterManager.GetSyncLockName(bLegacy)); - fallbackState.motion = fallbackOffAnimation; + for (var i = 0; i < keyLength / syncSize; ++i) + { + var keyState = layer.stateMachine.AddState("key" + i); + + for (var j = 0; j < syncSize; j++) + { + var behaviour = keyState.AddStateMachineBehaviour(); + behaviour.parameters.Add(new VRCAvatarParameterDriver.Parameter + { + type = VRC_AvatarParameterDriver.ChangeType.Copy, + source = ParameterManager.GetSyncedKeyName(j, bLegacy), + name = ParameterManager.GetKeyName(i * syncSize + j) + }); + } + + transition = layer.stateMachine.AddAnyStateTransition(keyState); + transition.canTransitionToSelf = false; + transition.exitTime = 0; + transition.duration = 0; + transition.hasExitTime = false; + AddTransition(transition, keyLength, syncSize, i); + } } public static bool IsMaterialInClip(AnimationClip clip, Material originalMaterial) @@ -518,12 +488,12 @@ public static void ChangeMaterialInClip(AnimationClip clip, Material source, Mat } } - void SearchStateMachine(AnimatorStateMachine stateMachine, Material targetMaterial, Material encrypted, bool clone, string clonePath) + void SearchStateMachine(AnimatorStateMachine stateMachine, Material targetMaterial, Material encrypted, bool clone, OutputPaths paths, AssetWriter writer) { for (int i = 0; i < stateMachine.states.Length; i++) { ChildAnimatorState state = stateMachine.states[i]; - AnimationClip clip = SearchMotion(state.state.motion, targetMaterial, encrypted, clone, clonePath); + AnimationClip clip = SearchMotion(state.state.motion, targetMaterial, encrypted, clone, paths, writer); if (clip != null) { state.state.motion = clip; @@ -533,11 +503,11 @@ void SearchStateMachine(AnimatorStateMachine stateMachine, Material targetMateri foreach (ChildAnimatorStateMachine childStateMachine in stateMachine.stateMachines) { - SearchStateMachine(childStateMachine.stateMachine, targetMaterial, encrypted, clone, clonePath); + SearchStateMachine(childStateMachine.stateMachine, targetMaterial, encrypted, clone, paths, writer); } } - AnimationClip SearchMotion(Motion motion, Material targetMaterial, Material encrypted, bool clone, string clonePath) + AnimationClip SearchMotion(Motion motion, Material targetMaterial, Material encrypted, bool clone, OutputPaths paths, AssetWriter writer) { if (motion is AnimationClip clip) { @@ -547,7 +517,6 @@ AnimationClip SearchMotion(Motion motion, Material targetMaterial, Material encr if (clone) { string path = AssetDatabase.GetAssetPath(clip); - string copyPath = Path.Combine(clonePath, clip.name + "_encrypted.anim"); if (encryptedClip.ContainsKey(clip)) { @@ -555,7 +524,7 @@ AnimationClip SearchMotion(Motion motion, Material targetMaterial, Material encr } else { - if (!AssetDatabase.CopyAsset(path, copyPath)) + if (!writer.CopyAssetToFolder(path, paths.Folders.AnimGuid, paths.AnimationClipName(clip, "_encrypted"), out string copyPath)) { Debug.LogError("Copy error: " + copyPath); return null; @@ -576,7 +545,7 @@ AnimationClip SearchMotion(Motion motion, Material targetMaterial, Material encr for (int i = 0; i < blendTree.children.Length; ++i) { ChildMotion childMotion = blendTree.children[i]; - AnimationClip result = SearchMotion(childMotion.motion, targetMaterial, encrypted, clone, clonePath); + AnimationClip result = SearchMotion(childMotion.motion, targetMaterial, encrypted, clone, paths, writer); if (result != null) { childMotion.motion = result; @@ -590,7 +559,7 @@ AnimationClip SearchMotion(Motion motion, Material targetMaterial, Material encr return null; } - public void ChangeAnimationMaterial(AnimatorController anim, Material original, Material encrypted, bool clone, string clonePath) + public void ChangeAnimationMaterial(AnimatorController anim, Material original, Material encrypted, bool clone, OutputPaths paths, AssetWriter writer) { if (anim == null || original == null) return; @@ -605,11 +574,11 @@ public void ChangeAnimationMaterial(AnimatorController anim, Material original, var stateMachine = layer.stateMachine; if (stateMachine == null) continue; - SearchStateMachine(stateMachine, original, encrypted, clone, clonePath); + SearchStateMachine(stateMachine, original, encrypted, clone, paths, writer); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/AssetManager.cs b/Runtime/Scripts/AssetManager.cs index c08a1d8..c678512 100644 --- a/Runtime/Scripts/AssetManager.cs +++ b/Runtime/Scripts/AssetManager.cs @@ -1,33 +1,35 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; using System.Linq; +using System.Reflection; namespace Shell.Protector { public class AssetManager { - static AssetManager instance; - readonly Dictionary support_version = new Dictionary(); + static AssetManager _instance; + readonly Dictionary _supportedVersions = new Dictionary(); - static public AssetManager GetInstance() + public static AssetManager GetInstance() { - if(instance == null) - instance = new AssetManager(); - return instance; + if (_instance == null) + _instance = new AssetManager(); + return _instance; } AssetManager() { - support_version.Add("Poiyomi 7.3", 73); - support_version.Add("Poiyomi 8.0", 80); - support_version.Add("Poiyomi 8.1", 81); - support_version.Add("Poiyomi 8.2", 82); - support_version.Add("Poiyomi 9.0", 90); - support_version.Add("Poiyomi 9.1", 91); - support_version.Add("Poiyomi 9.2", 92); - support_version.Add("lilToon", 0); + _supportedVersions.Add("Poiyomi 8.0", 80); + _supportedVersions.Add("Poiyomi 8.1", 81); + _supportedVersions.Add("Poiyomi 8.2", 82); + _supportedVersions.Add("Poiyomi 9.0", 90); + _supportedVersions.Add("Poiyomi 9.1", 91); + _supportedVersions.Add("Poiyomi 9.2", 92); + _supportedVersions.Add("Poiyomi 9.3", 93); + _supportedVersions.Add("Poiyomi 10.0", 100); + _supportedVersions.Add("lilToon", 0); } public bool IsPoiyomi(Shader shader) { @@ -37,41 +39,43 @@ public bool IsPoiyomi(Shader shader) return true; return false; } - public bool IslilToon(Shader shader) + public bool IsLilToon(Shader shader) { if (shader.name.Contains("lilToon")) return true; return false; } - public bool IsLockPoiyomi(Shader shader) + public bool IsLockPoiyomi(Material mat) { - if (shader.name.Contains("Locked")) - return true; - return false; + return mat.shader.name.StartsWith("Hidden/") && mat.GetTag("OriginalShader", false, "") != ""; } public int GetShaderType(Shader shader) { - foreach (var version in support_version) + foreach (var version in _supportedVersions) { if (shader.name.Contains(version.Key)) - return support_version[version.Key]; + return _supportedVersions[version.Key]; } int poiyomiLabel = shader.FindPropertyIndex("shader_master_label"); if (poiyomiLabel != -1) { var str = shader.GetPropertyDescription(poiyomiLabel); + if (str.Contains("Poiyomi 10.0")) + return _supportedVersions["Poiyomi 10.0"]; + if (str.Contains("Poiyomi 9.3")) + return _supportedVersions["Poiyomi 9.3"]; if (str.Contains("Poiyomi 9.2")) - return support_version["Poiyomi 9.2"]; - else if (str.Contains("Poiyomi 9.1")) - return support_version["Poiyomi 9.1"]; - else if (str.Contains("Poiyomi 9.0")) - return support_version["Poiyomi 9.0"]; - else if(str.Contains("Poiymoi 8.0")) - return support_version["Poiyomi 8.0"]; - else if(str.Contains("Poiyomi 8.1")) - return support_version["Poiyomi 8.1"]; - else if (str.Contains("Poiyomi 8.2")) - return support_version["Poiyomi 8.2"]; + return _supportedVersions["Poiyomi 9.2"]; + if (str.Contains("Poiyomi 9.1")) + return _supportedVersions["Poiyomi 9.1"]; + if (str.Contains("Poiyomi 9.0")) + return _supportedVersions["Poiyomi 9.0"]; + if(str.Contains("Poiymoi 8.0")) + return _supportedVersions["Poiyomi 8.0"]; + if(str.Contains("Poiyomi 8.1")) + return _supportedVersions["Poiyomi 8.1"]; + if (str.Contains("Poiyomi 8.2")) + return _supportedVersions["Poiyomi 8.2"]; } return -1; } @@ -85,16 +89,18 @@ public bool IsSupportShader(Shader shader) public List CheckShader() { + Debug.Log("Checking Shader..."); string[] guids = AssetDatabase.FindAssets("lilConstants"); string symbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup); - string symbols_original = string.Copy(symbols); + string originalSymbols = string.Copy(symbols); symbols = symbols.Replace(";LILTOON", ""); + symbols = symbols.Replace(";POIYOMI91", ""); symbols = symbols.Replace(";POIYOMI", ""); - List return_shader = new List(); + List availableShaders = new List(); if(guids.Length > 0) { - return_shader.Add("lilToon"); + availableShaders.Add("lilToon"); symbols += ";LILTOON"; } guids = AssetDatabase.FindAssets("ThryEditor"); @@ -103,24 +109,22 @@ public List CheckShader() if (ClassExists("Thry.ThryEditor.ShaderOptimizer")) { symbols += ";POIYOMI91"; - return_shader.Add("Poiyomi9.1"); + availableShaders.Add("Poiyomi9.1>"); } else // < 9.1 { symbols += ";POIYOMI"; - return_shader.Add("Poiyomi"); + availableShaders.Add("Poiyomi"); } } - if (symbols_original.Contains(";LILTOON") != symbols.Contains(";LILTOON")) + if (originalSymbols.Contains(";LILTOON") != symbols.Contains(";LILTOON") || + originalSymbols.Contains(";POIYOMI") != symbols.Contains(";POIYOMI")) { - if (symbols_original.Contains(";POIYOMI") != symbols.Contains(";POIYOMI")) - { - PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, symbols); - } + PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, symbols); } - return return_shader; + return availableShaders; } public static bool NamespaceExists(string namespaceName) { @@ -138,20 +142,20 @@ public static bool ClassExists(string className) public void CheckModular() { string symbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup); - string symbols_original = string.Copy(symbols); + string originalSymbols = string.Copy(symbols); symbols = symbols.Replace(";MODULAR", ""); if (!NamespaceExists("nadena.dev.ndmf")) { Debug.Log("ShellProtector: Can't find Modular!"); - if (symbols != symbols_original) + if (symbols != originalSymbols) PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, symbols); return; } Debug.Log("ShellProtector: Find Modular!"); symbols += ";MODULAR"; - if (symbols_original.Contains(";MODULAR") != symbols.Contains(";MODULAR")) + if (originalSymbols.Contains(";MODULAR") != symbols.Contains(";MODULAR")) { PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, symbols); } @@ -168,6 +172,56 @@ public void ResetDefine() PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, symbols); } + + public bool LockShader(Material mat) + { + if (IsLockPoiyomi(mat)) + return true; + + bool isOldOptimizer = false; + + Type optimizer = Type.GetType("Thry.ThryEditor.ShaderOptimizer, ThryAssemblyDefinition"); + if (optimizer == null) + { + isOldOptimizer = true; + optimizer = Type.GetType("Thry.ShaderOptimizer, ThryAssemblyDefinition"); + } + + if (optimizer == null) + { + Debug.LogError("Not found the ShaderOptimizer!"); + return false; + } + if (!isOldOptimizer) + { + MethodInfo lockFn = optimizer.GetMethod("LockMaterials"); + if (lockFn == null) + { + Debug.LogError("Not found LockMaterials()"); + return false; + } + object[] param = + { + new[] { mat }, 0 + }; + lockFn.Invoke(null, param); + } + else + { + MethodInfo lockFn = optimizer.GetMethod("SetLockedForAllMaterials"); + if (lockFn == null) + { + Debug.LogError("Not found SetLockedForAllMaterials()"); + return false; + } + object[] param = + { + new[] { mat }, 1, true, false, true, null + }; + lockFn.Invoke(null, param); + } + return true; + } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/CompileErrorListener.cs b/Runtime/Scripts/CompileErrorListener.cs index b20bc34..e52a585 100644 --- a/Runtime/Scripts/CompileErrorListener.cs +++ b/Runtime/Scripts/CompileErrorListener.cs @@ -1,6 +1,4 @@ #if UNITY_EDITOR -using System.Collections; -using System.Collections.Generic; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; @@ -10,25 +8,53 @@ namespace Shell.Protector [InitializeOnLoad] public class CompileErrorListener { + private static int retryLimit = 3; + private static int retryCount = 0; + static CompileErrorListener() { - CompilationPipeline.assemblyCompilationFinished += (string s, CompilerMessage[] message) => + CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished; + } + + private static void OnAssemblyCompilationFinished(string assembly, CompilerMessage[] messages) + { + bool foundTargetError = false; + + foreach (var m in messages) + { + if (m.type != CompilerMessageType.Error) + continue; + + if ( + m.message.Contains("The type or namespace name 'lilToonInspector' could not be found") || + m.message.Contains("The type or namespace name 'Thry' could not be found") || + m.message.Contains("The name 'ShaderOptimizer' does not exist in the current context") + ) + { + foundTargetError = true; + break; + } + } + + if (foundTargetError) { - foreach(var m in message) + retryCount++; + + if (retryCount <= retryLimit) { - if(m.type == CompilerMessageType.Error) - { - if(!m.message.Contains("The type or namespace name 'lilToonInspector' could not be found")) - continue; - if (!m.message.Contains("The type or namespace name 'Thry' could not be found")) - continue; - AssetManager.GetInstance().ResetDefine(); - break; - } + Debug.LogErrorFormat("Shader check error -> ResetDefine {0}/{0}", retryCount, retryLimit); + AssetManager.GetInstance().ResetDefine(); } - - }; + else + { + Debug.LogError("Failed ResetDefine"); + } + } + else + { + retryCount = 0; + } } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/Constants.cs b/Runtime/Scripts/Constants.cs new file mode 100644 index 0000000..342e56a --- /dev/null +++ b/Runtime/Scripts/Constants.cs @@ -0,0 +1,49 @@ +namespace Shell.Protector +{ + public enum ShellProtectorAlgorithm + { + XXTEA = 0, + Chacha = 1 + } + + public enum ShellProtectorTextureFilter + { + Point = 0, + Bilinear = 1 + } + + public enum ShellProtectorFallback + { + White = 0, + Black = 1, + Size4 = 2, + Size8 = 3, + Size16 = 4, + Size32 = 5, + Size64 = 6, + Size128 = 7 + } + + public static class ShaderProperties + { + public const string KeywordPrefix = "_SHELL_PROTECTOR_"; + public const string XXTEAKeyword = "_SHELL_PROTECTOR_XXTEA"; + public const string ChachaKeyword = "_SHELL_PROTECTOR_CHACHA"; + public const string Format0Keyword = "_SHELL_PROTECTOR_FORMAT0"; + public const string Format1Keyword = "_SHELL_PROTECTOR_FORMAT1"; + public const string RimLightKeyword = "_SHELL_PROTECTOR_RIMLIGHT"; + + public const string MipTexture = "_MipTex"; + public const string EncryptTexture0 = "_EncryptTex0"; + public const string EncryptTexture1 = "_EncryptTex1"; + public const string WidthOffset = "_Woffset"; + public const string HeightOffset = "_Hoffset"; + public const string Nonce0 = "_Nonce0"; + public const string Nonce1 = "_Nonce1"; + public const string Nonce2 = "_Nonce2"; + public const string Rounds = "_Rounds"; + public const string PasswordHash = "_PasswordHash"; + public const string HashMagic = "_HashMagic"; + public const string KeyPrefix = "_Key"; + } +} diff --git a/Runtime/Scripts/Constants.cs.meta b/Runtime/Scripts/Constants.cs.meta new file mode 100644 index 0000000..6088b1f --- /dev/null +++ b/Runtime/Scripts/Constants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8020f6b777ad47f6a2bc684c00a1c208 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/EncryptTexture.cs b/Runtime/Scripts/EncryptTexture.cs deleted file mode 100644 index 88251de..0000000 --- a/Runtime/Scripts/EncryptTexture.cs +++ /dev/null @@ -1,538 +0,0 @@ -#if UNITY_EDITOR -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using System; -using System.Linq; -using UnityEditor; - -namespace Shell.Protector -{ - public class EncryptTexture - { - public static int GetCanMipmapLevel(int w, int h) - { - int w_level, h_level; - if (w < 1 || h <= 1) - return 0; - w_level = (int)Mathf.Log(w, 2); - h_level = (int)Mathf.Log(h, 2); - return Math.Max(w_level, h_level); - } - public int GetDXT1Length(int w, int h, int m) - { - int len = 0; - for (int i = 0; i < m + 1; ++i) - { - len += w * h / 16 * 8; - w /= 2; - h /= 2; - } - return len; - } - public static bool HasAlpha(Texture2D texture) - { - Color32[] pixels = texture.GetPixels32(); - for (int i = 0; i < pixels.Length; i++) - { - if (pixels[i].a != 255) - { - return true; - } - } - return false; - } - private int GetArrayIdxInBlock(int block, int n, int w) - { - int wblock = w / 4; - int x = (block % wblock); - int y = block / wblock; - - int pivot = x * 4 + y * w * 4; - - return pivot + n / 4 * w + n % 4; - } - /// - /// 밉 레벨에 해당하는 DXT데이터 배열을 가져오는 함수 - /// - private byte[] GetArrayDXT(byte[] data, int texture_width, int texture_height, bool dxt5, int miplv) - { - int start = 0; - int end = 0; - - for (int i = 0; i <= miplv; ++i) - { - start += end; - int w = texture_width / (int)(Mathf.Pow(2, i)); - int h = texture_height / (int)(Mathf.Pow(2, i)); - int block_count = (w / 4) * (h / 4); - int len = block_count * 8; - if (dxt5) - len *= 2; - end = len; - } - - var segment = new ArraySegment(data, start, end); - return segment.ToArray(); - } - public Texture2D GenerateRefMipmap(int width, int height, bool small = false) - { - int mip_lv = GetCanMipmapLevel(width, height); - Debug.LogFormat("mip {0}, {1} : {2}", width, height, mip_lv); - - Texture2D mip = new Texture2D(width, (small == false) ? height : 1, TextureFormat.RGB24, mip_lv, true); - mip.filterMode = FilterMode.Bilinear; - mip.anisoLevel = (small == false) ? 1 : 0; - - for (int m = 0; m < mip.mipmapCount; ++m) - { - Color32[] pixels_mip = mip.GetPixels32(m); - for (int i = 0; i < pixels_mip.Length; ++i) - { - pixels_mip[i].r = (byte)(m * 10); - pixels_mip[i].g = 0; - pixels_mip[i].b = 0; - } - mip.SetPixels32(pixels_mip, m); - } - if (small == false) - mip.Compress(false); - return mip; - } - - public Texture2D GenerateFallback(Texture2D original, int size = 32) - { - if(original.width < 128 || original.height < 128) - { - return null; - } - TextureFormat format = TextureFormat.RGB24; - bool hasAlpha = HasAlpha(original); - if (hasAlpha) - format = TextureFormat.RGBA32; - - RenderTexture renderTexture = new RenderTexture(size, size, 0); - RenderTexture.active = renderTexture; - - Graphics.Blit(original, renderTexture); - - Texture2D resizedTexture = new Texture2D(size, size, format, true); - resizedTexture.ReadPixels(new Rect(0, 0, size, size), 0, 0); - resizedTexture.Apply(); - - resizedTexture.filterMode = FilterMode.Point; - resizedTexture.anisoLevel = 0; - resizedTexture.Compress(false); - if(hasAlpha) - resizedTexture.alphaIsTransparency = true; - - RenderTexture.active = null; - - return resizedTexture; - } - - private Texture2D[] EncryptDXT1(Texture2D texture, byte[] key, IEncryptor encryptor) - { - Texture2D[] result = new Texture2D[2]; - uint[] key_uint = new uint[4]; - key_uint[0] = (uint)(key[0] | (key[1] << 8) | (key[2] << 16) | (key[3] << 24)); - key_uint[1] = (uint)(key[4] | (key[5] << 8) | (key[6] << 16) | (key[7] << 24)); - key_uint[2] = (uint)(key[8] | (key[9] << 8) | (key[10] << 16) | (key[11] << 24)); - key_uint[3] = 0; - - int mip_lv = GetCanMipmapLevel(texture.width / 4, texture.height / 4); - Texture2D dxt1 = texture; - if (texture.format == TextureFormat.DXT1Crunched) - { - Debug.LogWarningFormat("{0} is the crunch compression format. There may be degradation in image quality.", texture.name); - dxt1 = new Texture2D(texture.width, texture.height, TextureFormat.RGB24, mip_lv, true); - for (int m = 0; m <= mip_lv; ++m) - { - if (m != 0 && m == mip_lv) - break; - dxt1.SetPixels32(texture.GetPixels32(m), m); - dxt1.Apply(); - } - dxt1.Compress(false); - } - if (mip_lv != 0) - { - result[0] = new Texture2D(dxt1.width, dxt1.height, TextureFormat.DXT1, mip_lv, true); - result[1] = new Texture2D(dxt1.width / 4, dxt1.height / 4, TextureFormat.RGBA32, mip_lv, true); - } - else - { - result[0] = new Texture2D(dxt1.width, dxt1.height, TextureFormat.DXT1, false, true); - result[1] = new Texture2D(dxt1.width / 4, dxt1.height / 4, TextureFormat.RGBA32, false, true); - } - result[1].filterMode = FilterMode.Point; - result[1].anisoLevel = 0; - //Note: DXT1 per 4x4 block is 64bit - var raw_data = dxt1.GetRawTextureData(); - int lenidx = 0; - - for (int m = 0; m <= mip_lv; ++m) - { - if (m != 0 && m == mip_lv) - break; - var tex_data = GetArrayDXT(raw_data, dxt1.width, dxt1.height, false, m); - var pixel = result[1].GetPixels32(m); - - for (int i = 0; i < tex_data.Length; i += 16) //reference color texture - { - key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); - key_uint[3] ^= (uint)(i / 8); //8 bytes(1block) are same id. - - uint[] data = new uint[2]; - data[0] = (uint)(tex_data[i + 0] + (tex_data[i + 1] << 8) + (tex_data[i + 2] << 16) + (tex_data[i + 3] << 24)); - data[1] = (uint)(tex_data[(i + 8) + 0] + (tex_data[(i + 8) + 1] << 8) + (tex_data[(i + 8) + 2] << 16) + (tex_data[(i + 8) + 3] << 24)); - - uint[] data_enc = encryptor.Encrypt(data, key_uint); - - for (int j = 0; j < 2; ++j) - { - pixel[i / 8 + j].r = (byte)((data_enc[j] & 0x000000FF) >> 0); - pixel[i / 8 + j].g = (byte)((data_enc[j] & 0x0000FF00) >> 8); - pixel[i / 8 + j].b = (byte)((data_enc[j] & 0x00FF0000) >> 16); - pixel[i / 8 + j].a = (byte)((data_enc[j] & 0xFF000000) >> 24); - } - } - for (int i = 0; i < tex_data.Length; i += 8) - { - tex_data[i + 0] = 255; - tex_data[i + 1] = 255; - tex_data[i + 2] = 0; - tex_data[i + 3] = 0; - } - for (int i = 0; i < tex_data.Length; ++i) - { - raw_data[i + lenidx] = tex_data[i]; - } - lenidx += tex_data.Length; - result[1].SetPixels32(pixel, m); - } - result[0].LoadRawTextureData(raw_data); - - return result; - } - private Texture2D[] EncryptDXT5(Texture2D tex, byte[] key, IEncryptor encryptor) - { - Texture2D[] result = new Texture2D[2]; - uint[] key_uint = new uint[4]; - key_uint[0] = (uint)(key[0] | (key[1] << 8) | (key[2] << 16) | (key[3] << 24)); - key_uint[1] = (uint)(key[4] | (key[5] << 8) | (key[6] << 16) | (key[7] << 24)); - key_uint[2] = (uint)(key[8] | (key[9] << 8) | (key[10] << 16) | (key[11] << 24)); - key_uint[3] = 0; - - int mip_lv = GetCanMipmapLevel(tex.width / 4, tex.height / 4); - Texture2D dxt5 = tex; - if (tex.format == TextureFormat.DXT5Crunched) - { - Debug.LogWarningFormat("{0} is the crunch compression format. There may be degradation in image quality.", tex.name); - dxt5 = new Texture2D(tex.width, tex.height, TextureFormat.RGBA32, mip_lv, true); - for (int m = 0; m < mip_lv; ++m) - { - if (m != 0 && m == mip_lv) - break; - dxt5.SetPixels32(tex.GetPixels32(m), m); - dxt5.Apply(); - } - - dxt5.Compress(true); - } - - if (mip_lv != 0) - { - result[0] = new Texture2D(tex.width, tex.height, TextureFormat.DXT5, mip_lv, true); - result[1] = new Texture2D(tex.width / 4, tex.height / 4, TextureFormat.RGBA32, mip_lv, true); - } - else - { - result[0] = new Texture2D(tex.width, tex.height, TextureFormat.DXT5, false, true); - result[1] = new Texture2D(tex.width / 4, tex.height / 4, TextureFormat.RGBA32, false, true); - } - result[0].alphaIsTransparency = true; - result[1].filterMode = FilterMode.Point; - result[1].anisoLevel = 0; - var raw_data = dxt5.GetRawTextureData(); - int lenidx = 0; - for (int m = 0; m <= mip_lv; ++m) - { - if (m != 0 && m == mip_lv) - break; - var tex_data = GetArrayDXT(raw_data, tex.width, tex.height, true, m); - var pixel = result[1].GetPixels32(m); - - for (int i = 0; i < tex_data.Length; i += 32) //reference color texture - { - key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); - key_uint[3] ^= (uint)(i / 16); - - uint[] data = new uint[2]; - data[0] = (uint)(tex_data[i + 8] + (tex_data[i + 9] << 8) + (tex_data[i + 10] << 16) + (tex_data[i + 11] << 24)); - data[1] = (uint)(tex_data[i + 16 + 8] + (tex_data[i + 16 + 9] << 8) + (tex_data[i + 16 + 10] << 16) + (tex_data[i + 16 + 11] << 24)); - - uint[] data_enc = encryptor.Encrypt(data, key_uint); - - for (int j = 0; j < 2; ++j) - { - pixel[i / 16 + j].r = (byte)((data_enc[j] & 0x000000FF) >> 0); - pixel[i / 16 + j].g = (byte)((data_enc[j] & 0x0000FF00) >> 8); - pixel[i / 16 + j].b = (byte)((data_enc[j] & 0x00FF0000) >> 16); - pixel[i / 16 + j].a = (byte)((data_enc[j] & 0xFF000000) >> 24); - } - } - for (int i = 0; i < tex_data.Length; i += 16) - { - tex_data[i + 8] = 255; - tex_data[i + 9] = 255; - tex_data[i + 10] = 0; - tex_data[i + 11] = 0; - } - for (int i = 0; i < tex_data.Length; ++i) - { - raw_data[i + lenidx] = tex_data[i]; - } - lenidx += tex_data.Length; - result[1].SetPixels32(pixel, m); - } - result[0].LoadRawTextureData(raw_data); - - return result; - } - private Texture2D[] EncryptRGB24(Texture2D tex, byte[] key, IEncryptor encryptor) - { - Texture2D[] result = new Texture2D[2]; - uint[] key_uint = new uint[4]; - key_uint[0] = (uint)(key[0] | (key[1] << 8) | (key[2] << 16) | (key[3] << 24)); - key_uint[1] = (uint)(key[4] | (key[5] << 8) | (key[6] << 16) | (key[7] << 24)); - key_uint[2] = (uint)(key[8] | (key[9] << 8) | (key[10] << 16) | (key[11] << 24)); - key_uint[3] = 0; - - int mip_lv = GetCanMipmapLevel(tex.width, tex.height); - result[0] = new Texture2D(tex.width, tex.height, TextureFormat.RGB24, mip_lv - 2, true); //mip_lv-2 is blur trick (like the box filter) - for (int m = 0; m < result[0].mipmapCount; ++m) - { - Color32[] pixels = tex.GetPixels32(m); - - for (int i = 0; i < pixels.Length; i += 4) - { - key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); - key_uint[3] ^= (uint)i; - - uint[] data = new uint[3]; - data[0] = (uint)(pixels[i + 0].r + (pixels[i + 0].g << 8) + (pixels[i + 0].b << 16) + (pixels[i + 1].r << 24)); - data[1] = (uint)(pixels[i + 1].g + (pixels[i + 1].b << 8) + (pixels[i + 2].r << 16) + (pixels[i + 2].g << 24)); - data[2] = (uint)(pixels[i + 2].b + (pixels[i + 3].r << 8) + (pixels[i + 3].g << 16) + (pixels[i + 3].b << 24)); - - uint[] data_enc = encryptor.Encrypt(data, key_uint); - - pixels[i + 0].r = (byte)((data_enc[0] & 0x000000FF) >> 0); - pixels[i + 0].g = (byte)((data_enc[0] & 0x0000FF00) >> 8); - pixels[i + 0].b = (byte)((data_enc[0] & 0x00FF0000) >> 16); - pixels[i + 1].r = (byte)((data_enc[0] & 0xFF000000) >> 24); - pixels[i + 1].g = (byte)((data_enc[1] & 0x000000FF) >> 0); - pixels[i + 1].b = (byte)((data_enc[1] & 0x0000FF00) >> 8); - pixels[i + 2].r = (byte)((data_enc[1] & 0x00FF0000) >> 16); - pixels[i + 2].g = (byte)((data_enc[1] & 0xFF000000) >> 24); - pixels[i + 2].b = (byte)((data_enc[2] & 0x000000FF) >> 0); - pixels[i + 3].r = (byte)((data_enc[2] & 0x0000FF00) >> 8); - pixels[i + 3].g = (byte)((data_enc[2] & 0x00FF0000) >> 16); - pixels[i + 3].b = (byte)((data_enc[2] & 0xFF000000) >> 24); - } - result[0].SetPixels32(pixels, m); - } - return result; - } - - private Texture2D[] EncryptRGBA32(Texture2D tex, byte[] key, IEncryptor encryptor) - { - Texture2D[] result = new Texture2D[2]; - uint[] key_uint = new uint[4]; - key_uint[0] = (uint)(key[0] | (key[1] << 8) | (key[2] << 16) | (key[3] << 24)); - key_uint[1] = (uint)(key[4] | (key[5] << 8) | (key[6] << 16) | (key[7] << 24)); - key_uint[2] = (uint)(key[8] | (key[9] << 8) | (key[10] << 16) | (key[11] << 24)); - key_uint[3] = 0; - - int mip_lv = GetCanMipmapLevel(tex.width, tex.height); - result[0] = new Texture2D(tex.width, tex.height, TextureFormat.RGBA32, mip_lv - 2, true); //mip_lv-2 is blur trick (like the box filter) - for (int m = 0; m < result[0].mipmapCount; ++m) - { - Color32[] pixels = tex.GetPixels32(m); - - for (int i = 0; i < pixels.Length; i += 2) - { - key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); - key_uint[3] ^= (uint)i; - - uint[] data = new uint[2]; - data[0] = (uint)(pixels[i + 0].r + (pixels[i + 0].g << 8) + (pixels[i + 0].b << 16) + (pixels[i + 0].a << 24)); - data[1] = (uint)(pixels[i + 1].r + (pixels[i + 1].g << 8) + (pixels[i + 1].b << 16) + (pixels[i + 1].a << 24)); - - uint[] data_enc = encryptor.Encrypt(data, key_uint); - - pixels[i + 0].r = (byte)((data_enc[0] & 0x000000FF) >> 0); - pixels[i + 0].g = (byte)((data_enc[0] & 0x0000FF00) >> 8); - pixels[i + 0].b = (byte)((data_enc[0] & 0x00FF0000) >> 16); - pixels[i + 0].a = (byte)((data_enc[0] & 0xFF000000) >> 24); - pixels[i + 1].r = (byte)((data_enc[1] & 0x000000FF) >> 0); - pixels[i + 1].g = (byte)((data_enc[1] & 0x0000FF00) >> 8); - pixels[i + 1].b = (byte)((data_enc[1] & 0x00FF0000) >> 16); - pixels[i + 1].a = (byte)((data_enc[1] & 0xFF000000) >> 24); - } - result[0].SetPixels32(pixels, m); - } - - return result; - } - - public Texture2D[] TextureEncrypt(Texture2D texture, byte[] key, IEncryptor encryptor) - { - Texture2D tex = texture; - if (tex.width % 2 != 0 && tex.height % 2 != 0) - { - Debug.LogErrorFormat("{0} : The texture size must be a multiple of 2!", texture.name); - return null; - } - - Texture2D[] result = new Texture2D[2]; - - if (texture.format == TextureFormat.DXT1 || - texture.format == TextureFormat.DXT1Crunched || - texture.format == TextureFormat.DXT5 || - texture.format == TextureFormat.DXT5Crunched) - { - if (tex.width < 8) - { - Debug.LogErrorFormat("{0} : The texture width must be >= 8px", texture.name); - return null; - } - if (tex.height < 4) - { - Debug.LogErrorFormat("{0} : The texture height must be >= 4px", texture.name); - return null; - } - } - - if (texture.format == TextureFormat.DXT1 || texture.format == TextureFormat.DXT1Crunched) - { - result = EncryptDXT1(texture, key, encryptor); - } - else if (texture.format == TextureFormat.DXT5 || texture.format == TextureFormat.DXT5Crunched) - { - result = EncryptDXT5(texture, key, encryptor); - } - else if (tex.format == TextureFormat.RGB24) - { - result = EncryptRGB24(tex, key, encryptor); - } - else if (tex.format == TextureFormat.RGBA32) - { - result = EncryptRGBA32(tex, key, encryptor); - } - else - { - Debug.LogErrorFormat("{0} is not supported texture format! supported type:DXT1, DXT5, RGB, RGBA", tex.name); - result[0] = null; - } - if (result[0]) - { - result[0].filterMode = FilterMode.Point; - result[0].anisoLevel = 0; - } - return result; - } - /*public Texture2D TextureDecryptXXTEA(Texture2D texture, byte[] key) - { - Texture2D tex = texture; - - if (tex.width % 2 != 0 && tex.height % 2 != 0) - { - Debug.LogErrorFormat("{0} : The texture size must be a multiple of 2!", texture.name); - return null; - } - - int mip_lv = GetCanMipmapLevel(tex.width, tex.height); - - Texture2D tmp; - - XXTEA xxtea = new XXTEA(); - - uint[] key_uint = new uint[4]; - key_uint[0] = (uint)(key[0] | (key[1] << 8) | (key[2] << 16) | (key[3] << 24)); - key_uint[1] = (uint)(key[4] | (key[5] << 8) | (key[6] << 16) | (key[7] << 24)); - key_uint[2] = (uint)(key[8] | (key[9] << 8) | (key[10] << 16) | (key[11] << 24)); - key_uint[3] = 0; - if (HasAlpha(tex)) - { - tmp = new Texture2D(tex.width, tex.height, TextureFormat.RGB24, mip_lv - 2, true); //mip_lv-2 is blur trick (like the box filter) - for (int m = 0; m < tmp.mipmapCount; ++m) - { - Color32[] pixels = tex.GetPixels32(m); - - for (int i = 0; i < pixels.Length; i += 4) - { - key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); - key_uint[3] ^= (uint)i; - - uint[] data = new uint[2]; - data[0] = (uint)(pixels[i + 0].r + (pixels[i + 0].g << 8) + (pixels[i + 0].b << 16) + (pixels[i + 0].a << 24)); - data[1] = (uint)(pixels[i + 1].r + (pixels[i + 1].g << 8) + (pixels[i + 1].b << 16) + (pixels[i + 1].a << 24)); - - uint[] data_enc = xxtea.Decrypt(data, key_uint); - - pixels[i + 0].r = (byte)((data_enc[0] & 0x000000FF) >> 0); - pixels[i + 0].g = (byte)((data_enc[0] & 0x0000FF00) >> 8); - pixels[i + 0].b = (byte)((data_enc[0] & 0x00FF0000) >> 16); - pixels[i + 0].a = (byte)((data_enc[0] & 0xFF000000) >> 24); - pixels[i + 1].r = (byte)((data_enc[1] & 0x000000FF) >> 0); - pixels[i + 1].g = (byte)((data_enc[1] & 0x0000FF00) >> 8); - pixels[i + 1].b = (byte)((data_enc[1] & 0x00FF0000) >> 16); - pixels[i + 1].a = (byte)((data_enc[1] & 0xFF000000) >> 24); - } - tmp.SetPixels32(pixels, m); - } - } - else - { - tmp = new Texture2D(tex.width, tex.height, TextureFormat.RGB24, mip_lv - 2, true); //mip_lv-2 is blur trick (like the box filter) - for (int m = 0; m < tmp.mipmapCount; ++m) - { - Color32[] pixels = tex.GetPixels32(m); - - for (int i = 0; i < pixels.Length; i += 4) - { - key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); - key_uint[3] ^= (uint)i; - - uint[] data = new uint[3]; - data[0] = (uint)(pixels[i + 0].r + (pixels[i + 0].g << 8) + (pixels[i + 0].b << 16) + (pixels[i + 1].r << 24)); - data[1] = (uint)(pixels[i + 1].g + (pixels[i + 1].b << 8) + (pixels[i + 2].r << 16) + (pixels[i + 2].g << 24)); - data[2] = (uint)(pixels[i + 2].b + (pixels[i + 3].r << 8) + (pixels[i + 3].g << 16) + (pixels[i + 3].b << 24)); - - uint[] data_enc = xxtea.Decrypt(data, key_uint); - - pixels[i + 0].r = (byte)((data_enc[0] & 0x000000FF) >> 0); - pixels[i + 0].g = (byte)((data_enc[0] & 0x0000FF00) >> 8); - pixels[i + 0].b = (byte)((data_enc[0] & 0x00FF0000) >> 16); - pixels[i + 1].r = (byte)((data_enc[0] & 0xFF000000) >> 24); - pixels[i + 1].g = (byte)((data_enc[1] & 0x000000FF) >> 0); - pixels[i + 1].b = (byte)((data_enc[1] & 0x0000FF00) >> 8); - pixels[i + 2].r = (byte)((data_enc[1] & 0x00FF0000) >> 16); - pixels[i + 2].g = (byte)((data_enc[1] & 0xFF000000) >> 24); - pixels[i + 2].b = (byte)((data_enc[2] & 0x000000FF) >> 0); - pixels[i + 3].r = (byte)((data_enc[2] & 0x0000FF00) >> 8); - pixels[i + 3].g = (byte)((data_enc[2] & 0x00FF0000) >> 16); - pixels[i + 3].b = (byte)((data_enc[2] & 0xFF000000) >> 24); - } - tmp.SetPixels32(pixels, m); - } - } - - tmp.filterMode = FilterMode.Point; - tmp.anisoLevel = 0; - return tmp; - }*/ - } -} -#endif \ No newline at end of file diff --git a/Runtime/Scripts/EncryptedHistory.cs b/Runtime/Scripts/EncryptedHistory.cs index 46f701b..5d3420f 100644 --- a/Runtime/Scripts/EncryptedHistory.cs +++ b/Runtime/Scripts/EncryptedHistory.cs @@ -1,4 +1,4 @@ -using Shell.Protector; +#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; @@ -6,97 +6,101 @@ using UnityEditor; using UnityEngine; -#if UNITY_EDITOR - -[CreateAssetMenu(fileName = "EncryptedHistory", menuName = "ShellProtector/EncryptedHistory", order = 1)] -public class EncryptedHistory : ScriptableObject +namespace Shell.Protector { - [Serializable] - class ShaderInfo + [CreateAssetMenu(fileName = "EncryptedHistory", menuName = "ShellProtector/EncryptedHistory", order = 1)] + public class EncryptedHistory : ScriptableObject { - public string shader; - public long size; - public string hash; - } - - [SerializeField] - List shaderHistory = new List(); - Dictionary shaderHistoryDic = new Dictionary(); + [Serializable] + class ShaderInfo + { + public string shader; + public long size; + public string hash; + } - public void LoadData() - { - if (shaderHistoryDic.Count != 0) - return; + [SerializeField] + List shaderHistory = new List(); + Dictionary shaderHistoryDic = new Dictionary(); - foreach (var info in shaderHistory) + public void LoadData() { - Shader shader = Shader.Find(info.shader); - if (shader) + if (shaderHistoryDic.Count != 0) + return; + + foreach (var info in shaderHistory) { - shaderHistoryDic.TryAdd(shader, info); + Shader shader = Shader.Find(info.shader); + if (shader) + { + shaderHistoryDic.TryAdd(shader, info); + } } } - } - public void Save(Shader shader) - { - if (AssetManager.GetInstance().IslilToon(shader)) - return; - string dir = AssetDatabase.GetAssetPath(shader); - FileInfo fileInfo = new FileInfo(dir); - long size = fileInfo.Length; - string hash = CalculateMD5(dir); - if (shaderHistoryDic.ContainsKey(shader)) - { - shaderHistoryDic[shader].size = size; - shaderHistoryDic[shader].hash = hash; - } - else + public void Save(Shader shader) { - var shaderInfo = new ShaderInfo { shader = shader.name, size = size, hash = hash }; - shaderHistoryDic.Add(shader, shaderInfo); - shaderHistory.Add(shaderInfo); - } + if (AssetManager.GetInstance().IsLilToon(shader)) + return; - EditorUtility.SetDirty(this); - AssetDatabase.SaveAssets(); - } - public Shader IsEncryptedBefore(Shader originalShader) - { - if (shaderHistoryDic.ContainsKey(originalShader)) - { - string dir = AssetDatabase.GetAssetPath(originalShader); + string dir = AssetDatabase.GetAssetPath(shader); FileInfo fileInfo = new FileInfo(dir); long size = fileInfo.Length; - long oldSize = shaderHistoryDic[originalShader].size; - string oldHash = shaderHistoryDic[originalShader].hash; + string hash = CalculateMD5(dir); - if (size == oldSize) + if (shaderHistoryDic.ContainsKey(shader)) { - if(oldHash == CalculateMD5(dir)) - return Shader.Find(originalShader.name + "_encrypted"); - return null; + shaderHistoryDic[shader].size = size; + shaderHistoryDic[shader].hash = hash; } else - return null; + { + var shaderInfo = new ShaderInfo { shader = shader.name, size = size, hash = hash }; + shaderHistoryDic.Add(shader, shaderInfo); + shaderHistory.Add(shaderInfo); + } + + EditorUtility.SetDirty(this); + AssetDatabase.SaveAssets(); } - return null; - } - string CalculateMD5(string filePath) - { - if (!File.Exists(filePath)) + public Shader IsEncryptedBefore(Shader originalShader) { - Debug.LogError($"File not found: {filePath}"); + if (shaderHistoryDic.ContainsKey(originalShader)) + { + string dir = AssetDatabase.GetAssetPath(originalShader); + FileInfo fileInfo = new FileInfo(dir); + long size = fileInfo.Length; + long oldSize = shaderHistoryDic[originalShader].size; + string oldHash = shaderHistoryDic[originalShader].hash; + + if (size == oldSize) + { + if (oldHash == CalculateMD5(dir)) + return Shader.Find(originalShader.name + "_encrypted"); + return null; + } + else + return null; + } return null; } - using (FileStream stream = File.OpenRead(filePath)) + string CalculateMD5(string filePath) { - MD5 md5 = MD5.Create(); - byte[] hashBytes = md5.ComputeHash(stream); - return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + if (!File.Exists(filePath)) + { + Debug.LogError($"File not found: {filePath}"); + return null; + } + + using (FileStream stream = File.OpenRead(filePath)) + { + MD5 md5 = MD5.Create(); + byte[] hashBytes = md5.ComputeHash(stream); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/Formats.meta b/Runtime/Scripts/Formats.meta new file mode 100644 index 0000000..42f816d --- /dev/null +++ b/Runtime/Scripts/Formats.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 30147c802ecd3944ba60a816157bea4c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Formats/DXTFormat.cs b/Runtime/Scripts/Formats/DXTFormat.cs new file mode 100644 index 0000000..ae40564 --- /dev/null +++ b/Runtime/Scripts/Formats/DXTFormat.cs @@ -0,0 +1,206 @@ +using UnityEngine; +using System; + +#if UNITY_EDITOR + +namespace Shell.Protector +{ + public abstract class DXTFormat : BaseTextureFormat { + protected byte[] GetArrayDXT(byte[] data, int texture_width, int texture_height, bool dxt5, int miplv) { + int start = 0; + int end = 0; + + for (int i = 0; i <= miplv; ++i) { + start += end; + int w = texture_width / (int)(Mathf.Pow(2, i)); + int h = texture_height / (int)(Mathf.Pow(2, i)); + int block_count = (w / 4) * (h / 4); + int len = block_count * 8; + if (dxt5) len *= 2; + end = len; + } + + var segment = new ArraySegment(data, start, end); + return segment.ToArray(); + } + + protected Texture2D HandleCrunchedFormat(Texture2D texture, int mip_lv, bool isDXT5) { + if (!texture.name.Contains("Crunched")) return texture; + + Debug.LogWarningFormat("{0} is the crunch compression format. There may be degradation in image quality.", texture.name); + var format = isDXT5 ? TextureFormat.RGBA32 : TextureFormat.RGB24; + var result = new Texture2D(texture.width, texture.height, format, mip_lv, true); + + for (int m = 0; m <= mip_lv; ++m) { + if (m != 0 && m == mip_lv) break; + result.SetPixels32(texture.GetPixels32(m), m); + result.Apply(); + } + + result.Compress(isDXT5); + return result; + } + + public override void SetFormatKeywords(Material material) { + material.DisableKeyword(ShaderProperties.Format0Keyword); + material.DisableKeyword(ShaderProperties.Format1Keyword); + } + + public override (int, int) CalculateOffsets(Texture2D texture) { + int woffset = 13 - (int)Mathf.Log(texture.width, 2) - 1 + 2; + int hoffset = 13 - (int)Mathf.Log(texture.height, 2) - 1 + 2; + return (woffset, hoffset); + } + } + + public class DXT1Format : DXTFormat { + public override bool CanHandle(TextureFormat format) { + return format == TextureFormat.DXT1 || format == TextureFormat.DXT1Crunched; + } + + public override EncryptResult Encrypt(Texture2D texture, byte[] key, IEncryptor algorithm) { + if (texture.width < 8) { + throw new Exception($"{texture.name} : The texture width must be >= 8px"); + } + + if (texture.height < 4) { + throw new Exception($"{texture.name} : The texture height must be >= 4px"); + } + + int mip_lv = GetCanMipmapLevel(texture.width / 4, texture.height / 4); + Texture2D dxt1 = HandleCrunchedFormat(texture, mip_lv, false); + + var result = new EncryptResult(); + if (mip_lv != 0) { + result.Texture1 = new Texture2D(dxt1.width, dxt1.height, TextureFormat.DXT1, mip_lv, true); + result.Texture2 = new Texture2D(dxt1.width / 4, dxt1.height / 4, TextureFormat.RGBA32, mip_lv, true); + } else { + result.Texture1 = new Texture2D(dxt1.width, dxt1.height, TextureFormat.DXT1, false, true); + result.Texture2 = new Texture2D(dxt1.width / 4, dxt1.height / 4, TextureFormat.RGBA32, false, true); + } + result.Texture2.filterMode = FilterMode.Point; + result.Texture2.anisoLevel = 0; + + var raw_data = dxt1.GetRawTextureData(); + int lenidx = 0; + var key_uint = ConvertKeyToUInt(key); + + for (int m = 0; m <= mip_lv; ++m) { + if (m != 0 && m == mip_lv) break; + var tex_data = GetArrayDXT(raw_data, dxt1.width, dxt1.height, false, m); + var pixel = result.Texture2.GetPixels32(m); + + for (int i = 0; i < tex_data.Length; i += 16) { + key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); + key_uint[3] ^= (uint)(i / 8); + + uint[] data = new uint[2]; + data[0] = (uint)(tex_data[i + 0] + (tex_data[i + 1] << 8) + (tex_data[i + 2] << 16) + (tex_data[i + 3] << 24)); + data[1] = (uint)(tex_data[(i + 8) + 0] + (tex_data[(i + 8) + 1] << 8) + (tex_data[(i + 8) + 2] << 16) + (tex_data[(i + 8) + 3] << 24)); + + uint[] data_enc = algorithm.Encrypt(data, key_uint); + + for (int j = 0; j < 2; ++j) { + pixel[i / 8 + j].r = (byte)((data_enc[j] & 0x000000FF) >> 0); + pixel[i / 8 + j].g = (byte)((data_enc[j] & 0x0000FF00) >> 8); + pixel[i / 8 + j].b = (byte)((data_enc[j] & 0x00FF0000) >> 16); + pixel[i / 8 + j].a = (byte)((data_enc[j] & 0xFF000000) >> 24); + } + } + for (int i = 0; i < tex_data.Length; i += 8) { + tex_data[i + 0] = 255; + tex_data[i + 1] = 255; + tex_data[i + 2] = 0; + tex_data[i + 3] = 0; + } + for (int i = 0; i < tex_data.Length; ++i) { + raw_data[i + lenidx] = tex_data[i]; + } + lenidx += tex_data.Length; + result.Texture2.SetPixels32(pixel, m); + } + result.Texture1.LoadRawTextureData(raw_data); + result.Texture1.filterMode = FilterMode.Point; + result.Texture1.anisoLevel = 0; + + return result; + } + } + + public class DXT5Format : DXTFormat { + public override bool CanHandle(TextureFormat format) { + return format == TextureFormat.DXT5 || format == TextureFormat.DXT5Crunched; + } + + public override EncryptResult Encrypt(Texture2D texture, byte[] key, IEncryptor algorithm) { + if (texture.width < 8) { + throw new Exception($"{texture.name} : The texture width must be >= 8px"); + } + + if (texture.height < 4) { + throw new Exception($"{texture.name} : The texture height must be >= 4px"); + } + + int mip_lv = GetCanMipmapLevel(texture.width / 4, texture.height / 4); + Texture2D dxt5 = HandleCrunchedFormat(texture, mip_lv, true); + + var result = new EncryptResult(); + if (mip_lv != 0) { + result.Texture1 = new Texture2D(texture.width, texture.height, TextureFormat.DXT5, mip_lv, true); + result.Texture2 = new Texture2D(texture.width / 4, texture.height / 4, TextureFormat.RGBA32, mip_lv, true); + } else { + result.Texture1 = new Texture2D(texture.width, texture.height, TextureFormat.DXT5, false, true); + result.Texture2 = new Texture2D(texture.width / 4, texture.height / 4, TextureFormat.RGBA32, false, true); + } + result.Texture1.alphaIsTransparency = true; + result.Texture2.filterMode = FilterMode.Point; + result.Texture2.anisoLevel = 0; + + var raw_data = dxt5.GetRawTextureData(); + int lenidx = 0; + var key_uint = ConvertKeyToUInt(key); + + for (int m = 0; m <= mip_lv; ++m) { + if (m != 0 && m == mip_lv) break; + var tex_data = GetArrayDXT(raw_data, texture.width, texture.height, true, m); + var pixel = result.Texture2.GetPixels32(m); + + for (int i = 0; i < tex_data.Length; i += 32) { + key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); + key_uint[3] ^= (uint)(i / 16); + + uint[] data = new uint[2]; + data[0] = (uint)(tex_data[i + 8] + (tex_data[i + 9] << 8) + (tex_data[i + 10] << 16) + (tex_data[i + 11] << 24)); + data[1] = (uint)(tex_data[i + 16 + 8] + (tex_data[i + 16 + 9] << 8) + (tex_data[i + 16 + 10] << 16) + (tex_data[i + 16 + 11] << 24)); + + uint[] data_enc = algorithm.Encrypt(data, key_uint); + + for (int j = 0; j < 2; ++j) { + pixel[i / 16 + j].r = (byte)((data_enc[j] & 0x000000FF) >> 0); + pixel[i / 16 + j].g = (byte)((data_enc[j] & 0x0000FF00) >> 8); + pixel[i / 16 + j].b = (byte)((data_enc[j] & 0x00FF0000) >> 16); + pixel[i / 16 + j].a = (byte)((data_enc[j] & 0xFF000000) >> 24); + } + } + for (int i = 0; i < tex_data.Length; i += 16) { + tex_data[i + 8] = 255; + tex_data[i + 9] = 255; + tex_data[i + 10] = 0; + tex_data[i + 11] = 0; + } + for (int i = 0; i < tex_data.Length; ++i) { + raw_data[i + lenidx] = tex_data[i]; + } + lenidx += tex_data.Length; + result.Texture2.SetPixels32(pixel, m); + } + result.Texture1.LoadRawTextureData(raw_data); + result.Texture1.filterMode = FilterMode.Point; + result.Texture1.anisoLevel = 0; + + return result; + } + } +} + +#endif diff --git a/Runtime/Scripts/Formats/DXTFormat.cs.meta b/Runtime/Scripts/Formats/DXTFormat.cs.meta new file mode 100644 index 0000000..e91f11f --- /dev/null +++ b/Runtime/Scripts/Formats/DXTFormat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54fcef2bb0fb0b243b8a33682d209c41 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Formats/Format.cs b/Runtime/Scripts/Formats/Format.cs new file mode 100644 index 0000000..9dbff3b --- /dev/null +++ b/Runtime/Scripts/Formats/Format.cs @@ -0,0 +1,40 @@ +using Shell.Protector; +using UnityEngine; + +namespace Shell.Protector +{ + public struct EncryptResult { + public Texture2D Texture1; + public Texture2D Texture2; + } + + public interface ITextureFormat { + bool CanHandle(TextureFormat format); + EncryptResult Encrypt(Texture2D texture, byte[] key, IEncryptor algorithm); + void SetFormatKeywords(Material material); + (int, int) CalculateOffsets(Texture2D texture); + } + + public abstract class BaseTextureFormat : ITextureFormat { + protected uint[] ConvertKeyToUInt(byte[] key) { + uint[] key_uint = new uint[4]; + key_uint[0] = (uint)(key[0] | (key[1] << 8) | (key[2] << 16) | (key[3] << 24)); + key_uint[1] = (uint)(key[4] | (key[5] << 8) | (key[6] << 16) | (key[7] << 24)); + key_uint[2] = (uint)(key[8] | (key[9] << 8) | (key[10] << 16) | (key[11] << 24)); + key_uint[3] = 0; + return key_uint; + } + + protected int GetCanMipmapLevel(int w, int h) { + if (w < 1 || h <= 1) return 0; + int w_level = (int)Mathf.Log(w, 2); + int h_level = (int)Mathf.Log(h, 2); + return Mathf.Max(w_level, h_level); + } + + public abstract bool CanHandle(TextureFormat format); + public abstract EncryptResult Encrypt(Texture2D texture, byte[] key, IEncryptor algorithm); + public abstract void SetFormatKeywords(Material material); + public abstract (int, int) CalculateOffsets(Texture2D texture); + } +} diff --git a/Runtime/Scripts/Formats/Format.cs.meta b/Runtime/Scripts/Formats/Format.cs.meta new file mode 100644 index 0000000..aba9c45 --- /dev/null +++ b/Runtime/Scripts/Formats/Format.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28a232c48b14ddb4594ae2600ebedd44 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Formats/RGBFormat.cs b/Runtime/Scripts/Formats/RGBFormat.cs new file mode 100644 index 0000000..6e1b607 --- /dev/null +++ b/Runtime/Scripts/Formats/RGBFormat.cs @@ -0,0 +1,125 @@ +using UnityEngine; + +#if UNITY_EDITOR + +namespace Shell.Protector +{ + public abstract class RGBFormat : BaseTextureFormat { + protected Texture2D CreateResultTexture(Texture2D texture, TextureFormat format) { + int mip_lv = GetCanMipmapLevel(texture.width, texture.height); + var result = new Texture2D(texture.width, texture.height, format, mip_lv - 2, true); + result.filterMode = FilterMode.Point; + result.anisoLevel = 0; + return result; + } + } + + public class RGB24Format : RGBFormat { + public override bool CanHandle(TextureFormat format) { + return format == TextureFormat.RGB24; + } + + public override EncryptResult Encrypt(Texture2D texture, byte[] key, IEncryptor algorithm) { + var result = new EncryptResult(); + result.Texture1 = CreateResultTexture(texture, TextureFormat.RGB24); + + var key_uint = ConvertKeyToUInt(key); + + for (int m = 0; m < result.Texture1.mipmapCount; ++m) { + Color32[] pixels = texture.GetPixels32(m); + + for (int i = 0; i < pixels.Length; i += 4) { + key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); + key_uint[3] ^= (uint)i; + + uint[] data = new uint[3]; + data[0] = (uint)(pixels[i + 0].r + (pixels[i + 0].g << 8) + (pixels[i + 0].b << 16) + (pixels[i + 1].r << 24)); + data[1] = (uint)(pixels[i + 1].g + (pixels[i + 1].b << 8) + (pixels[i + 2].r << 16) + (pixels[i + 2].g << 24)); + data[2] = (uint)(pixels[i + 2].b + (pixels[i + 3].r << 8) + (pixels[i + 3].g << 16) + (pixels[i + 3].b << 24)); + + uint[] data_enc = algorithm.Encrypt(data, key_uint); + + pixels[i + 0].r = (byte)((data_enc[0] & 0x000000FF) >> 0); + pixels[i + 0].g = (byte)((data_enc[0] & 0x0000FF00) >> 8); + pixels[i + 0].b = (byte)((data_enc[0] & 0x00FF0000) >> 16); + pixels[i + 1].r = (byte)((data_enc[0] & 0xFF000000) >> 24); + pixels[i + 1].g = (byte)((data_enc[1] & 0x000000FF) >> 0); + pixels[i + 1].b = (byte)((data_enc[1] & 0x0000FF00) >> 8); + pixels[i + 2].r = (byte)((data_enc[1] & 0x00FF0000) >> 16); + pixels[i + 2].g = (byte)((data_enc[1] & 0xFF000000) >> 24); + pixels[i + 2].b = (byte)((data_enc[2] & 0x000000FF) >> 0); + pixels[i + 3].r = (byte)((data_enc[2] & 0x0000FF00) >> 8); + pixels[i + 3].g = (byte)((data_enc[2] & 0x00FF0000) >> 16); + pixels[i + 3].b = (byte)((data_enc[2] & 0xFF000000) >> 24); + } + result.Texture1.SetPixels32(pixels, m); + } + + return result; + } + + public override void SetFormatKeywords(Material material) { + material.EnableKeyword(ShaderProperties.Format0Keyword); + material.DisableKeyword(ShaderProperties.Format1Keyword); + } + + public override (int, int) CalculateOffsets(Texture2D texture) { + int woffset = 13 - (int)Mathf.Log(texture.width, 2) - 1; + int hoffset = 13 - (int)Mathf.Log(texture.height, 2) - 1; + return (woffset, hoffset); + } + } + + public class RGBA32Format : RGBFormat { + public override bool CanHandle(TextureFormat format) { + return format == TextureFormat.RGBA32; + } + + public override EncryptResult Encrypt(Texture2D texture, byte[] key, IEncryptor algorithm) { + var result = new EncryptResult(); + result.Texture1 = CreateResultTexture(texture, TextureFormat.RGBA32); + + var key_uint = ConvertKeyToUInt(key); + + for (int m = 0; m < result.Texture1.mipmapCount; ++m) { + Color32[] pixels = texture.GetPixels32(m); + + for (int i = 0; i < pixels.Length; i += 2) { + key_uint[3] = (uint)(key[12] | (key[13] << 8) | (key[14] << 16) | (key[15] << 24)); + key_uint[3] ^= (uint)i; + + uint[] data = new uint[2]; + data[0] = (uint)(pixels[i + 0].r + (pixels[i + 0].g << 8) + (pixels[i + 0].b << 16) + (pixels[i + 0].a << 24)); + data[1] = (uint)(pixels[i + 1].r + (pixels[i + 1].g << 8) + (pixels[i + 1].b << 16) + (pixels[i + 1].a << 24)); + + uint[] data_enc = algorithm.Encrypt(data, key_uint); + + pixels[i + 0].r = (byte)((data_enc[0] & 0x000000FF) >> 0); + pixels[i + 0].g = (byte)((data_enc[0] & 0x0000FF00) >> 8); + pixels[i + 0].b = (byte)((data_enc[0] & 0x00FF0000) >> 16); + pixels[i + 0].a = (byte)((data_enc[0] & 0xFF000000) >> 24); + pixels[i + 1].r = (byte)((data_enc[1] & 0x000000FF) >> 0); + pixels[i + 1].g = (byte)((data_enc[1] & 0x0000FF00) >> 8); + pixels[i + 1].b = (byte)((data_enc[1] & 0x00FF0000) >> 16); + pixels[i + 1].a = (byte)((data_enc[1] & 0xFF000000) >> 24); + } + result.Texture1.SetPixels32(pixels, m); + } + + return result; + } + + public override void SetFormatKeywords(Material material) { + material.DisableKeyword(ShaderProperties.Format0Keyword); + material.EnableKeyword(ShaderProperties.Format1Keyword); + } + + public override (int, int) CalculateOffsets(Texture2D texture) { + int woffset = 13 - (int)Mathf.Log(texture.width, 2) - 1; + int hoffset = 13 - (int)Mathf.Log(texture.height, 2) - 1; + return (woffset, hoffset); + } + } +} + +#endif diff --git a/Runtime/Scripts/Formats/RGBFormat.cs.meta b/Runtime/Scripts/Formats/RGBFormat.cs.meta new file mode 100644 index 0000000..38d3beb --- /dev/null +++ b/Runtime/Scripts/Formats/RGBFormat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d80e9a000061de4b8709ef3f1eef4b5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Injector/IShaderAdapter.cs b/Runtime/Scripts/Injector/IShaderAdapter.cs new file mode 100644 index 0000000..b0fca1b --- /dev/null +++ b/Runtime/Scripts/Injector/IShaderAdapter.cs @@ -0,0 +1,14 @@ +#if UNITY_EDITOR +using UnityEngine; + +namespace Shell.Protector +{ + public interface IShaderAdapter + { + bool CanHandle(Shader shader); + bool WasInjected(Shader shader); + void SetKeywords(Material material, bool hasLimTexture = false); + Shader Inject(Material material, string decoderPath, string outputPath, Texture2D mainTexture, AuxiliaryTextures auxiliaryTextures); + } +} +#endif diff --git a/Runtime/Scripts/Injector/IShaderAdapter.cs.meta b/Runtime/Scripts/Injector/IShaderAdapter.cs.meta new file mode 100644 index 0000000..784a0fc --- /dev/null +++ b/Runtime/Scripts/Injector/IShaderAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b6fcb5c843134bc3bb07144b57fbce16 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Injector/Injector.cs b/Runtime/Scripts/Injector/Injector.cs index 86345a7..9166314 100644 --- a/Runtime/Scripts/Injector/Injector.cs +++ b/Runtime/Scripts/Injector/Injector.cs @@ -1,69 +1,38 @@ -using System.Collections; -using System.Collections.Generic; using System.IO; -using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; -using static VRC.SDK3.Dynamics.PhysBone.PhysBoneMigration.DynamicBoneColliderData; #if UNITY_EDITOR namespace Shell.Protector { - abstract public class Injector + public abstract class Injector : IShaderAdapter { - protected ushort[] keys = new ushort[8]; //16byte - protected AssetManager shader_manager = AssetManager.GetInstance(); - protected int filter = 1; - protected string asset_dir; - protected int user_key_length = 4; + protected readonly ushort[] Keys = new ushort[8]; // 16 byte + protected readonly AssetManager ShaderManager = AssetManager.GetInstance(); + protected int Filter = 1; + protected string AssetDir; + protected int UserKeyLength = 4; - protected string shader_code_nofilter = @" + protected string ShaderCodeNoFilter = @" half4 mainTexture; UNITY_BRANCH - if(_fallback == 0) + if(isDecrypted) { - half4 mip_texture = _MipTex.Sample(sampler_MipTex, mainUV); - - int mip = round(mip_texture.r * 255 / 10); //fucking precision problems - const int m[13] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10 }; // max size 4k - - half4 c00 = DecryptTexture(mainUV, m[mip]); - - mainTexture = c00; + mainTexture = DecryptTextureBox(_EncryptTex0, _EncryptTex1, sampler_EncryptTex0, _EncryptTex0_TexelSize, _MipTex, sampler_MipTex, mainUV); } else { mainTexture = _MainTex.Sample(sampler_MainTex, mainUV); } "; - protected string shader_code_bilinear = @" + protected string ShaderCodeBilinear = @" half4 mainTexture; UNITY_BRANCH - if(_fallback == 0) + if(isDecrypted) { - half4 mip_texture = _MipTex.Sample(sampler_MipTex, mainUV); - - half2 uv_unit = _EncryptTex0_TexelSize.xy; - //bilinear interpolation - half2 uv_bilinear = poiMesh.uv[0] - 0.5 * uv_unit; - int mip = round(mip_texture.r * 255 / 10); //fucking precision problems - const int m[13] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10 }; // max size 4k - - half4 c00 = DecryptTexture(uv_bilinear + half2(uv_unit.x * 0, uv_unit.y * 0), m[mip]); - half4 c10 = DecryptTexture(uv_bilinear + half2(uv_unit.x * 1, uv_unit.y * 0), m[mip]); - half4 c01 = DecryptTexture(uv_bilinear + half2(uv_unit.x * 0, uv_unit.y * 1), m[mip]); - half4 c11 = DecryptTexture(uv_bilinear + half2(uv_unit.x * 1, uv_unit.y * 1), m[mip]); - - half2 f = frac(uv_bilinear * _EncryptTex0_TexelSize.zw); - - half4 c0 = lerp(c00, c10, f.x); - half4 c1 = lerp(c01, c11, f.x); - - half4 bilinear = lerp(c0, c1, f.y); - - mainTexture = bilinear; + mainTexture = DecryptTextureBilinear(_EncryptTex0, _EncryptTex1, sampler_EncryptTex0, _EncryptTex0_TexelSize, _MipTex, sampler_MipTex, mainUV); } else { @@ -71,42 +40,44 @@ abstract public class Injector } "; - protected GameObject target; - protected Texture2D main_tex; - protected IEncryptor encryptor; + protected GameObject Target; + protected Texture2D MainTexture; + protected IEncryptor Encryptor; protected struct Decoder { public Decoder(string decrypt = null, string xxtea = null, string chacha = null) { - this.decrypt = decrypt; - this.xxtea = xxtea; - this.chacha = chacha; + Decrypt = decrypt; + Xxtea = xxtea; + Chacha = chacha; } - public string decrypt; - public string xxtea; - public string chacha; + public string Decrypt; + public string Xxtea; + public string Chacha; } - public void Init(GameObject target, Texture2D main_tex, byte[] key, int user_key_length, int filter, string asset_dir, IEncryptor encryptor) + public void Init(GameObject target, Texture2D mainTexture, byte[] key, int userKeyLength, int filter, string assetDir, IEncryptor encryptor) { if (key.Length != 16) { Debug.LogError("Key bytes requires 16 byte"); return; } - this.target = target; - for (int i = 0, j = 0; i < keys.Length; ++i, j += 2) + Target = target; + for (int i = 0, j = 0; i < Keys.Length; ++i, j += 2) { - keys[i] = (ushort)(key[j] | key[j + 1] << 8); + Keys[i] = (ushort)(key[j] | key[j + 1] << 8); } - this.main_tex = main_tex; - this.filter = filter; - this.asset_dir = asset_dir; - this.user_key_length = user_key_length; - this.encryptor = encryptor; + MainTexture = mainTexture; + Filter = filter; + AssetDir = assetDir; + UserKeyLength = userKeyLength; + Encryptor = encryptor; } + public abstract bool CanHandle(Shader shader); + public bool WasInjected(Shader shader) { string shader_path = AssetDatabase.GetAssetPath(shader); @@ -117,7 +88,47 @@ public bool WasInjected(Shader shader) return false; } - abstract public Shader Inject(Material mat, string decode_dir, string output_dir, Texture2D tex, bool has_lim_texture = false, bool has_lim_texture2 = false, bool outline_tex = false); + public void SetKeywords(Material material, bool hasLimTexture = false) + { + // Clear keywords prefixed with _SHELL_PROTECTOR_ + var keywords = material.shaderKeywords; + foreach (string keyword in keywords) + { + if (keyword.StartsWith(ShaderProperties.KeywordPrefix)) { + material.DisableKeyword(keyword); + } + } + + // Set format keywords + TextureEncryptManager.SetFormatKeywords(material); + + // Set rimlight keyword + if (hasLimTexture) + material.EnableKeyword(ShaderProperties.RimLightKeyword); + + // Set encryptor keyword + material.EnableKeyword(Encryptor.Keyword); + } + + public Shader Inject(Material material, string decoderPath, string outputPath, Texture2D mainTexture, AuxiliaryTextures auxiliaryTextures) + { + return Inject( + material, + decoderPath, + outputPath, + mainTexture, + auxiliaryTextures.LimTexture != null, + auxiliaryTextures.LimTexture2 != null, + auxiliaryTextures.OutlineTexture != null + ); + } + + public Shader Inject(Material mat, string decodeDir, string outputDir, Texture2D tex, bool hasLimTexture = false, bool hasLimTexture2 = false, bool outlineTex = false) + { + return CustomInject(mat, decodeDir, outputDir, tex, hasLimTexture, hasLimTexture2, outlineTex); + } + + protected abstract Shader CustomInject(Material mat, string decodeDir, string outputDir, Texture2D tex, bool hasLimTexture = false, bool hasLimTexture2 = false, bool outlineTex = false); } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/Injector/InjectorFactory.cs b/Runtime/Scripts/Injector/InjectorFactory.cs index cc86f7e..89082bb 100644 --- a/Runtime/Scripts/Injector/InjectorFactory.cs +++ b/Runtime/Scripts/Injector/InjectorFactory.cs @@ -10,14 +10,20 @@ public class InjectorFactory { public static Injector GetInjector(Shader shader) { - AssetManager shader_manager = AssetManager.GetInstance(); - if (shader_manager.IsPoiyomi(shader)) - return new PoiyomiInjector(); - else if (shader_manager.IslilToon(shader)) - return new LilToonInjector(); - else - return null; + Injector[] adapters = + { + new PoiyomiInjector(), + new LilToonInjector() + }; + + foreach (Injector adapter in adapters) + { + if (adapter.CanHandle(shader)) + return adapter; + } + + return null; } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/Injector/LilToonInjector.cs b/Runtime/Scripts/Injector/LilToonInjector.cs index 91f4441..4046440 100644 --- a/Runtime/Scripts/Injector/LilToonInjector.cs +++ b/Runtime/Scripts/Injector/LilToonInjector.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using System; using System.Collections; using System.Collections.Generic; @@ -13,49 +13,22 @@ namespace Shell.Protector { public class LilToonInjector : Injector { - public override Shader Inject(Material mat, string decode_dir, string output_path, Texture2D tex, bool has_lim_texture = false, bool has_lim_texture2 = false, bool outline_tex = false) + public override bool CanHandle(Shader shader) { - // Keyword setting - TextureFormat format = ((Texture2D)mat.mainTexture).format; - if (format == TextureFormat.DXT1 || format == TextureFormat.DXT5) - { - mat.DisableKeyword("_FORMAT0"); - mat.DisableKeyword("_FORMAT1"); - } - else if (format == TextureFormat.RGBA32) - { - mat.DisableKeyword("_FORMAT0"); - mat.EnableKeyword("_FORMAT1"); - } - else if (format == TextureFormat.RGB24) - { - mat.EnableKeyword("_FORMAT0"); - mat.DisableKeyword("_FORMAT1"); - } - else - { - Debug.LogErrorFormat("{0} - main texture is unsupported format!", mat.name); - return null; - } - if (has_lim_texture) - mat.EnableKeyword("_LIMLIGHT_ENCRYPTED"); - else - mat.DisableKeyword("_LIMLIGHT_ENCRYPTED"); - - if ((this.encryptor as XXTEA) != null) - mat.EnableKeyword("_XXTEA"); - else - mat.DisableKeyword("_XXTEA"); + return ShaderManager.IsLilToon(shader); + } - string[] files = Directory.GetFiles(Path.Combine(asset_dir, "liltoonProtector", "Shaders")); + protected override Shader CustomInject(Material mat, string decodeDir, string outputPath, Texture2D tex, bool hasLimTexture = false, bool hasLimTexture2 = false, bool outlineTex = false) + { + string[] files = Directory.GetFiles(Path.Combine(AssetDir, "liltoonProtector", "Shaders")); // Find pass - string shader_dir = AssetDatabase.GetAssetPath(mat.shader); - string shader_name = Path.GetFileNameWithoutExtension(shader_dir); + string shaderDir = AssetDatabase.GetAssetPath(mat.shader); + string shaderName = Path.GetFileNameWithoutExtension(shaderDir); string pass = ""; foreach (string file in files) { string filename = Path.GetFileName(file); - if (filename.Contains(shader_name)) + if (filename.Contains(shaderName)) { string f = File.ReadAllText(file); Match match = Regex.Match(f, "lilPassShaderName \".*/(.*?)\""); @@ -70,10 +43,10 @@ public override Shader Inject(Material mat, string decode_dir, string output_pat string filename = Path.GetFileName(file); if (filename.Contains(".lilcontainer")) { - if (filename == shader_name + ".lilcontainer") + if (filename == shaderName + ".lilcontainer") { Debug.Log(filename); - return AssetDatabase.LoadAssetAtPath(Path.Combine(Path.Combine(asset_dir, "liltoonProtector", "Shaders"), filename)); + return AssetDatabase.LoadAssetAtPath(Path.Combine(Path.Combine(AssetDir, "liltoonProtector", "Shaders"), filename)); } } } @@ -81,4 +54,4 @@ public override Shader Inject(Material mat, string decode_dir, string output_pat } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/Injector/PoiyomiInjector.cs b/Runtime/Scripts/Injector/PoiyomiInjector.cs index 3ad0818..7c482ee 100644 --- a/Runtime/Scripts/Injector/PoiyomiInjector.cs +++ b/Runtime/Scripts/Injector/PoiyomiInjector.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using System.Collections; using System.Collections.Generic; using System.IO; @@ -10,40 +10,44 @@ namespace Shell.Protector { public class PoiyomiInjector : Injector { - public override Shader Inject(Material mat, string decode_dir, string output_path, Texture2D tex, bool has_lim_texture = false, bool has_lim_texture2 = false, bool outline_tex = false) + public override bool CanHandle(Shader shader) { - if (!File.Exists(decode_dir)) + return ShaderManager.IsPoiyomi(shader); + } + + protected override Shader CustomInject(Material mat, string decodeDir, string outputPath, Texture2D tex, bool hasLimTexture = false, bool hasLimTexture2 = false, bool outlineTex = false) + { + if (!File.Exists(decodeDir)) { - Debug.LogError(decode_dir + " is not exits."); + Debug.LogError(decodeDir + " is not exits."); return null; } Shader shader = mat.shader; - if (!AssetDatabase.IsValidFolder(output_path)) + if (!AssetDatabase.IsValidFolder(outputPath)) { - AssetDatabase.CreateFolder(Path.GetDirectoryName(output_path), Path.GetFileName(output_path)); + AssetDatabase.CreateFolder(Path.GetDirectoryName(outputPath), Path.GetFileName(outputPath)); AssetDatabase.Refresh(); } - string shader_path = AssetDatabase.GetAssetPath(shader); - string shader_name = Path.GetFileName(shader_path); - + string shaderPath = AssetDatabase.GetAssetPath(shader); + string shaderName = Path.GetFileName(shaderPath); - string[] files = Directory.GetFiles(Path.GetDirectoryName(shader_path)); + string[] files = Directory.GetFiles(Path.GetDirectoryName(shaderPath)); foreach (string file in files) { string filename = Path.GetFileName(file); if (filename.Contains(".meta")) continue; - File.Copy(file, Path.Combine(output_path, filename), true); + File.Copy(file, Path.Combine(outputPath, filename), true); } - string shader_data = File.ReadAllText(Path.Combine(output_path, shader_name)); - shader_data = shader_data.Insert(0, "//ShellProtect\n"); + string shaderData = File.ReadAllText(Path.Combine(outputPath, shaderName)); + shaderData = shaderData.Insert(0, "//ShellProtect\n"); - InsertProperties(ref shader_data); - if (shader_data == null) + InsertProperties(ref shaderData); + if (shaderData == null) return null; const string declare = @" @@ -53,78 +57,51 @@ public override Shader Inject(Material mat, string decode_dir, string output_pat Texture2D _EncryptTex1; float4 _EncryptTex0_TexelSize; - fixed _fallback; + int _PasswordHash; "; + const string vertexOut = @" + struct VertexOut + { + int isDecrypted : SHELL0;"; + const string returnO = @" + o.isDecrypted = IsDecrypted(); + return o;"; + int version = AssetManager.GetInstance().GetShaderType(shader); - if(version == 73) + if(version >= 80) { - string path = output_path + "/CGI_Poicludes.cginc"; - string poicludes = File.ReadAllText(path); - poicludes = Regex.Replace(poicludes, "UNITY_DECLARE_TEX2D\\(_MainTex\\);(.*?)", declare); - File.WriteAllText(path, poicludes); - - path = output_path + "/CGI_PoiFrag.cginc"; - string frag = File.ReadAllText(path); - frag = Regex.Replace(frag, "float4 frag\\(", "#include \"Decrypt.cginc\"\nfloat4 frag("); - - string shader_code = shader_code_nofilter; - if (filter == 0) - shader_code = shader_code_nofilter; - else if (filter == 1) - shader_code = shader_code_bilinear; - - frag = Regex.Replace(frag, "float4 mainTexture = .*?;", shader_code); - frag = Regex.Replace(frag, "float4 mip_texture = _MipTex.Sample\\(sampler_MipTex, .*?\\);", "float4 mip_texture = _MipTex.Sample(sampler_MipTex, poiMesh.uv[0]);"); - if (tex.format == TextureFormat.DXT1) - { - frag = Regex.Replace(frag, "DecryptTexture", "DecryptTextureDXT"); - } - else if (EncryptTexture.HasAlpha(tex)) - { - frag = Regex.Replace(frag, "DecryptTexture", "DecryptTextureRGBA"); - } - File.WriteAllText(path, frag); - } - else if(version >= 80) - { - shader_data = Regex.Replace(shader_data, "UNITY_DECLARE_TEX2D\\(_MainTex\\);", declare); - - shader_data = Regex.Replace(shader_data, "POI2D_SAMPLER_PAN\\((.*?), _MainTex", "POI2D_SAMPLER_PAN($1, _MipTex"); - shader_data = Regex.Replace(shader_data, "UNITY_SAMPLE_TEX2D_SAMPLER_LOD\\((.*?), _MainTex", "UNITY_SAMPLE_TEX2D_SAMPLER_LOD($1, _MipTex"); - shader_data = Regex.Replace(shader_data, "UNITY_SAMPLE_TEX2D_SAMPLER\\((.*?), _MainTex", "UNITY_SAMPLE_TEX2D_SAMPLER($1, _MipTex"); - shader_data = Regex.Replace(shader_data, "float4 frag\\(", "#include \"" + decode_dir + "\"\n\t\t\tfloat4 frag("); - string shader_code = shader_code_nofilter; - if (filter == 0) - shader_code = shader_code_nofilter; - else if (filter == 1) - shader_code = shader_code_bilinear; - - shader_data = Regex.Replace(shader_data, "float4 mainTexture = .*?;", shader_code); - if (tex.format == TextureFormat.DXT1 || tex.format == TextureFormat.DXT5) - { - shader_data = Regex.Replace(shader_data, "DecryptTexture", "DecryptTextureDXT"); - } - else if (EncryptTexture.HasAlpha(tex)) - { - shader_data = Regex.Replace(shader_data, "DecryptTexture", "DecryptTextureRGBA"); - } - - if (has_lim_texture) + string includeStr = string.Format("#include \"{0}\"", decodeDir); + shaderData = Regex.Replace(shaderData, "UNITY_DECLARE_TEX2D\\(_MainTex\\);", declare); + + shaderData = Regex.Replace(shaderData, "POI2D_SAMPLER_PAN\\((.*?), _MainTex", "POI2D_SAMPLER_PAN($1, _MipTex"); + shaderData = Regex.Replace(shaderData, "UNITY_SAMPLE_TEX2D_SAMPLER_LOD\\((.*?), _MainTex", "UNITY_SAMPLE_TEX2D_SAMPLER_LOD($1, _MipTex"); + shaderData = Regex.Replace(shaderData, "UNITY_SAMPLE_TEX2D_SAMPLER\\((.*?), _MainTex", "UNITY_SAMPLE_TEX2D_SAMPLER($1, _MipTex"); + shaderData = Regex.Replace(shaderData, "float4 frag\\(", "#include \"" + decodeDir + "\"\n\t\t\tfloat4 frag("); + shaderData = Regex.Replace(shaderData, "struct VertexOut[\r\n]+[ \t]*\\{", string.Format("{0}\r\n{1}", includeStr, vertexOut)); + shaderData = Regex.Replace(shaderData, "\t+return o;", returnO); + string shaderCode = ShaderCodeNoFilter; + if (Filter == 0) + shaderCode = ShaderCodeNoFilter; + else if (Filter == 1) + shaderCode = ShaderCodeBilinear; + + shaderData = Regex.Replace(shaderData, "float4 mainTexture = .*?;", string.Format("bool isDecrypted = i.isDecrypted == 1;\r\n{0}", shaderCode)); + if (hasLimTexture) { if(version == 80) - shader_data = Regex.Replace(shader_data, @"float4 rimColor = .*?_RimTex.*?;", "float4 rimColor = float4(poiFragData.baseColor, poiFragData.alpha);"); + shaderData = Regex.Replace(shaderData, @"float4 rimColor = .*?_RimTex.*?;", "float4 rimColor = float4(poiFragData.baseColor, poiFragData.alpha);"); else - shader_data = Regex.Replace(shader_data, @"float4 rimColor = .*?_RimTex.*?;", "float4 rimColor = mainTexture;"); + shaderData = Regex.Replace(shaderData, @"float4 rimColor = .*?_RimTex.*?;", "float4 rimColor = mainTexture;"); } - if (has_lim_texture2) + if (hasLimTexture2) { if(version == 80) - shader_data = Regex.Replace(shader_data, @"float4 rim2Color = .*?_Rim2Tex.*?;", "float4 rim2Color = float4(poiFragData.baseColor, poiFragData.alpha);"); + shaderData = Regex.Replace(shaderData, @"float4 rim2Color = .*?_Rim2Tex.*?;", "float4 rim2Color = float4(poiFragData.baseColor, poiFragData.alpha);"); else - shader_data = Regex.Replace(shader_data, @"float4 rim2Color = .*?_Rim2Tex.*?;", "float4 rim2Color = mainTexture;"); + shaderData = Regex.Replace(shaderData, @"float4 rim2Color = .*?_Rim2Tex.*?;", "float4 rim2Color = mainTexture;"); } - if (outline_tex) - shader_data = Regex.Replace(shader_data, @"float4 col = .*?_OutlineTexture.*?\* float4(.*?);", "float4 col = float4(poiFragData.baseColor, poiFragData.alpha) * float4$1;"); + if (outlineTex) + shaderData = Regex.Replace(shaderData, @"float4 col = .*?_OutlineTexture.*?\* float4(.*?);", "float4 col = float4(poiFragData.baseColor, poiFragData.alpha) * float4$1;"); } else @@ -132,12 +109,12 @@ public override Shader Inject(Material mat, string decode_dir, string output_pat Debug.LogErrorFormat("{0} is unsupported Poiyomi version!", mat.name); return null; } - File.WriteAllText(Path.Combine(output_path, shader_name), shader_data); + File.WriteAllText(Path.Combine(outputPath, shaderName), shaderData); AssetDatabase.Refresh(); - Shader return_shader = AssetDatabase.LoadAssetAtPath(Path.Combine(output_path, shader_name), typeof(Shader)) as Shader; - return return_shader; + Shader returnShader = AssetDatabase.LoadAssetAtPath(Path.Combine(outputPath, shaderName), typeof(Shader)) as Shader; + return returnShader; } private void InsertProperties(ref string data) @@ -148,23 +125,25 @@ private void InsertProperties(ref string data) Match match = Regex.Match(data, "Properties\\W*{"); if (match.Success) { - int suffix_idx = match.Index + match.Length; + int suffixIndex = match.Index + match.Length; string properties = @" - _MipTex (""MipReference"", 2D) = ""white"" { } - _EncryptTex0 (""Encrypted0"", 2D) = ""white"" { } - _EncryptTex1 (""Encrypted1"", 2D) = ""white"" { } - [MaterialToggle] _fallback(""Fallback"", float) = 0 - _Woffset (""Woffset"", integer) = 0 - _Hoffset (""Hoffset"", integer) = 0 - _Nonce0 (""Nonce"", integer) = 0 - _Nonce1 (""Nonce"", integer) = 0 - _Nonce2 (""Nonce"", integer) = 0 +" + ShaderProperties.MipTexture + @" (""MipReference"", 2D) = ""white"" { } +" + ShaderProperties.EncryptTexture0 + @" (""Encrypted0"", 2D) = ""white"" { } +" + ShaderProperties.EncryptTexture1 + @" (""Encrypted1"", 2D) = ""white"" { } +" + ShaderProperties.WidthOffset + @" (""Woffset"", integer) = 0 +" + ShaderProperties.HeightOffset + @" (""Hoffset"", integer) = 0 +" + ShaderProperties.Nonce0 + @" (""Nonce"", integer) = 0 +" + ShaderProperties.Nonce1 + @" (""Nonce"", integer) = 0 +" + ShaderProperties.Nonce2 + @" (""Nonce"", integer) = 0 +" + ShaderProperties.Rounds + @" (""Rounds"", integer) = 0 +" + ShaderProperties.PasswordHash + @" (""PasswordHash"", integer) = 0 +" + ShaderProperties.HashMagic + @" (""HashMagic"", integer) = 0 "; for (int i = 0; i < 16; ++i) - properties += "_Key" + i + " (\"key" + i + "\", float) = 0\n"; + properties += ShaderProperties.KeyPrefix + i + " (\"key" + i + "\", float) = 0\n"; - data = data.Insert(suffix_idx, properties); + data = data.Insert(suffixIndex, properties); } else { @@ -174,4 +153,4 @@ [MaterialToggle] _fallback(""Fallback"", float) = 0 } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/KeyGenerator.cs b/Runtime/Scripts/KeyGenerator.cs index 294deb3..42b8442 100644 --- a/Runtime/Scripts/KeyGenerator.cs +++ b/Runtime/Scripts/KeyGenerator.cs @@ -2,6 +2,9 @@ using System.Security.Cryptography; using System; using UnityEngine; + +namespace Shell.Protector +{ public class KeyGenerator { //key1 is fixed key @@ -59,13 +62,49 @@ public static string GenerateRandomString(int length) const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+=|\\/?.>,<~`\'\" "; StringBuilder builder = new StringBuilder(); - System.Random random = new System.Random(); - for (int i = 0; i < length; i++) + using (RandomNumberGenerator random = RandomNumberGenerator.Create()) { - int index = random.Next(chars.Length); - builder.Append(chars[index]); + byte[] buffer = new byte[4]; + for (int i = 0; i < length; i++) + { + random.GetBytes(buffer); + int index = (int)(BitConverter.ToUInt32(buffer, 0) % chars.Length); + builder.Append(chars[index]); + } } return builder.ToString(); } + + public static uint SimpleHash(byte[] data, uint hashMagic) + { + if (data.Length != 16) + throw new ArgumentException("Input must be exactly 16 bytes."); + + uint hash = 0x811C9DC5u; + hash *= hashMagic; + + for (int i = 0; i < 16; i++) + { + uint k = data[i]; + + k *= 0xcc9e2d51u; + k = (k << 15) | (k >> 17); + k *= 0x1b873593u; + + hash ^= k; + hash = (hash << 13) | (hash >> 19); + hash = hash * 5u + 0xe6546b64u; + } + + hash ^= 16u; + hash ^= (hash >> 16); + hash *= 0x85ebca6bu; + hash ^= (hash >> 13); + hash *= 0xc2b2ae35u; + hash ^= (hash >> 16); + + return hash; + } +} } diff --git a/Runtime/Scripts/lang.meta b/Runtime/Scripts/Localization.meta similarity index 100% rename from Runtime/Scripts/lang.meta rename to Runtime/Scripts/Localization.meta diff --git a/Runtime/Scripts/lang/LanguageManager.cs b/Runtime/Scripts/Localization/LanguageManager.cs similarity index 97% rename from Runtime/Scripts/lang/LanguageManager.cs rename to Runtime/Scripts/Localization/LanguageManager.cs index 9057a38..cb95e4f 100644 --- a/Runtime/Scripts/lang/LanguageManager.cs +++ b/Runtime/Scripts/Localization/LanguageManager.cs @@ -66,7 +66,7 @@ private LanguageManager() { "Small mip texture", "작은 밉 텍스쳐" }, { "It uses a smaller mipTexture to reduce memory usage and improve performance. It may look slightly different from the original when viewed from the side.", "작은 밉 텍스쳐를 사용하여 메모리 사용량을 줄이고 성능을 개선합니다. 옆에서 봤을 때 원본과 약간 다르게 보일 수 있습니다."}, { "Object list", "오브젝트 목록" }, - { "Manual Encrypt!", "수동 암호화 시작!" }, + { "Manual Encrypt! (for testing)", "수동 암호화 시작! (테스트용)" }, { "Modular avatars exist. It is automatically encrypted on upload.", "모듈러 아바타가 존재합니다. 업로드 시 자동으로 암호화됩니다." }, { "Force progress", "강제 진행" }, { "Obfustactor Options", "난독화 옵션" }, @@ -89,7 +89,8 @@ private LanguageManager() { "New shader", "새로운 셰이더" }, { "The main texture is not Texture2D.", "메인 텍스쳐가 Texture2D가 아닙니다." }, { "Opponents with Safety option turned on will see degraded textures instead of noise.", "세이프티를 켜둔 상대방은 노이즈 대신 저하된 텍스처를 보게 됩니다."}, - { "Default fallback texture", "기본 폴백 텍스쳐" } + { "Default fallback texture", "기본 폴백 텍스쳐" }, + { "Under development.", "개발 중입니다." } }; var jpStrings = new Dictionary() @@ -138,7 +139,7 @@ private LanguageManager() { "Small mip texture", "小さなミップテクスチャ" }, { "It uses a smaller mipTexture to reduce memory usage and improve performance. It may look slightly different from the original when viewed from the side.", "小さなミップテクスチャを使用して、メモリ使用量を減らし、パフォーマンスを向上させます。 横から見ると、オリジナルと少し違って見えるかもしれません。"}, { "Object list", "オブジェクト一覧"}, - { "Manual Encrypt!", "手動暗号化開始!" }, + { "Manual Encrypt! (for testing)", "手動暗号化開始!(テスト用)" }, { "Modular avatars exist. it is automatically encrypted on upload.", "Modular Avatarが存在します。アップロード時に自動的に暗号化されます。" }, { "Force progress", "強制的に進行" }, { "Obfustactor Options", "難読化オプション" }, @@ -161,7 +162,8 @@ private LanguageManager() { "New shader", "新しいシェーダー" }, {"The main texture is not Texture2D.", "メインテクスチャがTexture2Dではありません。" }, { "Opponents with Safety option turned on will see degraded textures instead of noise.", "Safetyオプションをオンにした相手には、ノイズの代わりに劣化したテクスチャが表示されます。"}, - { "Default fallback texture", "デフォルトのフォールバックテクスチャ" } + { "Default fallback texture", "デフォルトのフォールバックテクスチャ" }, + { "Under development.", "開発中です。" } }; languageMap.Add("kor", koreanStrings); diff --git a/Runtime/Scripts/lang/LanguageManager.cs.meta b/Runtime/Scripts/Localization/LanguageManager.cs.meta similarity index 100% rename from Runtime/Scripts/lang/LanguageManager.cs.meta rename to Runtime/Scripts/Localization/LanguageManager.cs.meta diff --git a/Runtime/Scripts/NDMF/ShellProtectorNDMFPlugin.cs b/Runtime/Scripts/NDMF/NdmfPlugin.cs similarity index 82% rename from Runtime/Scripts/NDMF/ShellProtectorNDMFPlugin.cs rename to Runtime/Scripts/NDMF/NdmfPlugin.cs index ded8572..1078c50 100644 --- a/Runtime/Scripts/NDMF/ShellProtectorNDMFPlugin.cs +++ b/Runtime/Scripts/NDMF/NdmfPlugin.cs @@ -4,9 +4,9 @@ using Shell.Protector; using UnityEngine; -[assembly: ExportsPlugin(typeof(ShellProtectorNDMFPlugin))] +[assembly: ExportsPlugin(typeof(NdmfPlugin))] -public class ShellProtectorNDMFPlugin : Plugin +public class NdmfPlugin : Plugin { protected override void Configure() { @@ -21,7 +21,6 @@ protected override void Configure() shellProtector.Encrypt(isModular: true); shellProtector.ReplaceMaterials(ctx.AvatarRootObject); shellProtector.RemoveDuplicatedTextures(ctx.AvatarRootObject); - shellProtector.SetMaterialFallbackValue(ctx.AvatarRootObject, true); shellProtector.SetAnimations(ctx.AvatarRootObject, false); shellProtector.ObfuscateBlendShape(ctx.AvatarRootObject, false); @@ -32,4 +31,4 @@ protected override void Configure() } } #endif -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/NDMF/ShellProtectorNDMFPlugin.cs.meta b/Runtime/Scripts/NDMF/NdmfPlugin.cs.meta similarity index 100% rename from Runtime/Scripts/NDMF/ShellProtectorNDMFPlugin.cs.meta rename to Runtime/Scripts/NDMF/NdmfPlugin.cs.meta diff --git a/Runtime/Scripts/Obfuscator.cs b/Runtime/Scripts/Obfuscator.cs index a5e1d5c..7122218 100644 --- a/Runtime/Scripts/Obfuscator.cs +++ b/Runtime/Scripts/Obfuscator.cs @@ -1,28 +1,29 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using UnityEditor; using UnityEditor.Animations; using UnityEngine; +using UnityEngine.Serialization; using VRC.SDK3.Avatars.Components; namespace Shell.Protector { public class Obfuscator : ScriptableObject { - string animDir = ""; + OutputPaths outputPaths; + AssetWriter assetWriter; List obfuscatedBlendShapeIndex = new List(); Dictionary obfuscatedBlendShapeNames = new Dictionary(); // before, after Dictionary obfuscatedClip = new Dictionary(); // before, after HashSet mmdShapes = new HashSet(); - readonly Regex re = new Regex(".*?/(.*)"); - - public bool clone = true; - public bool bPreserveMMD = true; + [FormerlySerializedAs("clone")] + public bool Clone = true; + [FormerlySerializedAs("bPreserveMMD")] + public bool PreserveMmd = true; public Obfuscator() { @@ -45,13 +46,28 @@ public Obfuscator() } public void Clean() { - clone = true; - animDir = ""; + Clone = true; + outputPaths = null; + assetWriter = null; obfuscatedBlendShapeNames.Clear(); obfuscatedBlendShapeIndex.Clear(); + obfuscatedClip.Clear(); + } + + static string GetAnimationPath(Transform transform) + { + var names = new List(); + Transform current = transform; + while (current != null && current.parent != null) + { + names.Add(current.name); + current = current.parent; + } + names.Reverse(); + return string.Join("/", names); } - public Mesh ObfuscateBlendShapeMesh(Mesh mesh, string newPath) + public Mesh ObfuscateBlendShapeMesh(Mesh mesh, OutputPaths paths, AssetWriter writer) { Mesh obfuscatedMesh = Instantiate(mesh); obfuscatedMesh.ClearBlendShapes(); @@ -83,7 +99,7 @@ public Mesh ObfuscateBlendShapeMesh(Mesh mesh, string newPath) float weight = mesh.GetBlendShapeFrameWeight(shapeIndex, frameIndex); string blendShapeName = mesh.GetBlendShapeName(shapeIndex); - if (bPreserveMMD) + if (PreserveMmd) { if (mmdShapes.Contains(blendShapeName)) { @@ -109,7 +125,7 @@ public Mesh ObfuscateBlendShapeMesh(Mesh mesh, string newPath) } Debug.LogFormat("Obfuscator blendshapes : {0}", string.Join(", ", obfuscatedBlendShapeNames.Select(kv => $"{kv.Key}: {kv.Value}"))); - AssetDatabase.CreateAsset(obfuscatedMesh, Path.Combine(newPath, obfuscatedMesh.name + ".asset")); + writer.CreateAssetInFolder(obfuscatedMesh, paths.Folders.MeshGuid, paths.MeshAssetName(mesh)); AssetDatabase.Refresh(); return obfuscatedMesh; } @@ -124,7 +140,7 @@ public void ChangeObfuscatedBlendShapeInDescriptor(VRCAvatarDescriptor descripto for (int i = 0; i < descriptor.customEyeLookSettings.eyelidsBlendshapes.Length; i++) { int idx = descriptor.customEyeLookSettings.eyelidsBlendshapes[i]; - descriptor.customEyeLookSettings.eyelidsBlendshapes[i] = obfuscatedBlendShapeIndex.FindIndex(0, obfuscatedBlendShapeIndex.Count - 1, + descriptor.customEyeLookSettings.eyelidsBlendshapes[i] = obfuscatedBlendShapeIndex.FindIndex(0, obfuscatedBlendShapeIndex.Count, x => { return x == idx; @@ -134,12 +150,13 @@ public void ChangeObfuscatedBlendShapeInDescriptor(VRCAvatarDescriptor descripto } - public void ObfuscateBlendshapeInAnim(AnimatorController anim, GameObject obj, string newDir) + public void ObfuscateBlendshapeInAnim(AnimatorController anim, GameObject obj, OutputPaths paths, AssetWriter writer) { if (anim == null) return; - animDir = newDir; + outputPaths = paths; + assetWriter = writer; var layers = anim.layers; foreach (var layer in layers) @@ -210,8 +227,7 @@ public AnimationClip ChangeBlendShapeInClip(AnimationClip clip, GameObject obj) { if (binding.type == typeof(SkinnedMeshRenderer) && binding.propertyName.StartsWith("blendShape.")) { - Match m = re.Match(obj.transform.GetHierarchyPath()); - string hierarchyPath = m.Groups[1].Value; + string hierarchyPath = GetAnimationPath(obj.transform); if (binding.path != hierarchyPath) continue; @@ -230,14 +246,14 @@ public AnimationClip ChangeBlendShapeInClip(AnimationClip clip, GameObject obj) } AnimationClip newClip = clip; - if (clone) + if (Clone) { if (obfuscatedClip.ContainsKey(clip)) newClip = obfuscatedClip[clip]; else { newClip = Instantiate(clip); - AssetDatabase.CreateAsset(newClip, Path.Combine(animDir, clip.name + "_obfuscated.anim")); + assetWriter.CreateAssetInFolder(newClip, outputPaths.Folders.AnimGuid, outputPaths.AnimationClipName(clip, "_obfuscated")); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); obfuscatedClip.Add(clip, newClip); @@ -249,8 +265,7 @@ public AnimationClip ChangeBlendShapeInClip(AnimationClip clip, GameObject obj) { if (binding.type == typeof(SkinnedMeshRenderer) && binding.propertyName.StartsWith("blendShape.")) { - Match m = re.Match(obj.transform.GetHierarchyPath()); - string hierarchyPath = m.Groups[1].Value; + string hierarchyPath = GetAnimationPath(obj.transform); string blendShapeName = binding.propertyName.Substring("blendShape.".Length); @@ -287,4 +302,4 @@ public string GetOriginalBlendShapeName(string obfuscatedBlendShape) } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/ParameterManager.cs b/Runtime/Scripts/ParameterManager.cs index 6cc94d9..2004f37 100644 --- a/Runtime/Scripts/ParameterManager.cs +++ b/Runtime/Scripts/ParameterManager.cs @@ -1,122 +1,104 @@ #if UNITY_EDITOR -using System.Collections; using System.Collections.Generic; +using System.Linq; using UnityEngine; -using VRC.SDK3.Avatars; using VRC.SDK3.Avatars.ScriptableObjects; +namespace Shell.Protector +{ public static class ParameterManager { - public static VRCExpressionParameters.Parameter CloneParameter(VRCExpressionParameters.Parameter parameter) + private const string Prefix = "SHELL_PROTECTOR_"; + public static string GetSyncedKeyName(int index, bool bLegacy = false) { - var tmp = new VRCExpressionParameters.Parameter(); - tmp.saved = parameter.saved; - tmp.name = parameter.name; - tmp.networkSynced = parameter.networkSynced; - tmp.valueType = parameter.valueType; - tmp.defaultValue = parameter.defaultValue; - return tmp; + if (bLegacy) + return "pkey"; + return Prefix + "synced_key" + index; } - - public static VRCExpressionParameters AddKeyParameter(VRCExpressionParameters vrc_parameters, int key_length, bool optimize = false) + public static string GetKeyName(int index) => Prefix + "key" + index; + public static string GetSavedKeyName(int index) => Prefix + "saved_key" + index; + public static string GetSyncSwitchName(int index, bool bLegacy = false) { - VRCExpressionParameters result = new VRCExpressionParameters(); - result.name = vrc_parameters.name + "_encrypted"; - - var parameters = vrc_parameters.parameters; + if (bLegacy) + return "encrypt_switch" + index; + return Prefix + "sync_switch" + index; + } + public static string GetSyncLockName(bool bLegacy = false) + { + if (bLegacy) + return "encrypt_lock"; + return Prefix + "sync_lock"; + } + public static string GetIsLocalName() => "IsLocal"; - int switch_size = 0; - int etc = 0; - if (optimize == true) - { - etc = 2; //lock + pkey(sync) - switch(key_length) - { - case 4: - switch_size = 2; - break; - case 8: - switch_size = 3; - break; - case 12: - case 16: - switch_size = 4; - break; - default: - Debug.LogErrorFormat("ParameterManager: key_length = {} is wrong!", key_length); - return result; - } - } - VRCExpressionParameters.Parameter[] tmp = new VRCExpressionParameters.Parameter[parameters.Length + switch_size + key_length + etc]; - int idx; - for(idx = 0; idx < parameters.Length; ++idx) - tmp[idx] = CloneParameter(parameters[idx]); + public static VRCExpressionParameters AddKeyParameter(VRCExpressionParameters vrcParameters, int keyLength, int syncSize) + { + bool bLegacy = syncSize == 1; + var parameters = new List(); - if (optimize == false) + parameters.Add(new VRCExpressionParameters.Parameter { - for (int i = 0; i < key_length; ++i) - { - var para = new VRCExpressionParameters.Parameter - { - name = "pkey" + i, - saved = true, - networkSynced = true, - valueType = VRCExpressionParameters.ValueType.Float, - defaultValue = 0.0f - }; + name = GetSyncLockName(bLegacy), + saved = true, + networkSynced = true, + valueType = VRCExpressionParameters.ValueType.Bool, + defaultValue = 0.0f + }); - tmp[idx++] = para; - } - } - else + for (var i = 0; i < syncSize; i++) { - var pkey = new VRCExpressionParameters.Parameter + parameters.Add(new VRCExpressionParameters.Parameter { - name = "pkey", + name = GetSyncedKeyName(i, bLegacy), saved = true, networkSynced = true, valueType = VRCExpressionParameters.ValueType.Float, defaultValue = 0.0f - }; - tmp[idx++] = pkey; - for (int i = 0; i < key_length; ++i) - { - var para = new VRCExpressionParameters.Parameter - { - name = "pkey" + i, - saved = false, - networkSynced = false, - valueType = VRCExpressionParameters.ValueType.Float, - defaultValue = 0.0f - }; + }); + } - tmp[idx++] = para; - } - var plock = new VRCExpressionParameters.Parameter + for (var i = 0; i < ShellProtector.GetRequiredSwitchCount(keyLength, syncSize); ++i) + { + parameters.Add(new VRCExpressionParameters.Parameter { - name = "encrypt_lock", + name = GetSyncSwitchName(i, bLegacy), saved = true, networkSynced = true, valueType = VRCExpressionParameters.ValueType.Bool, defaultValue = 0.0f - }; - tmp[idx++] = plock; - for (int i = 0; i < switch_size; ++i) + }); + } + + for (var i = 0; i < keyLength; ++i) + { + parameters.Add(new VRCExpressionParameters.Parameter + { + name = GetKeyName(i), + saved = false, + networkSynced = false, + valueType = VRCExpressionParameters.ValueType.Float, + defaultValue = 0.0f + }); + + if (!bLegacy) { - var para = new VRCExpressionParameters.Parameter + parameters.Add(new VRCExpressionParameters.Parameter { - name = "encrypt_switch" + i, + name = GetSavedKeyName(i), saved = true, - networkSynced = true, - valueType = VRCExpressionParameters.ValueType.Bool, + networkSynced = false, + valueType = VRCExpressionParameters.ValueType.Float, defaultValue = 0.0f - }; - tmp[idx++] = para; + }); } } - result.parameters = tmp; + + var result = ScriptableObject.CreateInstance(); + result.name = vrcParameters.name + "_encrypted"; + result.parameters = vrcParameters.parameters.Concat(parameters).ToArray();; return result; } } -#endif \ No newline at end of file +} +#endif diff --git a/Runtime/Scripts/Pipeline.meta b/Runtime/Scripts/Pipeline.meta new file mode 100644 index 0000000..7e02195 --- /dev/null +++ b/Runtime/Scripts/Pipeline.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a7c3a9f0f2e44e2ba4762f4929f4c221 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Pipeline/AssetWriter.cs b/Runtime/Scripts/Pipeline/AssetWriter.cs new file mode 100644 index 0000000..3ca5556 --- /dev/null +++ b/Runtime/Scripts/Pipeline/AssetWriter.cs @@ -0,0 +1,105 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +namespace Shell.Protector +{ + public interface IAssetWriter + { + void CreateAsset(Object asset, string path); + bool CopyAsset(string sourcePath, string targetPath); + T LoadAssetAtPath(string path) where T : Object; + bool IsValidFolder(string path); + void CreateFolder(string parentFolder, string newFolderName); + void DeleteAsset(string path); + void SaveAssets(); + void Refresh(); + void SaveAndRefresh(); + string EnsureFolderAndGetGuid(string path); + string ResolveFolderPath(string guid); + string UniquePathInFolder(string folderGuid, string fileName); + void CreateAssetInFolder(Object asset, string folderGuid, string fileName); + bool CopyAssetToFolder(string sourcePath, string folderGuid, string fileName, out string targetPath); + } + + public sealed class AssetWriter : IAssetWriter + { + public void CreateAsset(Object asset, string path) => AssetDatabase.CreateAsset(asset, path); + public bool CopyAsset(string sourcePath, string targetPath) => AssetDatabase.CopyAsset(sourcePath, targetPath); + public T LoadAssetAtPath(string path) where T : Object => AssetDatabase.LoadAssetAtPath(path); + public bool IsValidFolder(string path) => AssetDatabase.IsValidFolder(path); + public void CreateFolder(string parentFolder, string newFolderName) => AssetDatabase.CreateFolder(parentFolder, newFolderName); + public void DeleteAsset(string path) => AssetDatabase.DeleteAsset(path); + public void SaveAssets() => AssetDatabase.SaveAssets(); + public void Refresh() => AssetDatabase.Refresh(); + + public void SaveAndRefresh() + { + SaveAssets(); + Refresh(); + } + + public string UniquePath(string path) => AssetDatabase.GenerateUniqueAssetPath(OutputPaths.Normalize(path)); + + public string EnsureFolderAndGetGuid(string path) + { + string normalized = OutputPaths.Normalize(path).TrimEnd('/'); + EnsureFolder(normalized); + + string guid = AssetDatabase.AssetPathToGUID(normalized); + if (string.IsNullOrEmpty(guid)) + { + SaveAndRefresh(); + guid = AssetDatabase.AssetPathToGUID(normalized); + } + + if (string.IsNullOrEmpty(guid)) + throw new System.InvalidOperationException("Failed to resolve folder GUID: " + normalized); + + return guid; + } + + public string ResolveFolderPath(string guid) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrEmpty(path) || !AssetDatabase.IsValidFolder(path)) + throw new System.InvalidOperationException("Failed to resolve folder path from GUID: " + guid); + return OutputPaths.Normalize(path); + } + + public string UniquePathInFolder(string folderGuid, string fileName) + { + string folder = ResolveFolderPath(folderGuid); + return AssetDatabase.GenerateUniqueAssetPath(OutputPaths.Combine(folder, OutputPaths.Sanitize(fileName))); + } + + public void CreateAssetInFolder(Object asset, string folderGuid, string fileName) + { + CreateAsset(asset, UniquePathInFolder(folderGuid, fileName)); + } + + public bool CopyAssetToFolder(string sourcePath, string folderGuid, string fileName, out string targetPath) + { + targetPath = UniquePathInFolder(folderGuid, fileName); + return CopyAsset(sourcePath, targetPath); + } + + public void EnsureFolder(string path) + { + string normalized = OutputPaths.Normalize(path).TrimEnd('/'); + string[] parts = normalized.Split('/'); + if (parts.Length == 0 || parts[0] != "Assets") + return; + + string current = "Assets"; + for (int i = 1; i < parts.Length; i++) + { + string next = current + "/" + parts[i]; + if (!AssetDatabase.IsValidFolder(next)) + AssetDatabase.CreateFolder(current, parts[i]); + current = next; + } + } + } +} +#endif diff --git a/Runtime/Scripts/Pipeline/AssetWriter.cs.meta b/Runtime/Scripts/Pipeline/AssetWriter.cs.meta new file mode 100644 index 0000000..13fbd3e --- /dev/null +++ b/Runtime/Scripts/Pipeline/AssetWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 032df40621b84d208e71e152d75ac1e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Pipeline/AvatarProcessor.cs b/Runtime/Scripts/Pipeline/AvatarProcessor.cs new file mode 100644 index 0000000..54a6084 --- /dev/null +++ b/Runtime/Scripts/Pipeline/AvatarProcessor.cs @@ -0,0 +1,41 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using UnityEngine; + +namespace Shell.Protector +{ + internal static class AvatarProcessor + { + public static void ReplaceMaterials(GameObject avatar, BuildResult result) + { + ReplaceRendererMaterials(avatar.GetComponentsInChildren(true), result); + ReplaceRendererMaterials(avatar.GetComponentsInChildren(true), result); + } + + static void ReplaceRendererMaterials(IEnumerable renderers, BuildResult result) where T : Renderer + { + foreach (T renderer in renderers) + { + Material[] materials = renderer.sharedMaterials; + if (materials == null) + continue; + + bool changed = false; + for (int i = 0; i < materials.Length; ++i) + { + Material material = materials[i]; + if (material == null || !result.EncryptedMaterials.ContainsKey(material)) + continue; + + materials[i] = result.EncryptedMaterials[material]; + result.Meshes.Add(renderer.gameObject); + changed = true; + } + + if (changed) + renderer.sharedMaterials = materials; + } + } + } +} +#endif diff --git a/Runtime/Scripts/Pipeline/AvatarProcessor.cs.meta b/Runtime/Scripts/Pipeline/AvatarProcessor.cs.meta new file mode 100644 index 0000000..ac9c15e --- /dev/null +++ b/Runtime/Scripts/Pipeline/AvatarProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3b131f08a0e41bd8be4bb2421140d82 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Pipeline/BuildRequest.cs b/Runtime/Scripts/Pipeline/BuildRequest.cs new file mode 100644 index 0000000..4d5bfc3 --- /dev/null +++ b/Runtime/Scripts/Pipeline/BuildRequest.cs @@ -0,0 +1,22 @@ +#if UNITY_EDITOR +using VRC.SDK3.Avatars.Components; + +namespace Shell.Protector +{ + public sealed class BuildRequest + { + public BuildRequest(ShellProtector owner, VRCAvatarDescriptor descriptor, bool useSmallMipTexture, bool isModular) + { + Owner = owner; + Descriptor = descriptor; + UseSmallMipTexture = useSmallMipTexture; + IsModular = isModular; + } + + public ShellProtector Owner { get; } + public VRCAvatarDescriptor Descriptor { get; } + public bool UseSmallMipTexture { get; } + public bool IsModular { get; } + } +} +#endif diff --git a/Runtime/Scripts/Pipeline/BuildRequest.cs.meta b/Runtime/Scripts/Pipeline/BuildRequest.cs.meta new file mode 100644 index 0000000..9e17c37 --- /dev/null +++ b/Runtime/Scripts/Pipeline/BuildRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2750924c1964b17afaf3457d6e4f56a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Pipeline/BuildResult.cs b/Runtime/Scripts/Pipeline/BuildResult.cs new file mode 100644 index 0000000..62138f3 --- /dev/null +++ b/Runtime/Scripts/Pipeline/BuildResult.cs @@ -0,0 +1,43 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using UnityEngine; + +namespace Shell.Protector +{ + public struct ProcessedTexture + { + public EncryptResult Encrypted; + public List Fallbacks; + public List FallbackOptions; + public byte[] Nonce; + } + + public struct AuxiliaryTextures + { + public Texture2D LimTexture; + public Texture2D LimTexture2; + public Texture2D OutlineTexture; + public Texture2D LimShadeTexture; + } + + public sealed class BuildResult + { + public GameObject Avatar { get; set; } + public string AvatarDir { get; set; } + public byte[] KeyBytes { get; set; } + public HashSet Meshes { get; } = new HashSet(); + public Dictionary EncryptedMaterials { get; } = new Dictionary(); + public Dictionary ProcessedTextures { get; } = new Dictionary(); + + public void Clear() + { + Avatar = null; + AvatarDir = null; + KeyBytes = null; + Meshes.Clear(); + EncryptedMaterials.Clear(); + ProcessedTextures.Clear(); + } + } +} +#endif diff --git a/Runtime/Scripts/Pipeline/BuildResult.cs.meta b/Runtime/Scripts/Pipeline/BuildResult.cs.meta new file mode 100644 index 0000000..0ae4c26 --- /dev/null +++ b/Runtime/Scripts/Pipeline/BuildResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ee7d88ba50648469ab6ad1df9c25381 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Pipeline/BuildSettings.cs b/Runtime/Scripts/Pipeline/BuildSettings.cs new file mode 100644 index 0000000..54e23b1 --- /dev/null +++ b/Runtime/Scripts/Pipeline/BuildSettings.cs @@ -0,0 +1,23 @@ +#if UNITY_EDITOR +namespace Shell.Protector +{ + public sealed class BuildSettings + { + public string AssetDir { get; set; } + public string FixedPassword { get; set; } + public string UserPassword { get; set; } + public string Language { get; set; } + public int LanguageIndex { get; set; } + public uint Rounds { get; set; } + public int Filter { get; set; } + public int Fallback { get; set; } + public int Algorithm { get; set; } + public int KeySize { get; set; } + public int SyncSize { get; set; } + public bool DeleteFolders { get; set; } + public bool UseSmallMipTexture { get; set; } + public bool PreserveMMD { get; set; } + public bool TurnOnAllSafetyFallback { get; set; } + } +} +#endif diff --git a/Runtime/Scripts/Pipeline/BuildSettings.cs.meta b/Runtime/Scripts/Pipeline/BuildSettings.cs.meta new file mode 100644 index 0000000..b6b8b41 --- /dev/null +++ b/Runtime/Scripts/Pipeline/BuildSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b1aef04344d846ce8aa6f936291351af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Pipeline/MaterialEncryptor.cs b/Runtime/Scripts/Pipeline/MaterialEncryptor.cs new file mode 100644 index 0000000..b138787 --- /dev/null +++ b/Runtime/Scripts/Pipeline/MaterialEncryptor.cs @@ -0,0 +1,74 @@ +#if UNITY_EDITOR +using UnityEngine; + +namespace Shell.Protector +{ + internal sealed class MaterialEncryptor + { + readonly AssetWriter writer; + readonly bool turnOnAllSafetyFallback; + readonly int algorithm; + readonly uint rounds; + + public MaterialEncryptor(AssetWriter writer, bool turnOnAllSafetyFallback, int algorithm, uint rounds) + { + this.writer = writer; + this.turnOnAllSafetyFallback = turnOnAllSafetyFallback; + this.algorithm = algorithm; + this.rounds = rounds; + } + + public Material CreateEncryptedMaterial(string folderGuid, string fileName, Material source, Shader shader, Texture2D fallback, Texture2D mip, AuxiliaryTextures auxiliary, ProcessedTexture texture, byte[] keyBytes, int fixedKeySize, IEncryptor encryptor, Injector injector) + { + Material result = new Material(source.shader); + result.CopyPropertiesFromMaterial(source); + result.shader = shader; + var originalTex = (Texture2D)result.mainTexture; + result.mainTexture = fallback; + + if (texture.Encrypted.Texture1 != null) + result.SetTexture(ShaderProperties.EncryptTexture0, texture.Encrypted.Texture1); + if (texture.Encrypted.Texture2 != null) + result.SetTexture(ShaderProperties.EncryptTexture1, texture.Encrypted.Texture2); + + result.SetTexture(ShaderProperties.MipTexture, mip); + result.renderQueue = source.renderQueue; + if (turnOnAllSafetyFallback) + result.SetOverrideTag("VRCFallback", "Unlit"); + + var (widthOffset, heightOffset) = TextureEncryptManager.CalculateOffsets(originalTex); + result.SetInteger(ShaderProperties.WidthOffset, widthOffset); + result.SetInteger(ShaderProperties.HeightOffset, heightOffset); + for (int i = 0; i < fixedKeySize; ++i) + result.SetFloat(ShaderProperties.KeyPrefix + i, keyBytes[i]); + + if (algorithm == (int)ShellProtectorAlgorithm.Chacha) + { + Chacha20 chacha = encryptor as Chacha20; + result.SetInteger(ShaderProperties.Nonce0, (int)chacha.GetNonceUint3()[0]); + result.SetInteger(ShaderProperties.Nonce1, (int)chacha.GetNonceUint3()[1]); + result.SetInteger(ShaderProperties.Nonce2, (int)chacha.GetNonceUint3()[2]); + } + else if (algorithm == (int)ShellProtectorAlgorithm.XXTEA) + { + result.SetInteger(ShaderProperties.Rounds, (int)rounds); + } + + var key = new byte[16]; + for (int i = 0; i < 16; i++) + key[i] = keyBytes[i]; + + uint hashMagic = (uint)source.GetInstanceID(); + var hash = KeyGenerator.SimpleHash(key, hashMagic); + result.SetInteger(ShaderProperties.HashMagic, (int)hashMagic); + result.SetInteger(ShaderProperties.PasswordHash, (int)hash); + + injector.SetKeywords(result, auxiliary.LimTexture != null); + TextureEncryptManager.SetFormatKeywords(result, originalTex); + writer.CreateAssetInFolder(result, folderGuid, fileName); + writer.SaveAndRefresh(); + return result; + } + } +} +#endif diff --git a/Runtime/Scripts/Pipeline/MaterialEncryptor.cs.meta b/Runtime/Scripts/Pipeline/MaterialEncryptor.cs.meta new file mode 100644 index 0000000..78afe5e --- /dev/null +++ b/Runtime/Scripts/Pipeline/MaterialEncryptor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b77120fbe64f4d67b9352e318588ee57 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Pipeline/OutputPaths.cs b/Runtime/Scripts/Pipeline/OutputPaths.cs new file mode 100644 index 0000000..8b3c715 --- /dev/null +++ b/Runtime/Scripts/Pipeline/OutputPaths.cs @@ -0,0 +1,152 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace Shell.Protector +{ + public sealed class OutputPaths + { + public const string TexFolder = "Tex"; + public const string MatFolder = "Mat"; + public const string ShaderFolder = "Shader"; + public const string AnimFolder = "Anim"; + public const string MeshFolder = "Mesh"; + + readonly Dictionary shaderFolderGuids = new Dictionary(); + + public OutputPaths(string root, GameObject avatar) + { + Root = Normalize(root); + AvatarName = Sanitize(avatar != null ? avatar.name : "Avatar"); + Avatar = Combine(Root, AvatarName); + Tex = Combine(Avatar, TexFolder); + Mat = Combine(Avatar, MatFolder); + Shader = Combine(Avatar, ShaderFolder); + Anim = Combine(Avatar, AnimFolder); + Mesh = Combine(Avatar, MeshFolder); + } + + public string Root { get; } + public string AvatarName { get; } + public string Avatar { get; } + public string Tex { get; } + public string Mat { get; } + public string Shader { get; } + public string Anim { get; } + public string Mesh { get; } + public OutputFolders Folders { get; private set; } + + public string MipTexture(int size) => Combine(Tex, "mip_" + size + ".asset"); + public string MipTextureName(int size) => "mip_" + size + ".asset"; + public string EncryptedTexture(Texture2D texture, int index) => Combine(Tex, BaseName(texture) + "_encrypt" + (index == 0 ? "" : index.ToString()) + ".asset"); + public string EncryptedTextureName(Texture2D texture, int index) => BaseName(texture) + "_encrypt" + (index == 0 ? "" : index.ToString()) + ".asset"; + public string FallbackTexture(Texture2D texture) => Combine(Tex, BaseName(texture) + "_fallback.asset"); + public string FallbackTextureName(Texture2D texture) => BaseName(texture) + "_fallback.asset"; + public string EncryptedMaterial(Material material) => Combine(Mat, BaseName(material) + "_encrypted.mat"); + public string EncryptedMaterialName(Material material) => BaseName(material) + "_encrypted.mat"; + public string DuplicatedMaterial(Material material) => Combine(Mat, BaseName(material) + "_duplicated.mat"); + public string DuplicatedMaterialName(Material material) => BaseName(material) + "_duplicated.mat"; + public string ShaderDirectory(Material material) => Combine(Shader, BaseName(material)); + public string Parameters(string name) => Combine(Avatar, Sanitize(name) + ".asset"); + public string ParametersName(string name) => Sanitize(name) + ".asset"; + public string Controller(RuntimeAnimatorController controller) => Combine(Anim, BaseName(controller) + "_encrypted.controller"); + public string ControllerName(RuntimeAnimatorController controller) => BaseName(controller) + "_encrypted.controller"; + public string AnimationClip(AnimationClip clip, string suffix) => Combine(Anim, BaseName(clip) + suffix + ".anim"); + public string AnimationClipName(AnimationClip clip, string suffix) => BaseName(clip) + suffix + ".anim"; + public string MeshAsset(Mesh mesh) => Combine(Mesh, BaseName(mesh) + ".asset"); + public string MeshAssetName(Mesh mesh) => BaseName(mesh) + ".asset"; + public string History() => Combine(Root, "EncryptedHistory.asset"); + public string HistoryName() => "EncryptedHistory.asset"; + + public OutputFolders PrepareFolders(AssetWriter writer, bool deleteExistingChildren) + { + string rootGuid = writer.EnsureFolderAndGetGuid(Root); + string avatarGuid = writer.EnsureFolderAndGetGuid(Avatar); + + if (deleteExistingChildren) + { + writer.DeleteAsset(Anim); + writer.DeleteAsset(Mat); + writer.DeleteAsset(Shader); + writer.DeleteAsset(Tex); + writer.DeleteAsset(Mesh); + writer.SaveAndRefresh(); + } + + Folders = new OutputFolders( + rootGuid, + avatarGuid, + writer.EnsureFolderAndGetGuid(Tex), + writer.EnsureFolderAndGetGuid(Mat), + writer.EnsureFolderAndGetGuid(Shader), + writer.EnsureFolderAndGetGuid(Anim), + writer.EnsureFolderAndGetGuid(Mesh) + ); + writer.SaveAndRefresh(); + shaderFolderGuids.Clear(); + return Folders; + } + + public string EnsureShaderFolder(AssetWriter writer, Material material) + { + if (material != null && shaderFolderGuids.TryGetValue(material, out string guid)) + return guid; + + string folderGuid = writer.EnsureFolderAndGetGuid(ShaderDirectory(material)); + if (material != null) + shaderFolderGuids[material] = folderGuid; + return folderGuid; + } + + public static string Combine(params string[] parts) + { + return Normalize(Path.Combine(parts)); + } + + public static string Normalize(string path) + { + return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', '/'); + } + + public static string Sanitize(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return "Asset"; + + string cleaned = Regex.Replace(name.Trim(), @"[^\w\- .가-힣ぁ-んァ-ヶ一-龠]", "_"); + cleaned = Regex.Replace(cleaned, @"\s+", "_"); + cleaned = cleaned.Trim('.', '_'); + return string.IsNullOrEmpty(cleaned) ? "Asset" : cleaned; + } + + static string BaseName(Object asset) + { + return Sanitize(asset != null ? asset.name : "Asset"); + } + } + + public sealed class OutputFolders + { + public OutputFolders(string rootGuid, string avatarGuid, string texGuid, string matGuid, string shaderGuid, string animGuid, string meshGuid) + { + RootGuid = rootGuid; + AvatarGuid = avatarGuid; + TexGuid = texGuid; + MatGuid = matGuid; + ShaderGuid = shaderGuid; + AnimGuid = animGuid; + MeshGuid = meshGuid; + } + + public string RootGuid { get; } + public string AvatarGuid { get; } + public string TexGuid { get; } + public string MatGuid { get; } + public string ShaderGuid { get; } + public string AnimGuid { get; } + public string MeshGuid { get; } + } +} +#endif diff --git a/Runtime/Scripts/Pipeline/OutputPaths.cs.meta b/Runtime/Scripts/Pipeline/OutputPaths.cs.meta new file mode 100644 index 0000000..d61e90a --- /dev/null +++ b/Runtime/Scripts/Pipeline/OutputPaths.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e1e52fb9784c4c34991285ea9885af75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Pipeline/Pipeline.cs b/Runtime/Scripts/Pipeline/Pipeline.cs new file mode 100644 index 0000000..9085027 --- /dev/null +++ b/Runtime/Scripts/Pipeline/Pipeline.cs @@ -0,0 +1,22 @@ +#if UNITY_EDITOR +using UnityEngine; + +namespace Shell.Protector +{ + public sealed class Pipeline + { + public BuildResult Encrypt(BuildRequest request, BuildSettings settings) + { + if (request == null || request.Owner == null) + return new BuildResult(); + + request.Owner.ApplySettings(settings); + GameObject avatar = request.Owner.EncryptLegacy(request.UseSmallMipTexture, request.IsModular); + + BuildResult result = request.Owner.CurrentBuildResult; + result.Avatar = avatar; + return result; + } + } +} +#endif diff --git a/Runtime/Scripts/Pipeline/Pipeline.cs.meta b/Runtime/Scripts/Pipeline/Pipeline.cs.meta new file mode 100644 index 0000000..f594feb --- /dev/null +++ b/Runtime/Scripts/Pipeline/Pipeline.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c42b6cf9a14f42429cdf2548e56669d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/ShellProtector.cs b/Runtime/Scripts/ShellProtector.cs index 53896d4..48b37d3 100644 --- a/Runtime/Scripts/ShellProtector.cs +++ b/Runtime/Scripts/ShellProtector.cs @@ -1,129 +1,187 @@ #if UNITY_EDITOR using System; -using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; -using UnityEditor; using UnityEngine; -using VRC.SDK3.Avatars.ScriptableObjects; +using UnityEditor.Animations; +using UnityEditor; +using UnityEngine.Serialization; using VRC.SDK3.Avatars.Components; -using System.Linq; +using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDKBase; -using UnityEditor.Animations; #if MODULAR using nadena.dev.modular_avatar.core; #endif -#if POIYOMI -using Thry; -#elif POIYOMI91 -using Thry.ThryEditor; -#endif - namespace Shell.Protector { public class ShellProtector : MonoBehaviour, IEditorOnly { + const string LegacyOutputDir = "Assets/ShellProtect"; + const string WrongRuntimeOutputDir = "Assets/ShellProtector/Runtime"; + const string DefaultOutputDir = "Assets/ShellProtector/Generated"; + + [FormerlySerializedAs("gameobjectList")] [SerializeField] - List gameobject_list = new List(); - [SerializeField] - List material_list = new List(); + List _gameObjectList = new List(); + [FormerlySerializedAs("materialList")] [SerializeField] - List texture_list = new List(); + List _materialList = new List(); + [FormerlySerializedAs("obfuscationRenderers")] [SerializeField] - List obfuscationRenderers = new List(); + List _obfuscationRenderers = new List(); - EncryptTexture encrypt = new EncryptTexture(); - Injector injector; - AssetManager shader_manager = AssetManager.GetInstance(); - bool init = false; + Injector _injector; + readonly AssetManager _shaderManager = AssetManager.GetInstance(); + readonly AssetWriter _assetWriter = new AssetWriter(); + bool _initialized; + string _packageAssetDir; + OutputPaths _outputPaths; enum Algorithm { - xxtea = 0, - chacha = 1 + Xxtea = 0, + Chacha = 1 } - public string asset_dir = "Assets/ShellProtect"; - public string pwd = "password"; // fixed password - public string pwd2 = "pass"; // user password - public int lang_idx = 0; - public string lang = "kor"; - public VRCAvatarDescriptor descriptor; + [FormerlySerializedAs("assetDir")] + [SerializeField] string _assetDir = DefaultOutputDir; + [FormerlySerializedAs("pwd")] + [SerializeField] string _fixedPassword = "password"; + [FormerlySerializedAs("pwd2")] + [SerializeField] string _userPassword = "pass"; + [FormerlySerializedAs("langIdx")] + [SerializeField] int _languageIndex; + [FormerlySerializedAs("lang")] + [SerializeField] string _language = "kor"; + [FormerlySerializedAs("descriptor")] + [SerializeField] VRCAvatarDescriptor _descriptor; + + public string AssetDir { get => _assetDir; set => _assetDir = value; } + public string FixedPassword { get => _fixedPassword; set => _fixedPassword = value; } + public string UserPassword { get => _userPassword; set => _userPassword = value; } + public int LanguageIndex { get => _languageIndex; set => _languageIndex = value; } + public string Language { get => _language; set => _language = value; } + public VRCAvatarDescriptor Descriptor { get => _descriptor; set => _descriptor = value; } [Serializable] public class MatOption { - public bool active = true; - public int filter = -1; - public int fallback = -1; + [FormerlySerializedAs("active")] + public bool Active = true; + [FormerlySerializedAs("filter")] + public int Filter = -1; + [FormerlySerializedAs("fallback")] + public int Fallback = -1; + [FormerlySerializedAs("emissionEnc")] + public bool EmissionEnc; } [Serializable] public class MaterialOptionPair { - public Material material; - public MatOption option; + [FormerlySerializedAs("material")] + public Material Material; + [FormerlySerializedAs("option")] + public MatOption Option; } + [FormerlySerializedAs("matOptionSaved")] [SerializeField] - List matOptionSaved = new List(); - public Dictionary matOptions = new Dictionary(); - - EncryptedHistory history; - - struct ProcessedTexture + List _matOptionSaved = new List(); + public Dictionary MaterialOptions = new Dictionary(); + + EncryptedHistory _history; + + BuildResult _buildResult = new BuildResult(); + HashSet Meshes => _buildResult.Meshes; + Dictionary EncryptedMaterials => _buildResult.EncryptedMaterials; + Dictionary ProcessedTextures => _buildResult.ProcessedTextures; + + [FormerlySerializedAs("rounds")] + [SerializeField] uint _rounds = 20; + [FormerlySerializedAs("filter")] + [SerializeField] int _filter = 1; + [FormerlySerializedAs("fallback")] + [SerializeField] int _fallback = 5; + [FormerlySerializedAs("algorithm")] + [SerializeField] int _algorithm = 1; +#pragma warning disable CS0414 + [FormerlySerializedAs("keySizeIdx")] + [SerializeField] int _keySizeIndex = 3; +#pragma warning restore CS0414 + [FormerlySerializedAs("keySize")] + [SerializeField] int _keySize = 12; + [FormerlySerializedAs("syncSize")] + [SerializeField] int _syncSize = 1; + [FormerlySerializedAs("deleteFolders")] + [SerializeField] bool _deleteFolders = true; + [FormerlySerializedAs("bUseSmallMipTexture")] + [SerializeField] bool _useSmallMipTexture = true; + + [FormerlySerializedAs("bPreserveMMD")] + [SerializeField] bool _preserveMmd = true; + + [FormerlySerializedAs("turnOnAllSafetyFallback")] + [SerializeField] bool _turnOnAllSafetyFallback = true; + + public static readonly string[] FilterStrings = new string[2] { "Point", "Bilinear" }; + public static readonly string[] FallbackStrings = new string[8] { "white", "black", "4x4", "8x8", "16x16", "32x32", "64x64", "128x128" }; + + Texture2D _fallbackWhite; + Texture2D _fallbackBlack; + + string GetPackageAssetDir() { - public Texture2D encrypted0; - public Texture2D encrypted1; - public List fallbacks; - public List fallbackOptions; - public byte[] nonce; + if (!string.IsNullOrEmpty(_packageAssetDir)) + return _packageAssetDir; + + MonoScript monoScript = MonoScript.FromMonoBehaviour(this); + string scriptPath = AssetDatabase.GetAssetPath(monoScript); + _packageAssetDir = OutputPaths.Normalize(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(scriptPath)))); + return _packageAssetDir; } - struct OtherTextures + + string GetRuntimeAssetDir() { - public Texture2D limTexture; - public Texture2D limTexture2; - public Texture2D outlineTexture; - public Texture2D limShadeTexture; + return OutputPaths.Combine(GetPackageAssetDir(), "Runtime"); } - //Must clear them before start encrypting// - HashSet meshes = new HashSet(); - Dictionary encryptedMaterials = new Dictionary(); // original, encrypted - Dictionary processedTextures = new Dictionary(); - ////////////////////////////////// - - [SerializeField] uint rounds = 20; - [SerializeField] int filter = 1; - [SerializeField] int fallback = 5; - [SerializeField] int algorithm = 1; - [SerializeField] int key_size_idx = 3; - [SerializeField] int key_size = 12; - [SerializeField] float animation_speed = 5.0f; - [SerializeField] bool delete_folders = true; - [SerializeField] bool parameter_multiplexing = false; - [SerializeField] bool bUseSmallMipTexture = true; - - [SerializeField] bool bPreserveMMD = true; + string ResolveOutputAssetDir() + { + string normalized = OutputPaths.Normalize(_assetDir).TrimEnd('/'); + if (string.IsNullOrEmpty(normalized) || normalized == LegacyOutputDir || normalized == WrongRuntimeOutputDir || normalized == GetRuntimeAssetDir()) + normalized = DefaultOutputDir; - [SerializeField] float fallbackTime = 5.0f; - [SerializeField] bool turnOnAllSafetyFallback = true; + _assetDir = normalized; + return _assetDir; + } - public static readonly string[] filterStrings = new string[2] { "Point", "Bilinear" }; - public static readonly string[] fallbackStrings = new string[8] { "white", "black", "4x4", "8x8", "16x16", "32x32", "64x64", "128x128" }; + OutputPaths GetOutputPaths() + { + if (_outputPaths == null) + _outputPaths = new OutputPaths(_assetDir, _descriptor != null ? _descriptor.gameObject : null); + return _outputPaths; + } - Texture2D fallbackWhite = null; - Texture2D fallbackBlack = null; + OutputPaths EnsureOutputFolders() + { + _assetDir = ResolveOutputAssetDir(); + OutputPaths paths = GetOutputPaths(); + if (paths.Folders == null) + paths.PrepareFolders(_assetWriter, false); + return paths; + } public void Init() { - if (init) + if (_initialized) return; - HashSet rednererSet = new HashSet(); - Transform child = descriptor.transform.Find("Body"); + HashSet rendererSet = new HashSet(); + Transform child = _descriptor.transform.Find("Body"); if (child != null) { SkinnedMeshRenderer renderer = child.GetComponent(); @@ -132,218 +190,146 @@ public void Init() Mesh mesh = renderer.sharedMesh; if (mesh != null) { - rednererSet.Add(renderer); + rendererSet.Add(renderer); } } } - foreach (var renderer in rednererSet) + foreach (var renderer in rendererSet) { - obfuscationRenderers.Add(renderer); + _obfuscationRenderers.Add(renderer); } - init = true; + _initialized = true; } public void SyncMatOption() { - foreach(var pair in matOptionSaved) + foreach (var pair in _matOptionSaved) { - if (pair.material != null) - matOptions[pair.material] = pair.option; + if (pair.Material != null) + MaterialOptions[pair.Material] = pair.Option; } } public void SaveMatOption() { - foreach(var pair in matOptions) + foreach (var pair in MaterialOptions) { - matOptionSaved.Add(new MaterialOptionPair { material = pair.Key, option = pair.Value }); + _matOptionSaved.Add(new MaterialOptionPair { Material = pair.Key, Option = pair.Value }); } } public byte[] GetKeyBytes() { - return KeyGenerator.MakeKeyBytes(pwd, pwd2, key_size); - } - public EncryptTexture GetEncryptTexture() - { - return encrypt; + return KeyGenerator.MakeKeyBytes(_fixedPassword, _userPassword, _keySize); } + public GameObject DuplicateAvatar(GameObject avatar) { GameObject cpy = Instantiate(avatar); - if(!avatar.name.Contains("_encrypted")) + if (!avatar.name.Contains("_encrypted")) cpy.name = avatar.name + "_encrypted"; return cpy; } - bool ConditionCheck(Material mat) + public GameObject Encrypt(bool isModular = true) { - if (shader_manager.IsPoiyomi(mat.shader)) - { - if (!shader_manager.IsLockPoiyomi(mat.shader)) - { -#if POIYOMI - ShaderOptimizer.SetLockedForAllMaterials(new[] { mat }, 1, true); -#elif POIYOMI91 - ShaderOptimizer.LockMaterials(new[] { mat }); -#endif - } - } - if (mat.mainTexture == null) - { - Debug.LogWarningFormat("{0} : The mainTexture is empty. it will be skip.", mat.name); - return false; - } - if ((mat.mainTexture is Texture2D) == false) - { - Debug.LogErrorFormat("MainTexture in {0} is not texture2D", mat.name); - return false; - } - if (mat.mainTexture.width % 2 != 0 && mat.mainTexture.height % 2 != 0) - { - Debug.LogErrorFormat("{0} : The texture size must be a multiple of 2!", mat.mainTexture.name); - return false; - } - if (injector.WasInjected(mat.shader)) - { - Debug.LogWarning(mat.name + ": The shader is already encrypted."); - return false; - } - var av3 = descriptor.gameObject.GetComponent(); - if (av3 == null) - { - Debug.LogError(descriptor.gameObject.name + ": can't find VRCAvatarDescriptor!"); - return false; - } - if (av3.expressionParameters == null) - { - Debug.LogError(descriptor.gameObject.name + ": can't find expressionParmeters!"); - return false; - } - return true; + return Encrypt(_useSmallMipTexture, isModular); } - bool CheckTextureFormat(Material mat) + public GameObject Encrypt(bool useSmallMip, bool isModular = true) { - if (mat == null) - return false; - if (mat.mainTexture == null) - return false; - var textureFormat = ((Texture2D)mat.mainTexture).format; - if (textureFormat != TextureFormat.DXT1 && textureFormat != TextureFormat.DXT5 && - textureFormat != TextureFormat.RGB24 && textureFormat != TextureFormat.RGBA32) - { - Debug.LogWarningFormat("{0} : is unsupported format", mat.mainTexture.name); - return false; - } - return true; + var request = new BuildRequest(this, _descriptor, useSmallMip, isModular); + var result = new Pipeline().Encrypt(request, CreateSettings()); + ApplyBuildResult(result); + return result.Avatar; } - public void CreateFolders() - { - if (!AssetDatabase.IsValidFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString()))) - { - AssetDatabase.CreateFolder(asset_dir, descriptor.gameObject.GetInstanceID().ToString()); - } - else - { - if (delete_folders) - { - AssetDatabase.DeleteAsset(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "animations")); - AssetDatabase.DeleteAsset(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "mat")); - AssetDatabase.DeleteAsset(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "shader")); - AssetDatabase.DeleteAsset(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "tex")); - } - } - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - if (!AssetDatabase.IsValidFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "tex"))) - AssetDatabase.CreateFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString()), "tex"); - if (!AssetDatabase.IsValidFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "mat"))) - AssetDatabase.CreateFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString()), "mat"); - if (!AssetDatabase.IsValidFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "shader"))) - AssetDatabase.CreateFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString()), "shader"); - if (!AssetDatabase.IsValidFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "animations"))) - AssetDatabase.CreateFolder(Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString()), "animations"); + internal BuildResult CurrentBuildResult => _buildResult; - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); + internal void ApplyBuildResult(BuildResult result) + { + _buildResult = result ?? new BuildResult(); } - public List GetMaterials() + internal BuildSettings CreateSettings() { - List materials = new List(); - foreach (GameObject g in gameobject_list) + return new BuildSettings { - if (g == null) - continue; - - var meshRenderers = g.GetComponentsInChildren(true); - foreach (var meshRenderer in meshRenderers) - { - foreach (var material in meshRenderer.sharedMaterials) - { - if (material != null) - { - materials.Add(material); - } - } - } - - var skinnedMeshRenderers = g.GetComponentsInChildren(true); - foreach (var skinnedMeshRenderer in skinnedMeshRenderers) - { - foreach (var material in skinnedMeshRenderer.sharedMaterials) - { - if (material != null) - { - materials.Add(material); - } - } - } - } - - return materials.Concat(material_list).Distinct().ToList(); + AssetDir = _assetDir, + FixedPassword = _fixedPassword, + UserPassword = _userPassword, + Language = _language, + LanguageIndex = _languageIndex, + Rounds = _rounds, + Filter = _filter, + Fallback = _fallback, + Algorithm = _algorithm, + KeySize = _keySize, + SyncSize = _syncSize, + DeleteFolders = _deleteFolders, + UseSmallMipTexture = _useSmallMipTexture, + PreserveMMD = _preserveMmd, + TurnOnAllSafetyFallback = _turnOnAllSafetyFallback + }; } - public GameObject Encrypt(bool isModular = true) + internal void ApplySettings(BuildSettings settings) { - return Encrypt(bUseSmallMipTexture, isModular); + if (settings == null) + return; + + _assetDir = settings.AssetDir; + _outputPaths = null; + _fixedPassword = settings.FixedPassword; + _userPassword = settings.UserPassword; + _language = settings.Language; + _languageIndex = settings.LanguageIndex; + _rounds = settings.Rounds; + _filter = settings.Filter; + _fallback = settings.Fallback; + _algorithm = settings.Algorithm; + _keySize = settings.KeySize; + _syncSize = settings.SyncSize; + _deleteFolders = settings.DeleteFolders; + _useSmallMipTexture = settings.UseSmallMipTexture; + _preserveMmd = settings.PreserveMMD; + _turnOnAllSafetyFallback = settings.TurnOnAllSafetyFallback; } - public GameObject Encrypt(bool bUseSmallMip, bool isModular = true) + internal GameObject EncryptLegacy(bool useSmallMip, bool isModular = true) { - meshes.Clear(); - encryptedMaterials.Clear(); - processedTextures.Clear(); + Meshes.Clear(); + EncryptedMaterials.Clear(); + ProcessedTextures.Clear(); SyncMatOption(); - MonoScript monoScript = MonoScript.FromMonoBehaviour(this); - string script_path = AssetDatabase.GetAssetPath(monoScript); - asset_dir = Path.GetDirectoryName(Path.GetDirectoryName(script_path)); - string avatarDir = Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString()); + string resourceDir = GetRuntimeAssetDir(); + _assetDir = ResolveOutputAssetDir(); + _outputPaths = new OutputPaths(_assetDir, _descriptor != null ? _descriptor.gameObject : null); + string avatarDir = _outputPaths.Avatar; + _buildResult.AvatarDir = avatarDir; - Debug.Log("AssetDir: " + asset_dir); + Debug.Log("AssetDir: " + _assetDir); - if (fallbackWhite == null) - fallbackWhite = AssetDatabase.LoadAssetAtPath(Path.Combine(asset_dir, "white.png"), typeof(Texture2D)) as Texture2D; - if (fallbackBlack == null) - fallbackBlack = AssetDatabase.LoadAssetAtPath(Path.Combine(asset_dir, "black.png"), typeof(Texture2D)) as Texture2D; + if (_fallbackWhite == null) + _fallbackWhite = AssetDatabase.LoadAssetAtPath(OutputPaths.Combine(resourceDir, "white.png"), typeof(Texture2D)) as Texture2D; + if (_fallbackBlack == null) + _fallbackBlack = AssetDatabase.LoadAssetAtPath(OutputPaths.Combine(resourceDir, "black.png"), typeof(Texture2D)) as Texture2D; - if (descriptor == null) + if (_descriptor == null) { Debug.LogError("Can't find avatar descriptor!"); return null; } - descriptor.gameObject.SetActive(true); + _descriptor.gameObject.SetActive(true); Debug.Log("Key bytes: " + string.Join(", ", GetKeyBytes())); var materials = new List(); foreach (var mat in GetMaterials()) { - if (CheckTextureFormat(mat)) + if (CheckIsSupportedFormat(mat)) { materials.Add(mat); } @@ -352,12 +338,12 @@ public GameObject Encrypt(bool bUseSmallMip, bool isModular = true) GameObject avatar; if (!isModular) { - avatar = DuplicateAvatar(descriptor.gameObject); + avatar = DuplicateAvatar(_descriptor.gameObject); Debug.Log("Duplicate avatar success."); } else { - avatar = descriptor.gameObject; + avatar = _descriptor.gameObject; } if (avatar == null) @@ -365,202 +351,137 @@ public GameObject Encrypt(bool bUseSmallMip, bool isModular = true) Debug.LogError("Cannot create duplicated avatar!"); return null; } - - var mips = new Dictionary(); - - byte[] key_bytes = GetKeyBytes(); + byte[] keyBytes = GetKeyBytes(); + _buildResult.KeyBytes = keyBytes; CreateFolders(); ///////////////////Select crypto algorithm///////////////////// IEncryptor encryptor = new XXTEA(); - if (algorithm == (int)Algorithm.xxtea) + if (_algorithm == (int)Algorithm.Xxtea) { XXTEA xxtea = new XXTEA(); - xxtea.m_rounds = rounds; + xxtea.Rounds = _rounds; encryptor = xxtea; } - else if(algorithm == (int)Algorithm.chacha) + else if (_algorithm == (int)Algorithm.Chacha) { Chacha20 chacha = new Chacha20(); - byte[] hash1 = KeyGenerator.GetKeyHash(key_bytes, KeyGenerator.GenerateRandomString(chacha.nonce.Length)); - Array.Copy(hash1, 0, chacha.nonce, 0, chacha.nonce.Length); + byte[] hash1 = KeyGenerator.GetKeyHash(keyBytes, KeyGenerator.GenerateRandomString(chacha.Nonce.Length)); + Array.Copy(hash1, 0, chacha.Nonce, 0, chacha.Nonce.Length); encryptor = chacha; } /////////////////////////////////////////////////////////////// - if (history == null) + if (_history == null) { - history = AssetDatabase.LoadAssetAtPath(Path.Combine(asset_dir, "EncryptedHistory.asset"), typeof(EncryptedHistory)) as EncryptedHistory; - if (history == null) + _history = AssetDatabase.LoadAssetAtPath(_outputPaths.History(), typeof(EncryptedHistory)) as EncryptedHistory; + if (_history == null) { - history = new EncryptedHistory(); - AssetDatabase.CreateAsset(history, Path.Combine(asset_dir, "EncryptedHistory.asset")); + _history = ScriptableObject.CreateInstance(); + _assetWriter.CreateAssetInFolder(_history, _outputPaths.Folders.RootGuid, _outputPaths.HistoryName()); } } - history.LoadData(); + _history.LoadData(); int progress = 0; int maxprogress = materials.Count; + + var mips = new Dictionary(); foreach (var mat in materials) { - int filter = this.filter; + if (mat == null) + continue; + int materialFilter = _filter; #if UNITY_2022 - MatOption option = matOptions.GetValueOrDefault(mat, null); + MatOption option = MaterialOptions.GetValueOrDefault(mat, null); #else MatOption option = null; - if (matOptions.ContainsKey(mat)) - option = matOptions[mat]; + if (MaterialOptions.ContainsKey(mat)) + option = MaterialOptions[mat]; #endif if (option != null) { - if (option.active == false) + if (option.Active == false) { Debug.LogFormat("{0} : Skip", mat.name); continue; } - filter = option.filter; + materialFilter = option.Filter; } - if (mat == null) - continue; EditorUtility.DisplayProgressBar("Encrypt...", "Encrypt Progress " + ++progress + " of " + maxprogress, (float)progress / (float)maxprogress); - injector = InjectorFactory.GetInjector(mat.shader); - if (injector == null) + _injector = InjectorFactory.GetInjector(mat.shader); + if (_injector == null) { - Debug.LogWarning(mat.shader + " is a unsupported shader! supported type:lilToon, poiyomi"); + Debug.LogError(mat.shader + " is a unsupported shader! supported type:lilToon, poiyomi"); continue; } if (!ConditionCheck(mat)) - { continue; - } - Debug.LogFormat("{0} : Start encrypt...", mat.name); - - Texture2D main_texture = (Texture2D)mat.mainTexture; - injector.Init(descriptor.gameObject, main_texture, key_bytes, key_size, filter, asset_dir, encryptor); - #region Generate mip_tex - int size = Math.Max(mat.mainTexture.width, mat.mainTexture.height); - if (!mips.ContainsKey(size)) - { - var mip = encrypt.GenerateRefMipmap(size, size, bUseSmallMip); - if (mip == null) - Debug.LogErrorFormat("{0} : Can't generate mip tex{1}.", mat.name, size); - else - { - mips.Add(size, mip); - AssetDatabase.CreateAsset(mip, Path.Combine(avatarDir, "tex", "mip_" + size + ".asset")); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - } - #endregion - TextureSettings.SetRWEnableTexture(main_texture); - TextureSettings.SetCrunchCompression(main_texture, false); - TextureSettings.SetGenerateMipmap(main_texture, true); - - string encrypt_tex_path = Path.Combine(avatarDir, "tex", main_texture.GetInstanceID() + "_encrypt.asset"); - string encrypt_tex2_path = Path.Combine(avatarDir, "tex", main_texture.GetInstanceID() + "_encrypt2.asset"); - string encrypted_mat_path = Path.Combine(avatarDir, "mat", mat.GetInstanceID() + "_encrypted.mat"); - string encrypted_shader_path = Path.Combine(avatarDir, "shader", mat.GetInstanceID().ToString()); - - #region Make encrypted textures - Texture2D[] encrypted_tex = new Texture2D[2] { null, null }; - bool processed = processedTextures.ContainsKey(main_texture); - ProcessedTexture processedTexture; - if (processed) - processedTexture = processedTextures[main_texture]; - else - processedTexture = new ProcessedTexture - { - encrypted0 = null, - encrypted1 = null, - fallbacks = new List(), - fallbackOptions = new List(), - nonce = new byte[12] - }; - - //Set chacha nonce - if (algorithm == (int)Algorithm.chacha) + if (_shaderManager.IsPoiyomi(mat.shader)) { - Chacha20 chacha = encryptor as Chacha20; - if (!processed) - { - byte[] hashMat = KeyGenerator.GetHash(mat.GetInstanceID()); - for (int i = 0; i < chacha.nonce.Length; ++i) - chacha.nonce[i] ^= hashMat[i]; - Array.Copy(chacha.nonce, 0, processedTexture.nonce, 0, processedTexture.nonce.Length); - } - else + if (!_shaderManager.IsLockPoiyomi(mat)) { - byte[] nonce = processedTextures[main_texture].nonce; - Array.Copy(nonce, 0, chacha.nonce, 0, chacha.nonce.Length); + _shaderManager.LockShader(mat); + Debug.LogFormat("Lock: {0} - {1}", mat.name, AssetDatabase.GetAssetPath(mat.shader)); } } - if (processed == false) - { - try - { - encrypted_tex = encrypt.TextureEncrypt(main_texture, key_bytes, encryptor); - } - catch(ArgumentException e) - { - Debug.LogErrorFormat("{0} : ArgumentException - {1}", main_texture.name, e.Message); - continue; - } - if (encrypted_tex == null) - { - Debug.LogErrorFormat("{0} : encrypt failed0.", main_texture.name); - continue; - } - if (encrypted_tex[0] == null) - { - Debug.LogErrorFormat("{0} : encrypt failed1.", main_texture.name); - continue; - } - AssetDatabase.CreateAsset(encrypted_tex[0], encrypt_tex_path); - Debug.Log(encrypted_tex[0].name + ": " + AssetDatabase.GetAssetPath(encrypted_tex[0])); - processedTexture.encrypted0 = encrypted_tex[0]; + Debug.LogFormat("{0} : Start encrypt...", mat.name); - if (encrypted_tex[1] != null) - { - AssetDatabase.CreateAsset(encrypted_tex[1], encrypt_tex2_path); - processedTexture.encrypted1 = encrypted_tex[1]; - } - } - else + Texture2D mainTexture = (Texture2D)mat.mainTexture; + _injector.Init(_descriptor.gameObject, mainTexture, keyBytes, _keySize, materialFilter, resourceDir, encryptor); + + int mipRefSize = Math.Max(mat.mainTexture.width, mat.mainTexture.height); + if (!mips.ContainsKey(mipRefSize)) { - encrypted_tex[0] = processedTexture.encrypted0; - encrypted_tex[1] = processedTexture.encrypted1; + Texture2D mipRef = GenerateMipRefTexture(_outputPaths.MipTextureName(mipRefSize), mipRefSize, useSmallMip); + if (mipRef != null) + mips.Add(mipRefSize, mipRef); } - #endregion + + TextureSettings.SetRWEnableTexture(mainTexture); + TextureSettings.SetCrunchCompression(mainTexture, false); + TextureSettings.SetGenerateMipmap(mainTexture, true); + + string encryptedShaderFolderGuid = _outputPaths.EnsureShaderFolder(_assetWriter, mat); + string encryptedShaderPath = _assetWriter.ResolveFolderPath(encryptedShaderFolderGuid); + + var processedTextureResult = GenerateEncryptedTexture(_outputPaths, mat, encryptor, keyBytes); + if (!processedTextureResult.HasValue) + continue; + ProcessedTexture processedTexture = processedTextureResult.Value; + + Texture2D encryptedTex1 = processedTexture.Encrypted.Texture1; + Texture2D encryptedTex2 = processedTexture.Encrypted.Texture2; //////////////////////Inject shader/////////////////////// - OtherTextures otherTex = GetLimOutlineTextures(mat); - Shader encrypted_shader = IsEncryptedBefore(mat.shader); - if (encrypted_shader == null) + AuxiliaryTextures otherTex = GetLimOutlineTextures(mat); + Shader encryptedShader = IsEncryptedBefore(mat.shader); + if (encryptedShader == null) { try { - string decodeDir = ""; - if (algorithm == (int)Algorithm.xxtea) - decodeDir = Path.Combine(asset_dir, "Decrypt.cginc"); - else if (algorithm == (int)Algorithm.chacha) - decodeDir = Path.Combine(asset_dir, "DecryptChacha.cginc"); - - encrypted_shader = injector.Inject(mat, decodeDir, encrypted_shader_path, encrypted_tex[0], - otherTex.limTexture != null, otherTex.limTexture2 != null, otherTex.outlineTexture != null); - - Selection.activeObject = encrypted_shader; + encryptedShader = _injector.Inject( + mat, + OutputPaths.Combine(resourceDir, "Shader/Protector.cginc"), + encryptedShaderPath, + encryptedTex1, + otherTex.LimTexture != null, + otherTex.LimTexture2 != null, + otherTex.OutlineTexture != null + ); + + Selection.activeObject = encryptedShader; EditorApplication.ExecuteMenuItem("Assets/Reimport"); - if (encrypted_shader == null) + if (encryptedShader == null) { Debug.LogErrorFormat("{0}: Injection failed", mat.name); continue; } - history.Save(mat.shader); + _history.Save(mat.shader); } catch (UnityException e) { @@ -568,265 +489,99 @@ public GameObject Encrypt(bool bUseSmallMip, bool isModular = true) continue; } } - #region FallbackTexture - /////////////////Generate fallback///////////////////// - string fallbackDir = Path.Combine(avatarDir, "tex", main_texture.GetInstanceID() + "_fallback.asset"); - int fallbackOption = this.fallback; - if (option != null) - fallbackOption = option.fallback; + ///////////////////////////////////////////////////////// + Texture2D fallback = GenerateFallbackTexture(_outputPaths.FallbackTextureName(mainTexture), option, mainTexture, ref processedTexture); + if (fallback == null) + Debug.LogErrorFormat("Failed to generate fallback texture: {0}", mainTexture.name); - int idx = processedTexture.fallbackOptions.FindIndex(option => option == fallbackOption); - Texture2D fallback = null; - if (idx == -1) - { - int fallbackSize = 32; - switch (fallbackOption) - { - case 0: // white - fallbackSize = 0; - break; - case 1: // black - fallbackSize = 1; - break; - case 2: - fallbackSize = 4; - break; - case 3: - fallbackSize = 8; - break; - case 4: - fallbackSize = 16; - break; - case 5: - fallbackSize = 32; - break; - case 6: - fallbackSize = 64; - break; - case 7: - fallbackSize = 128; - break; - } - if (fallbackSize > 1) - { - fallback = encrypt.GenerateFallback(main_texture, fallbackSize); - if (fallback != null) - { - processedTexture.fallbacks.Add(fallback); - processedTexture.fallbackOptions.Add(fallbackOption); - AssetDatabase.CreateAsset(fallback, fallbackDir); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - } - else - { - switch (fallbackSize) - { - case 0: - processedTexture.fallbacks.Add(fallbackWhite); - processedTexture.fallbackOptions.Add(fallbackOption); - fallback = fallbackWhite; - break; - case 1: - processedTexture.fallbacks.Add(fallbackBlack); - processedTexture.fallbackOptions.Add(fallbackOption); - fallback = fallbackBlack; - break; - } - } - } - else - fallback = processedTexture.fallbacks[idx]; - //////////////////////////////////////////////////////// - #endregion - - #region Material - //////////////////Create Material//////////////////////// - Material new_mat = new Material(mat.shader); - new_mat.CopyPropertiesFromMaterial(mat); - new_mat.shader = encrypted_shader; - var original_tex = new_mat.mainTexture; - new_mat.mainTexture = fallback; - - int max = Math.Max(encrypted_tex[0].width, encrypted_tex[0].height); - var mip_tex = mips[max]; - if(mip_tex == null) - Debug.LogWarningFormat("mip_{0} is not exsist", max); - - new_mat.SetTexture("_MipTex", mip_tex); - - if (encrypted_tex[0] != null) - new_mat.SetTexture("_EncryptTex0", encrypted_tex[0]); - if (encrypted_tex[1] != null) - new_mat.SetTexture("_EncryptTex1", encrypted_tex[1]); - - new_mat.renderQueue = mat.renderQueue; - if (turnOnAllSafetyFallback) - new_mat.SetOverrideTag("VRCFallback", "Unlit"); - - int woffset = 0; - int hoffset = 0; - if (main_texture.format == TextureFormat.DXT1 || main_texture.format == TextureFormat.DXT5) - { - woffset = 13 - (int)Mathf.Log(main_texture.width, 2) - 1 + 2; - hoffset = 13 - (int)Mathf.Log(main_texture.height, 2) - 1 + 2; - } - else - { - woffset = 13 - (int)Mathf.Log(main_texture.width, 2) - 1; - hoffset = 13 - (int)Mathf.Log(main_texture.height, 2) - 1; - } - new_mat.SetInteger("_Woffset", woffset); - new_mat.SetInteger("_Hoffset", hoffset); - for (int i = 0; i < 16 - key_size; ++i) - new_mat.SetFloat("_Key" + i, key_bytes[i]); - - if (algorithm == (int)Algorithm.chacha) - { - Chacha20 chacha = encryptor as Chacha20; - new_mat.SetInteger("_Nonce0", (int)chacha.GetNonceUint3()[0]); - new_mat.SetInteger("_Nonce1", (int)chacha.GetNonceUint3()[1]); - new_mat.SetInteger("_Nonce2", (int)chacha.GetNonceUint3()[2]); - } - - AssetDatabase.CreateAsset(new_mat, encrypted_mat_path); - Debug.LogFormat("{0} : create encrypted material : {1}", mat.name, AssetDatabase.GetAssetPath(new_mat)); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - ////////////////////////////////////////////////////// - #endregion + int maxSize = Math.Max(mainTexture.width, mainTexture.height); + Texture2D mipTex = mips[maxSize]; + if (mipTex == null) + Debug.LogWarningFormat("mip_{0} is not exsist", maxSize); - if (!processed) - processedTextures.Add(main_texture, processedTexture); - if (!encryptedMaterials.ContainsKey(mat)) - encryptedMaterials.Add(mat, new_mat); + GenerateEncryptedMaterial(_outputPaths.EncryptedMaterialName(mat), mat, encryptedShader, fallback, mipTex, otherTex, processedTexture, keyBytes, encryptor); } // Material loop EditorUtility.ClearProgressBar(); ///////////////////////parameter//////////////////// var av3 = avatar.GetComponent(); - av3.expressionParameters = ParameterManager.AddKeyParameter(av3.expressionParameters, key_size, parameter_multiplexing); - AssetDatabase.CreateAsset(av3.expressionParameters, Path.Combine(avatarDir, av3.expressionParameters.name + ".asset")); + av3.expressionParameters = ParameterManager.AddKeyParameter(av3.expressionParameters, _keySize, _syncSize); + _assetWriter.CreateAssetInFolder(av3.expressionParameters, _outputPaths.Folders.AvatarGuid, _outputPaths.ParametersName(av3.expressionParameters.name)); //////////////////////////////////////////////////// if (!isModular) { ReplaceMaterials(avatar); RemoveDuplicatedTextures(avatar); - SetMaterialFallbackValue(avatar, true); - descriptor.gameObject.SetActive(false); + _descriptor.gameObject.SetActive(false); var newDesriptor = avatar.transform.GetComponentInChildren(true).gameObject; var tester = newDesriptor.AddComponent(); - tester.lang = lang; - tester.lang_idx = lang_idx; - tester.protector = this; - tester.userKeyLength = key_size; + tester.Language = _language; + tester.LanguageIndex = _languageIndex; + tester.Protector = this; + tester.UserKeyLength = _keySize; Selection.activeObject = tester; #if MODULAR var maMergeAnims = avatar.GetComponentsInChildren(true); foreach (var maMergeAnim in maMergeAnims) { - AnimatorController newAnim = AnimatorManager.DuplicateAnimator(maMergeAnim.animator, Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString())); + AnimatorController newAnim = AnimatorManager.DuplicateAnimator(maMergeAnim.animator, _outputPaths, _assetWriter); maMergeAnim.animator = newAnim; } #endif SetAnimations(avatar, true); ObfuscateBlendShape(avatar, true); ChangeMaterialsInAnims(avatar, true); - CleanComponent(avatar); + CleanComponent(avatar); } - + return avatar; } public void ReplaceMaterials(GameObject avatar) { - var renderers = avatar.GetComponentsInChildren(true); - if (renderers != null) - { - for (int i = 0; i < renderers.Length; ++i) - { - var mats = renderers[i].sharedMaterials; - if (mats == null) - continue; - for (int j = 0; j < mats.Length; ++j) - { - if (mats[j] == null) - continue; - - if (encryptedMaterials.ContainsKey(mats[j])) - { - mats[j] = encryptedMaterials[mats[j]]; - meshes.Add(renderers[i].gameObject); - } - } - renderers[i].sharedMaterials = mats; - } - } - var skinnedRenderers = avatar.GetComponentsInChildren(true); - if (skinnedRenderers != null) - { - for (int i = 0; i < skinnedRenderers.Length; ++i) - { - var mats = skinnedRenderers[i].sharedMaterials; - if (mats == null) - continue; - for (int j = 0; j < mats.Length; ++j) - { - if (mats[j] == null) - continue; - - if (encryptedMaterials.ContainsKey(mats[j])) - { - mats[j] = encryptedMaterials[mats[j]]; - meshes.Add(skinnedRenderers[i].gameObject); - } - } - skinnedRenderers[i].sharedMaterials = mats; - } - } + AvatarProcessor.ReplaceMaterials(avatar, _buildResult); } - OtherTextures GetLimOutlineTextures(Material mat) + AuxiliaryTextures GetLimOutlineTextures(Material mat) { - OtherTextures others = new OtherTextures(); - if (shader_manager.IsPoiyomi(mat.shader)) + AuxiliaryTextures others = new AuxiliaryTextures(); + if (_shaderManager.IsPoiyomi(mat.shader)) { var tex_properties = mat.GetTexturePropertyNames(); foreach (var t in tex_properties) { if (t == "_RimTex") - others.limTexture = (Texture2D)mat.GetTexture(t); + others.LimTexture = (Texture2D)mat.GetTexture(t); else if (t == "_Rim2Tex") - others.limTexture2 = (Texture2D)mat.GetTexture(t); + others.LimTexture2 = (Texture2D)mat.GetTexture(t); else if (t == "_OutlineTexture") - others.outlineTexture = (Texture2D)mat.GetTexture(t); + others.OutlineTexture = (Texture2D)mat.GetTexture(t); } } - else if (shader_manager.IslilToon(mat.shader)) + else if (_shaderManager.IsLilToon(mat.shader)) { var tex_properties = mat.GetTexturePropertyNames(); foreach (var t in tex_properties) { if (t == "_RimColorTex") - others.limTexture = (Texture2D)mat.GetTexture(t); + others.LimTexture = (Texture2D)mat.GetTexture(t); else if (t == "_OutlineTex") - others.outlineTexture = (Texture2D)mat.GetTexture(t); + others.OutlineTexture = (Texture2D)mat.GetTexture(t); else if (t == "_RimShadeMask") - others.limShadeTexture = (Texture2D)mat.GetTexture(t); + others.LimShadeTexture = (Texture2D)mat.GetTexture(t); } } return others; } public void RemoveDuplicatedTextures(GameObject avatar) { - string avatarDir = Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString()); - foreach (var mat in encryptedMaterials.Values) + OutputPaths paths = EnsureOutputFolders(); + foreach (var mat in EncryptedMaterials.Values) { - OtherTextures otherTex = GetLimOutlineTextures(mat); + AuxiliaryTextures otherTex = GetLimOutlineTextures(mat); foreach (var name in mat.GetTexturePropertyNames()) { @@ -835,179 +590,168 @@ public void RemoveDuplicatedTextures(GameObject avatar) if (!(mat.GetTexture(name) is Texture2D)) continue; - if (processedTextures.ContainsKey((Texture2D)mat.GetTexture(name))) + if (ProcessedTextures.ContainsKey((Texture2D)mat.GetTexture(name))) { Texture2D mainTexture = (Texture2D)mat.GetTexture(name); - Texture2D encrypted0 = processedTextures[(Texture2D)mat.GetTexture(name)].encrypted0; - - int idx = processedTextures[(Texture2D)mat.GetTexture(name)].fallbackOptions.IndexOf(processedTextures[(Texture2D)mat.GetTexture(name)].fallbackOptions.Max()); - Texture2D bigFallbackTexture = processedTextures[(Texture2D)mat.GetTexture(name)].fallbacks[idx]; + Texture2D encrypted0 = ProcessedTextures[(Texture2D)mat.GetTexture(name)].Encrypted.Texture1; + + int idx = ProcessedTextures[(Texture2D)mat.GetTexture(name)].FallbackOptions.IndexOf(ProcessedTextures[(Texture2D)mat.GetTexture(name)].FallbackOptions.Max()); + Texture2D bigFallbackTexture = ProcessedTextures[(Texture2D)mat.GetTexture(name)].Fallbacks[idx]; - if (otherTex.limTexture != null) + if (otherTex.LimTexture != null) { string texName = ""; - if (shader_manager.IsPoiyomi(mat.shader)) + if (_shaderManager.IsPoiyomi(mat.shader)) texName = "_RimTex"; - else if (shader_manager.IslilToon(mat.shader)) + else if (_shaderManager.IsLilToon(mat.shader)) texName = "_RimColorTex"; - if (mainTexture == otherTex.limTexture) + if (mainTexture == otherTex.LimTexture) mat.SetTexture(texName, encrypted0); - else if (processedTextures.ContainsKey(otherTex.limTexture)) + else if (ProcessedTextures.ContainsKey(otherTex.LimTexture)) mat.SetTexture(texName, null); } - if (otherTex.limTexture2 != null) //only poiyomi + if (otherTex.LimTexture2 != null) //only poiyomi { string texName = ""; - if (shader_manager.IsPoiyomi(mat.shader)) + if (_shaderManager.IsPoiyomi(mat.shader)) texName = "_Rim2Tex"; - if (mainTexture == otherTex.limTexture2) + if (mainTexture == otherTex.LimTexture2) mat.SetTexture(texName, encrypted0); - else if (processedTextures.ContainsKey(otherTex.limTexture2)) + else if (ProcessedTextures.ContainsKey(otherTex.LimTexture2)) mat.SetTexture(texName, null); } - if (otherTex.outlineTexture != null) + if (otherTex.OutlineTexture != null) { string texName = ""; - if (shader_manager.IsPoiyomi(mat.shader)) + if (_shaderManager.IsPoiyomi(mat.shader)) texName = "_OutlineTexture"; - else if (shader_manager.IslilToon(mat.shader)) + else if (_shaderManager.IsLilToon(mat.shader)) texName = "_OutlineTex"; - if (mainTexture == otherTex.outlineTexture) + if (mainTexture == otherTex.OutlineTexture) mat.SetTexture(texName, bigFallbackTexture); - else if (processedTextures.ContainsKey(otherTex.outlineTexture)) - mat.SetTexture(texName, processedTextures[otherTex.outlineTexture].fallbacks[0]); + else if (ProcessedTextures.ContainsKey(otherTex.OutlineTexture)) + mat.SetTexture(texName, ProcessedTextures[otherTex.OutlineTexture].Fallbacks[0]); } - if (otherTex.limShadeTexture != null) //only liltoon + if (otherTex.LimShadeTexture != null) //only liltoon { string texName = "_RimShadeMask"; - if (mainTexture == otherTex.limShadeTexture) + if (mainTexture == otherTex.LimShadeTexture) mat.SetTexture(texName, bigFallbackTexture); - else if (processedTextures.ContainsKey(otherTex.limShadeTexture)) + else if (ProcessedTextures.ContainsKey(otherTex.LimShadeTexture)) mat.SetTexture(texName, null); } } } } // Encrypted materials loop - var renderers = avatar.GetComponentsInChildren(true); - if (renderers != null) + var duplicatedMaterials = new Dictionary(); + bool changedFallbackMaterials = ReplaceProcessedTexturesWithFallbacks(avatar.GetComponentsInChildren(true), paths, duplicatedMaterials); + changedFallbackMaterials |= ReplaceProcessedTexturesWithFallbacks(avatar.GetComponentsInChildren(true), paths, duplicatedMaterials); + if (changedFallbackMaterials) + _assetWriter.SaveAndRefresh(); + } + + bool ReplaceProcessedTexturesWithFallbacks(IEnumerable renderers, OutputPaths paths, Dictionary duplicatedMaterials) where T : Renderer + { + bool changed = false; + foreach (T renderer in renderers) { - for (int i = 0; i < renderers.Length; ++i) + Material[] mats = renderer.sharedMaterials; + if (mats == null) + continue; + + bool rendererChanged = false; + for (int i = 0; i < mats.Length; ++i) { - var mats = renderers[i].sharedMaterials; - if (mats == null) + Material sourceMaterial = mats[i]; + if (sourceMaterial == null) continue; - for (int j = 0; j < mats.Length; ++j) + + Material duplicatedMaterial = null; + foreach (string name in sourceMaterial.GetTexturePropertyNames()) { - if (mats[j] == null) + Texture2D texture = sourceMaterial.GetTexture(name) as Texture2D; + if (texture == null || !ProcessedTextures.TryGetValue(texture, out ProcessedTexture processedTexture)) continue; - Material tmp = null; - foreach (var name in mats[j].GetTexturePropertyNames()) - { - if (mats[j].GetTexture(name) == null) - continue; - if (!(mats[j].GetTexture(name) is Texture2D)) - continue; - Texture2D tex = (Texture2D)mats[j].GetTexture(name); - if (processedTextures.ContainsKey(tex)) - { - int idx = processedTextures[tex].fallbackOptions.IndexOf(processedTextures[tex].fallbackOptions.Max()); - Texture2D bigFallbackTexture = processedTextures[tex].fallbacks[idx]; - if (tmp == null) - { - Material mat = AssetDatabase.LoadAssetAtPath(Path.Combine(avatarDir, "mat", (mats[j].GetInstanceID() + "_duplicated.mat"))); - if (mat == null) - { - tmp = Instantiate(mats[j]); - AssetDatabase.CreateAsset(tmp, Path.Combine(avatarDir, "mat", (mats[j].GetInstanceID() + "_duplicated.mat"))); - AssetDatabase.SaveAssets(); - } - else - tmp = mat; - } - tmp.SetTexture(name, bigFallbackTexture); - } - } - AssetDatabase.Refresh(); - if (tmp != null) - mats[j] = tmp; + if (duplicatedMaterial == null) + duplicatedMaterial = GetOrCreateDuplicatedMaterial(sourceMaterial, paths, duplicatedMaterials); + + duplicatedMaterial.SetTexture(name, GetLargestFallback(processedTexture)); + EditorUtility.SetDirty(duplicatedMaterial); + changed = true; } - renderers[i].sharedMaterials = mats; - } - } - var skinned_renderers = avatar.GetComponentsInChildren(true); - if (skinned_renderers != null) - { - for (int i = 0; i < skinned_renderers.Length; ++i) - { - var mats = skinned_renderers[i].sharedMaterials; - if (mats == null) - continue; - for (int j = 0; j < mats.Length; ++j) - { - if (mats[j] == null) - continue; - Material tmp = null; - foreach (var name in mats[j].GetTexturePropertyNames()) - { - if (mats[j].GetTexture(name) == null) - continue; - if (!(mats[j].GetTexture(name) is Texture2D)) - continue; - - if (processedTextures.ContainsKey((Texture2D)mats[j].GetTexture(name))) - { - Texture2D mainTex = (Texture2D)mats[j].GetTexture(name); - int idx = processedTextures[mainTex].fallbackOptions.IndexOf(processedTextures[mainTex].fallbackOptions.Max()); - Texture2D bigFallbackTexture = processedTextures[mainTex].fallbacks[idx]; - if (tmp == null) - { - Material mat = AssetDatabase.LoadAssetAtPath(Path.Combine(avatarDir, "mat", (mats[j].GetInstanceID() + "_duplicated.mat"))); - if (mat == null) - { - tmp = Instantiate(mats[j]); - AssetDatabase.CreateAsset(tmp, Path.Combine(avatarDir, "mat", (mats[j].GetInstanceID() + "_duplicated.mat"))); - AssetDatabase.SaveAssets(); - } - else - tmp = mat; - } - tmp.SetTexture(name, bigFallbackTexture); - } - } - AssetDatabase.Refresh(); - if (tmp != null) - mats[j] = tmp; + if (duplicatedMaterial != null) + { + mats[i] = duplicatedMaterial; + rendererChanged = true; } - skinned_renderers[i].sharedMaterials = mats; } + + if (rendererChanged) + renderer.sharedMaterials = mats; } + + return changed; } + + Material GetOrCreateDuplicatedMaterial(Material source, OutputPaths paths, Dictionary duplicatedMaterials) + { + if (duplicatedMaterials.TryGetValue(source, out Material duplicatedMaterial)) + return duplicatedMaterial; + + string duplicatedPath = OutputPaths.Combine(_assetWriter.ResolveFolderPath(paths.Folders.MatGuid), paths.DuplicatedMaterialName(source)); + duplicatedMaterial = AssetDatabase.LoadAssetAtPath(duplicatedPath); + if (duplicatedMaterial == null) + { + duplicatedMaterial = Instantiate(source); + _assetWriter.CreateAssetInFolder(duplicatedMaterial, paths.Folders.MatGuid, paths.DuplicatedMaterialName(source)); + } + + duplicatedMaterials[source] = duplicatedMaterial; + return duplicatedMaterial; + } + + static Texture2D GetLargestFallback(ProcessedTexture processedTexture) + { + int idx = processedTexture.FallbackOptions.IndexOf(processedTexture.FallbackOptions.Max()); + return processedTexture.Fallbacks[idx]; + } + public void SetAnimations(GameObject avatar, bool clone) { var av3 = avatar.GetComponent(); + OutputPaths paths = EnsureOutputFolders(); AnimatorController fx; if (clone) - fx = AnimatorManager.DuplicateAnimator(av3.baseAnimationLayers[4].animatorController, Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString())); + { + var sourceControllers = new RuntimeAnimatorController[av3.baseAnimationLayers.Length]; + for (int i = 0; i < av3.baseAnimationLayers.Length; ++i) + sourceControllers[i] = av3.baseAnimationLayers[i].animatorController; + + AnimatorController[] duplicatedLayers = AnimatorManager.DuplicateAnimators(sourceControllers, paths, _assetWriter); + for (int i = 0; i < duplicatedLayers.Length; ++i) + { + if (duplicatedLayers[i] != null) + av3.baseAnimationLayers[i].animatorController = duplicatedLayers[i]; + } + + fx = duplicatedLayers[4]; + } else fx = av3.baseAnimationLayers[4].animatorController as AnimatorController; - av3.baseAnimationLayers[4].animatorController = fx; - string animationDir = Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "animations"); + string animationDir = _assetWriter.ResolveFolderPath(paths.Folders.AnimGuid); - GameObject[] meshArray = new GameObject[meshes.Count]; - meshes.CopyTo(meshArray); - AnimatorManager.CreateKeyAniamtions(Path.Combine(asset_dir, "Animations"), animationDir, meshArray); - var fallbackOnAnim = AnimatorManager.CreateFallbackAniamtions(Path.Combine(asset_dir, "Animations", "FallbackOn.anim"), animationDir, meshArray, false); - var fallbackOffAnim = AnimatorManager.CreateFallbackAniamtions(Path.Combine(asset_dir, "Animations", "FallbackOff.anim"), animationDir, meshArray, true); - AnimatorManager.AddKeyLayer(fx, animationDir, key_size, animation_speed, parameter_multiplexing); - AnimatorManager.AddFallbackLayer(fx, fallbackOnAnim, fallbackOffAnim, fallbackTime); + GameObject[] meshArray = new GameObject[Meshes.Count]; + Meshes.CopyTo(meshArray); + AnimatorManager.CreateKeyAnimations(OutputPaths.Combine(GetRuntimeAssetDir(), "Animations"), paths, _assetWriter, meshArray); + AnimatorManager.AddKeyLayer(fx, animationDir, _keySize, _syncSize, 3.0f); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); @@ -1022,13 +766,13 @@ public void ChangeMaterialsInAnims(GameObject avatar, bool clone) { var av3 = avatar.GetComponent(); var fx = av3.baseAnimationLayers[4].animatorController as AnimatorController; - string animationDir = Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "animations"); + OutputPaths paths = EnsureOutputFolders(); - AnimatorManager animManager = new AnimatorManager(); - foreach (var pair in encryptedMaterials) + AnimatorManager animManager = ScriptableObject.CreateInstance(); + foreach (var pair in EncryptedMaterials) { Debug.LogFormat("{0}, {1}", pair.Key.name, pair.Value.name); - animManager.ChangeAnimationMaterial(fx, pair.Key, pair.Value, clone, animationDir); + animManager.ChangeAnimationMaterial(fx, pair.Key, pair.Value, clone, paths, _assetWriter); } #if MODULAR @@ -1039,9 +783,9 @@ public void ChangeMaterialsInAnims(GameObject avatar, bool clone) { if (maMergeAnim.animator == null) continue; - foreach (var pair in encryptedMaterials) + foreach (var pair in EncryptedMaterials) { - animManager.ChangeAnimationMaterial(maMergeAnim.animator as AnimatorController, pair.Key, pair.Value, clone, animationDir); + animManager.ChangeAnimationMaterial(maMergeAnim.animator as AnimatorController, pair.Key, pair.Value, clone, paths, _assetWriter); } } } @@ -1050,30 +794,30 @@ public void ChangeMaterialsInAnims(GameObject avatar, bool clone) public VRCExpressionParameters GetParameter() { - var av3 = descriptor; + var av3 = _descriptor; if (av3 == null) return null; return av3.expressionParameters; } - public static AnimatorController Getfx(GameObject avatar) + public static AnimatorController GetFx(GameObject avatar, int playableLayer = 4) { var av3 = avatar.GetComponent(); if (av3 == null) return null; - return av3.baseAnimationLayers[4].animatorController as AnimatorController; + return av3.baseAnimationLayers[playableLayer].animatorController as AnimatorController; } - public void ObfuscateBlendShape(GameObject avatar, bool bClone) + public void ObfuscateBlendShape(GameObject avatar, bool clone) { - // bClone true = Manual encrypt + // Clone true = Manual encrypt var av3 = avatar.GetComponent(); - AnimatorController fx = Getfx(avatar); - string animDir = Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString(), "animations"); + AnimatorController fx = GetFx(avatar); + OutputPaths paths = EnsureOutputFolders(); - Obfuscator obfuscator = new Obfuscator(); - obfuscator.clone = bClone; - obfuscator.bPreserveMMD = bPreserveMMD; + Obfuscator obfuscator = ScriptableObject.CreateInstance(); + obfuscator.Clone = clone; + obfuscator.PreserveMmd = _preserveMmd; var childRenderers = avatar.GetComponentsInChildren(); @@ -1092,7 +836,7 @@ public void ObfuscateBlendShape(GameObject avatar, bool bClone) } } #endif - foreach (var renderer in obfuscationRenderers) + foreach (var renderer in _obfuscationRenderers) { SkinnedMeshRenderer selectRenderer = null; foreach (var childRenderer in childRenderers) @@ -1112,7 +856,7 @@ public void ObfuscateBlendShape(GameObject avatar, bool bClone) Debug.LogErrorFormat("{0} haven't mesh", renderer.transform.name); continue; } - Mesh newMesh = obfuscator.ObfuscateBlendShapeMesh(mesh, Path.Combine(asset_dir, descriptor.gameObject.GetInstanceID().ToString())); + Mesh newMesh = obfuscator.ObfuscateBlendShapeMesh(mesh, paths, _assetWriter); selectRenderer.sharedMesh = newMesh; ////////Change renderer component shape keys//////// @@ -1162,113 +906,68 @@ public void ObfuscateBlendShape(GameObject avatar, bool bClone) } } - if(bClone) + if (clone) { var maMergeAnims = avatar.GetComponentsInChildren(true); foreach (var maMergeAnim in maMergeAnims) { - obfuscator.ObfuscateBlendshapeInAnim(maMergeAnim.animator as AnimatorController, selectRenderer.gameObject, animDir); + obfuscator.ObfuscateBlendshapeInAnim(maMergeAnim.animator as AnimatorController, selectRenderer.gameObject, paths, _assetWriter); } } #endif - obfuscator.ObfuscateBlendshapeInAnim(fx, selectRenderer.gameObject, animDir); - obfuscator.ChangeObfuscatedBlendShapeInDescriptor(av3); - obfuscator.Clean(); - } - } - - public void SetMaterialFallbackValue(GameObject avatar, bool fallback) - { - var renderers = avatar.GetComponentsInChildren(true); - if (renderers != null) - { - foreach (var r in renderers) + for (int i = 0; i <= 4; ++i) { - var mats = r.sharedMaterials; - if (mats == null) - { - Debug.LogWarning(r.gameObject.name + ": can't find sharedMaterials"); + AnimatorController playableLayer = GetFx(avatar, i); + if (playableLayer == null) continue; - } - foreach (var mat in mats) - { - if (mat == null) - continue; - if (mat.name.Contains("_encrypted") || mat.name.Contains("_duplicated")) - { - mat.SetFloat("_fallback", fallback == true ? 1.0f : 0.0f); - } - } - } - } - var skinned_renderers = avatar.GetComponentsInChildren(true); - if (skinned_renderers != null) - { - foreach (var r in skinned_renderers) - { - var mats = r.sharedMaterials; - if (mats == null) - { - Debug.LogWarning(r.gameObject.name + ": can't find sharedMaterials"); - continue; - } - foreach (var mat in mats) - { - if (mat == null) - continue; - if (mat.name.Contains("_encrypted") || mat.name.Contains("_duplicated")) - { - mat.SetFloat("_fallback", fallback == true ? 1.0f : 0.0f); - } - } + obfuscator.ObfuscateBlendshapeInAnim(playableLayer, selectRenderer.gameObject, paths, _assetWriter); } + obfuscator.ChangeObfuscatedBlendShapeInDescriptor(av3); + obfuscator.Clean(); } } - public int GetEncyryptedFoldersCount() + public int GetEncryptedFoldersCount() { - if (!Directory.Exists(asset_dir)) + _assetDir = ResolveOutputAssetDir(); + if (!Directory.Exists(_assetDir)) { - Debug.LogError($"The specified path does not exist: {asset_dir}"); return 0; } else { - string[] directories = Directory.GetDirectories(asset_dir); + string[] directories = Directory.GetDirectories(_assetDir); int deletedCount = 0; foreach (string dir in directories) { - string folderName = Path.GetFileName(dir); - if (Regex.IsMatch(folderName, @"^-*\d+$")) - { + if (IsGeneratedOutputFolder(dir)) deletedCount++; - } } return deletedCount; } } public void CleanEncrypted() { - AssetDatabase.DeleteAsset(Path.Combine(asset_dir, "EncryptedHistory.asset")); + _assetDir = ResolveOutputAssetDir(); + AssetDatabase.DeleteAsset(OutputPaths.Combine(_assetDir, "EncryptedHistory.asset")); - if (!Directory.Exists(asset_dir)) + if (!Directory.Exists(_assetDir)) { - Debug.LogError($"The specified path does not exist: {asset_dir}"); + Debug.LogError($"The specified path does not exist: {_assetDir}"); } else { - string[] directories = Directory.GetDirectories(asset_dir); + string[] directories = Directory.GetDirectories(_assetDir); int deletedCount = 0; foreach (string dir in directories) { - string folderName = Path.GetFileName(dir); - if (Regex.IsMatch(folderName, @"^-*\d+$")) + if (IsGeneratedOutputFolder(dir)) { try { - AssetDatabase.DeleteAsset(dir); + AssetDatabase.DeleteAsset(OutputPaths.Normalize(dir)); deletedCount++; Debug.Log($"Deleted folder: {dir}"); } @@ -1284,39 +983,307 @@ public void CleanEncrypted() } } + bool IsGeneratedOutputFolder(string path) + { + string normalized = OutputPaths.Normalize(path); + string folderName = Path.GetFileName(normalized); + if (Regex.IsMatch(folderName, @"^-*\d+$")) + return true; + + return AssetDatabase.IsValidFolder(OutputPaths.Combine(normalized, OutputPaths.TexFolder)) || + AssetDatabase.IsValidFolder(OutputPaths.Combine(normalized, OutputPaths.MatFolder)) || + AssetDatabase.IsValidFolder(OutputPaths.Combine(normalized, OutputPaths.ShaderFolder)) || + AssetDatabase.IsValidFolder(OutputPaths.Combine(normalized, OutputPaths.AnimFolder)) || + AssetDatabase.IsValidFolder(OutputPaths.Combine(normalized, OutputPaths.MeshFolder)); + } + public int GetDefaultFilter() { - return filter; + return _filter; } public int GetDefaultFallback() { - return fallback; + return _fallback; } public int GetKeySize() { - return key_size; + return _keySize; } public void ResetMaterialOptions() { - matOptionSaved.Clear(); - matOptions.Clear(); + _matOptionSaved.Clear(); + MaterialOptions.Clear(); } public Shader IsEncryptedBefore(Shader shader) { - if (history == null) + if (_history == null) { - history = AssetDatabase.LoadAssetAtPath(Path.Combine(asset_dir, "EncryptedHistory.asset"), typeof(EncryptedHistory)) as EncryptedHistory; - if (history == null) + _history = AssetDatabase.LoadAssetAtPath(GetOutputPaths().History(), typeof(EncryptedHistory)) as EncryptedHistory; + if (_history == null) { - history = new EncryptedHistory(); - AssetDatabase.CreateAsset(history, Path.Combine(asset_dir, "EncryptedHistory.asset")); + _history = ScriptableObject.CreateInstance(); + OutputPaths paths = GetOutputPaths(); + if (paths.Folders == null) + paths.PrepareFolders(_assetWriter, false); + _assetWriter.CreateAssetInFolder(_history, paths.Folders.RootGuid, paths.HistoryName()); } } - history.LoadData(); - return history.IsEncryptedBefore(shader); + _history.LoadData(); + return _history.IsEncryptedBefore(shader); + } + + public static int GetRequiredSwitchCount(int keyLength, int syncSize) + { + keyLength /= syncSize; + return Mathf.CeilToInt(Mathf.Log(keyLength, 2)); + } + bool ConditionCheck(Material mat) + { + if (mat.mainTexture == null) + { + Debug.LogWarningFormat("{0} : The mainTexture is empty. it will be skip.", mat.name); + return false; + } + if ((mat.mainTexture is Texture2D) == false) + { + Debug.LogErrorFormat("MainTexture in {0} is not texture2D", mat.name); + return false; + } + if (mat.mainTexture.width % 2 != 0 || mat.mainTexture.height % 2 != 0) + { + Debug.LogErrorFormat("{0} : The texture size must be a multiple of 2!", mat.mainTexture.name); + return false; + } + if (_injector.WasInjected(mat.shader)) + { + Debug.LogWarning(mat.name + ": The shader is already encrypted."); + return false; + } + var av3 = _descriptor.gameObject.GetComponent(); + if (av3 == null) + { + Debug.LogError(_descriptor.gameObject.name + ": can't find VRCAvatarDescriptor!"); + return false; + } + if (av3.expressionParameters == null) + { + Debug.LogError(_descriptor.gameObject.name + ": can't find expressionParmeters!"); + return false; + } + return true; + } + public List GetMaterials() + { + List materials = new List(); + foreach (GameObject g in _gameObjectList) + { + if (g == null) + continue; + + var meshRenderers = g.GetComponentsInChildren(true); + foreach (var meshRenderer in meshRenderers) + { + foreach (var material in meshRenderer.sharedMaterials) + { + if (material != null) + { + materials.Add(material); + } + } + } + + var skinnedMeshRenderers = g.GetComponentsInChildren(true); + foreach (var skinnedMeshRenderer in skinnedMeshRenderers) + { + foreach (var material in skinnedMeshRenderer.sharedMaterials) + { + if (material != null) + { + materials.Add(material); + } + } + } + } + + return materials.Concat(_materialList).Distinct().ToList(); + } + bool CheckIsSupportedFormat(Material mat) + { + if (!TextureEncryptManager.IsSupportedFormat(mat)) + { + if (mat.mainTexture != null) + { + Debug.LogWarningFormat("{0} : is unsupported format", mat.mainTexture.name); + } + return false; + } + return true; + } + void CreateFolders() + { + OutputPaths paths = GetOutputPaths(); + paths.PrepareFolders(_assetWriter, _deleteFolders && AssetDatabase.IsValidFolder(paths.Avatar)); + } + + Texture2D GenerateMipRefTexture(string fileName, int size, bool useSmallMip) + { + var mip = TextureEncryptManager.GenerateRefMipmap(size, size, useSmallMip); + if (mip == null) + Debug.LogErrorFormat("{0} : Can't generate mip tex{1}.", fileName, size); + else + { + _assetWriter.CreateAssetInFolder(mip, GetOutputPaths().Folders.TexGuid, fileName); + _assetWriter.SaveAndRefresh(); + } + return mip; + } + ProcessedTexture? GenerateEncryptedTexture(OutputPaths paths, Material mat, IEncryptor encryptor, byte[] keyBytes) + { + Texture2D mainTexture = (Texture2D)mat.mainTexture; + + string texName1 = paths.EncryptedTextureName(mainTexture, 0); + string texName2 = paths.EncryptedTextureName(mainTexture, 2); + + bool processed = ProcessedTextures.ContainsKey(mainTexture); + ProcessedTexture processedTexture; + if (processed) + processedTexture = ProcessedTextures[mainTexture]; + else + { + processedTexture = new ProcessedTexture + { + Encrypted = new EncryptResult(), + Fallbacks = new List(), + FallbackOptions = new List(), + Nonce = new byte[12] + }; + } + + //Set chacha nonce + if (_algorithm == (int)Algorithm.Chacha) + { + Chacha20 chacha = encryptor as Chacha20; + if (!processed) + { + byte[] hashMat = KeyGenerator.GetHash(mat.GetInstanceID()); + for (int i = 0; i < chacha.Nonce.Length; ++i) + chacha.Nonce[i] ^= hashMat[i]; + Array.Copy(chacha.Nonce, 0, processedTexture.Nonce, 0, processedTexture.Nonce.Length); + } + else + { + byte[] nonce = ProcessedTextures[mainTexture].Nonce; + Array.Copy(nonce, 0, chacha.Nonce, 0, chacha.Nonce.Length); + } + } + + if (!processed) + { + EncryptResult encryptResult; + try + { + encryptResult = TextureEncryptManager.EncryptTexture(mainTexture, keyBytes, encryptor); + } + catch (ArgumentException e) + { + Debug.LogErrorFormat("{0} : ArgumentException - {1}", mainTexture.name, e.Message); + return null; + } + _assetWriter.CreateAssetInFolder(encryptResult.Texture1, paths.Folders.TexGuid, texName1); + if (encryptResult.Texture2 != null) + _assetWriter.CreateAssetInFolder(encryptResult.Texture2, paths.Folders.TexGuid, texName2); + + processedTexture.Encrypted = encryptResult; + + ProcessedTextures.Add(mainTexture, processedTexture); + } + + return processedTexture; + } + Texture2D GenerateFallbackTexture(string fileName, MatOption option, Texture2D mainTexture, ref ProcessedTexture processedTexture) + { + int fallbackOption = _fallback; + if (option != null) + fallbackOption = option.Fallback; + + int idx = processedTexture.FallbackOptions.FindIndex(option => option == fallbackOption); + Texture2D fallback = null; + if (idx == -1) + { + int fallbackSize = 32; + switch (fallbackOption) + { + case 0: // white + fallbackSize = 0; + break; + case 1: // black + fallbackSize = 1; + break; + case 2: + fallbackSize = 4; + break; + case 3: + fallbackSize = 8; + break; + case 4: + fallbackSize = 16; + break; + case 5: + fallbackSize = 32; + break; + case 6: + fallbackSize = 64; + break; + case 7: + fallbackSize = 128; + break; + } + if (fallbackSize > 1) + { + fallback = TextureEncryptManager.GenerateFallback(mainTexture, fallbackSize); + if (fallback != null) + { + processedTexture.Fallbacks.Add(fallback); + processedTexture.FallbackOptions.Add(fallbackOption); + _assetWriter.CreateAssetInFolder(fallback, GetOutputPaths().Folders.TexGuid, fileName); + _assetWriter.SaveAndRefresh(); + } + } + else + { + switch (fallbackSize) + { + case 0: + processedTexture.Fallbacks.Add(_fallbackWhite); + processedTexture.FallbackOptions.Add(fallbackOption); + fallback = _fallbackWhite; + break; + case 1: + processedTexture.Fallbacks.Add(_fallbackBlack); + processedTexture.FallbackOptions.Add(fallbackOption); + fallback = _fallbackBlack; + break; + } + } + } + else + fallback = processedTexture.Fallbacks[idx]; + + return fallback; + } + Material GenerateEncryptedMaterial(string fileName, Material mat, Shader encryptedShader, Texture2D fallback, Texture2D mip, AuxiliaryTextures otherTex, ProcessedTexture processedTexture, byte[] keyBytes, IEncryptor encryptor) + { + MaterialEncryptor materialEncryptor = new MaterialEncryptor(_assetWriter, _turnOnAllSafetyFallback, _algorithm, _rounds); + Material newMat = materialEncryptor.CreateEncryptedMaterial(GetOutputPaths().Folders.MatGuid, fileName, mat, encryptedShader, fallback, mip, otherTex, processedTexture, keyBytes, 16 - _keySize, encryptor, _injector); + Debug.LogFormat("{0} : create encrypted material : {1}", mat.name, AssetDatabase.GetAssetPath(newMat)); + + if (!EncryptedMaterials.ContainsKey(mat)) + EncryptedMaterials.Add(mat, newMat); + + return newMat; } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/ShellProtectorTester.cs b/Runtime/Scripts/ShellProtectorTester.cs index e986b57..df4c523 100644 --- a/Runtime/Scripts/ShellProtectorTester.cs +++ b/Runtime/Scripts/ShellProtectorTester.cs @@ -1,31 +1,31 @@ -#if UNITY_EDITOR -using System.Collections; -using System.Collections.Generic; -using System.Text; +#if UNITY_EDITOR using UnityEngine; +using UnityEngine.Serialization; using VRC.SDKBase; namespace Shell.Protector { public class ShellProtectorTester : MonoBehaviour, IEditorOnly { - public string lang = "eng"; - public int lang_idx = 0; - public int userKeyLength = 4; + [FormerlySerializedAs("lang")] + public string Language = "eng"; + [FormerlySerializedAs("langIdx")] + public int LanguageIndex; + [FormerlySerializedAs("userKeyLength")] + public int UserKeyLength = 4; - public ShellProtector protector; + [FormerlySerializedAs("protector")] + public ShellProtector Protector; public void CheckEncryption() { - if(protector == null) + if (Protector == null) { Debug.LogWarning("First, you need to set protector"); return; } - protector.SetMaterialFallbackValue(transform.root.gameObject, false); - - byte[] pwd_bytes = protector.GetKeyBytes(); + byte[] passwordBytes = Protector.GetKeyBytes(); var renderers = transform.root.GetComponentsInChildren(true); if (renderers != null) @@ -45,15 +45,15 @@ public void CheckEncryption() if (mat.name.Contains("_encrypted") || mat.name.Contains("_duplicated")) { for (int i = 0; i < 16; ++i) - mat.SetInt("_Key" + i, pwd_bytes[i]); + mat.SetInt(ShaderProperties.KeyPrefix + i, passwordBytes[i]); } } } } - var skinned_renderers = transform.root.GetComponentsInChildren(true); - if (skinned_renderers != null) + var skinnedRenderers = transform.root.GetComponentsInChildren(true); + if (skinnedRenderers != null) { - foreach (var r in skinned_renderers) + foreach (var r in skinnedRenderers) { var mats = r.sharedMaterials; if (mats == null) @@ -68,7 +68,7 @@ public void CheckEncryption() if (mat.name.Contains("_encrypted") || mat.name.Contains("_duplicated")) { for (int i = 0; i < 16; ++i) - mat.SetInt("_Key" + i, pwd_bytes[i]); + mat.SetInt(ShaderProperties.KeyPrefix + i, passwordBytes[i]); } } } @@ -76,8 +76,6 @@ public void CheckEncryption() } public void ResetEncryption() { - protector.SetMaterialFallbackValue(transform.root.gameObject, true); - var renderers = transform.root.GetComponentsInChildren(true); foreach (var r in renderers) { @@ -91,15 +89,15 @@ public void ResetEncryption() { if (mat == null) continue; - if (mat.name.Contains("_encrypted") || mat.name.Contains("_duplicated")) - { - for (int i = 16 - userKeyLength; i < 16; ++i) - mat.SetInt("_Key" + i, 0); - } + if (mat.name.Contains("_encrypted") || mat.name.Contains("_duplicated")) + { + for (int i = 16 - UserKeyLength; i < 16; ++i) + mat.SetInt(ShaderProperties.KeyPrefix + i, 0); + } } } - var skinned_renderers = transform.root.GetComponentsInChildren(true); - foreach (var r in skinned_renderers) + var skinnedRenderers = transform.root.GetComponentsInChildren(true); + foreach (var r in skinnedRenderers) { var mats = r.sharedMaterials; if (mats == null) @@ -111,14 +109,14 @@ public void ResetEncryption() { if (mat == null) continue; - if (mat.name.Contains("_encrypted") || mat.name.Contains("_duplicated")) - { - for (int i = 16 - userKeyLength; i < 16; ++i) - mat.SetInt("_Key" + i, 0); - } + if (mat.name.Contains("_encrypted") || mat.name.Contains("_duplicated")) + { + for (int i = 16 - UserKeyLength; i < 16; ++i) + mat.SetInt(ShaderProperties.KeyPrefix + i, 0); + } } } } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/Test.cs b/Runtime/Scripts/Test.cs index a68cb76..edb6fa1 100644 --- a/Runtime/Scripts/Test.cs +++ b/Runtime/Scripts/Test.cs @@ -1,25 +1,27 @@ using Shell.Protector; using UnityEngine; +namespace Shell.Protector +{ public class Test { public static void XXTEATest(string fixedKey, string userKey, int userKeySize) { - byte[] data_byte = new byte[12] { 255, 250, 245, 240, 235, 230, 225, 220, 215, 210, 205, 200 }; - byte[] key_byte = KeyGenerator.MakeKeyBytes(fixedKey, userKey, userKeySize); + byte[] dataBytes = new byte[12] { 255, 250, 245, 240, 235, 230, 225, 220, 215, 210, 205, 200 }; + byte[] keyBytes = KeyGenerator.MakeKeyBytes(fixedKey, userKey, userKeySize); uint[] data = new uint[3]; - data[0] = (uint)(data_byte[0] | (data_byte[1] << 8) | (data_byte[2] << 16) | (data_byte[3] << 24)); - data[1] = (uint)(data_byte[4] | (data_byte[5] << 8) | (data_byte[6] << 16) | (data_byte[7] << 24)); - data[2] = (uint)(data_byte[8] | (data_byte[9] << 8) | (data_byte[10] << 16) | (data_byte[11] << 24)); + data[0] = (uint)(dataBytes[0] | (dataBytes[1] << 8) | (dataBytes[2] << 16) | (dataBytes[3] << 24)); + data[1] = (uint)(dataBytes[4] | (dataBytes[5] << 8) | (dataBytes[6] << 16) | (dataBytes[7] << 24)); + data[2] = (uint)(dataBytes[8] | (dataBytes[9] << 8) | (dataBytes[10] << 16) | (dataBytes[11] << 24)); uint[] key = new uint[4]; - key[0] = (uint)(key_byte[0] | (key_byte[1] << 8) | (key_byte[2] << 16) | (key_byte[3] << 24)); - key[1] = (uint)(key_byte[4] | (key_byte[5] << 8) | (key_byte[6] << 16) | (key_byte[7] << 24)); - key[2] = (uint)(key_byte[8] | (key_byte[9] << 8) | (key_byte[10] << 16) | (key_byte[11] << 24)); - key[3] = (uint)(key_byte[12] | (key_byte[13] << 8) | (key_byte[14] << 16) | (key_byte[15] << 24)); + key[0] = (uint)(keyBytes[0] | (keyBytes[1] << 8) | (keyBytes[2] << 16) | (keyBytes[3] << 24)); + key[1] = (uint)(keyBytes[4] | (keyBytes[5] << 8) | (keyBytes[6] << 16) | (keyBytes[7] << 24)); + key[2] = (uint)(keyBytes[8] | (keyBytes[9] << 8) | (keyBytes[10] << 16) | (keyBytes[11] << 24)); + key[3] = (uint)(keyBytes[12] | (keyBytes[13] << 8) | (keyBytes[14] << 16) | (keyBytes[15] << 24)); - Debug.Log("Key bytes: " + string.Join(", ", key_byte)); + Debug.Log("Key bytes: " + string.Join(", ", keyBytes)); Debug.Log(string.Format("key1:{0}, key2:{1}, key3:{2}", key[0], key[1], key[2])); Debug.Log("Data: " + string.Join(", ", data)); @@ -32,20 +34,20 @@ public static void XXTEATest(string fixedKey, string userKey, int userKeySize) } public static void ChachaTest(string fixedKey, string userKey, int userKeySize) { - byte[] data_byte = new byte[8] { 255, 255, 245, 240, 235, 230, 225, 220 }; - byte[] key_byte = KeyGenerator.MakeKeyBytes(fixedKey, userKey, userKeySize); + byte[] dataBytes = new byte[8] { 255, 255, 245, 240, 235, 230, 225, 220 }; + byte[] keyBytes = KeyGenerator.MakeKeyBytes(fixedKey, userKey, userKeySize); uint[] data = new uint[2]; - data[0] = (uint)(data_byte[0] | (data_byte[1] << 8) | (data_byte[2] << 16) | (data_byte[3] << 24)); - data[1] = (uint)(data_byte[4] | (data_byte[5] << 8) | (data_byte[6] << 16) | (data_byte[7] << 24)); + data[0] = (uint)(dataBytes[0] | (dataBytes[1] << 8) | (dataBytes[2] << 16) | (dataBytes[3] << 24)); + data[1] = (uint)(dataBytes[4] | (dataBytes[5] << 8) | (dataBytes[6] << 16) | (dataBytes[7] << 24)); uint[] key = new uint[4]; - key[0] = (uint)(key_byte[0] | (key_byte[1] << 8) | (key_byte[2] << 16) | (key_byte[3] << 24)); - key[1] = (uint)(key_byte[4] | (key_byte[5] << 8) | (key_byte[6] << 16) | (key_byte[7] << 24)); - key[2] = (uint)(key_byte[8] | (key_byte[9] << 8) | (key_byte[10] << 16) | (key_byte[11] << 24)); - key[3] = (uint)(key_byte[12] | (key_byte[13] << 8) | (key_byte[14] << 16) | (key_byte[15] << 24)); + key[0] = (uint)(keyBytes[0] | (keyBytes[1] << 8) | (keyBytes[2] << 16) | (keyBytes[3] << 24)); + key[1] = (uint)(keyBytes[4] | (keyBytes[5] << 8) | (keyBytes[6] << 16) | (keyBytes[7] << 24)); + key[2] = (uint)(keyBytes[8] | (keyBytes[9] << 8) | (keyBytes[10] << 16) | (keyBytes[11] << 24)); + key[3] = (uint)(keyBytes[12] | (keyBytes[13] << 8) | (keyBytes[14] << 16) | (keyBytes[15] << 24)); - Debug.Log("Key bytes: " + string.Join(", ", key_byte)); + Debug.Log("Key bytes: " + string.Join(", ", keyBytes)); Debug.Log(string.Format("key1:{0}, key2:{1}, key3:{2}", key[0], key[1], key[2])); Debug.Log("Data: " + string.Join(", ", data)); @@ -56,3 +58,4 @@ public static void ChachaTest(string fixedKey, string userKey, int userKeySize) Debug.Log("Decrypted data: " + string.Join(", ", result)); } } +} diff --git a/Runtime/Scripts/TextureEncryptManager.cs b/Runtime/Scripts/TextureEncryptManager.cs new file mode 100644 index 0000000..05f9252 --- /dev/null +++ b/Runtime/Scripts/TextureEncryptManager.cs @@ -0,0 +1,175 @@ +using UnityEngine; +using System.Collections.Generic; +using System.Linq; + +#if UNITY_EDITOR + +namespace Shell.Protector +{ + public class TextureEncryptManager + { + private static readonly Dictionary _formats = new Dictionary { + { TextureFormat.DXT1, new DXT1Format() }, + { TextureFormat.DXT1Crunched, new DXT1Format() }, + { TextureFormat.DXT5, new DXT5Format() }, + { TextureFormat.DXT5Crunched, new DXT5Format() }, + { TextureFormat.RGB24, new RGB24Format() }, + { TextureFormat.RGBA32, new RGBA32Format() } + }; + + public static bool HasAlpha(Texture2D texture) + { + Color32[] pixels = texture.GetPixels32(); + for (int i = 0; i < pixels.Length; i++) + { + if (pixels[i].a != 255) return true; + } + return false; + } + + public static Texture2D GenerateRefMipmap(int width, int height, bool small = false) + { + int mip_lv = GetCanMipmapLevel(width, height); + Debug.LogFormat("mip {0}, {1} : {2}", width, height, mip_lv); + + Texture2D mip = new Texture2D(width, (small == false) ? height : 1, TextureFormat.RGB24, mip_lv, true); + mip.filterMode = FilterMode.Bilinear; + mip.anisoLevel = (small == false) ? 1 : 0; + + for (int m = 0; m < mip.mipmapCount; ++m) + { + Color32[] pixels_mip = mip.GetPixels32(m); + for (int i = 0; i < pixels_mip.Length; ++i) + { + pixels_mip[i].r = (byte)(m * 10); + pixels_mip[i].g = 0; + pixels_mip[i].b = 0; + } + mip.SetPixels32(pixels_mip, m); + } + if (small == false) + mip.Compress(false); + return mip; + } + + public static Texture2D GenerateFallback(Texture2D original, int size = 32) + { + if (original.width < 128 || original.height < 128) + { + return null; + } + TextureFormat format = TextureFormat.RGB24; + bool hasAlpha = HasAlpha(original); + if (hasAlpha) + format = TextureFormat.RGBA32; + + RenderTexture renderTexture = new RenderTexture(size, size, 0); + RenderTexture.active = renderTexture; + + Graphics.Blit(original, renderTexture); + + Texture2D resizedTexture = new Texture2D(size, size, format, true); + resizedTexture.ReadPixels(new Rect(0, 0, size, size), 0, 0); + resizedTexture.Apply(); + + resizedTexture.filterMode = FilterMode.Point; + resizedTexture.anisoLevel = 0; + resizedTexture.Compress(false); + if (hasAlpha) + resizedTexture.alphaIsTransparency = true; + + RenderTexture.active = null; + + return resizedTexture; + } + + private static int GetCanMipmapLevel(int w, int h) + { + if (w < 1 || h <= 1) return 0; + int w_level = (int)Mathf.Log(w, 2); + int h_level = (int)Mathf.Log(h, 2); + return Mathf.Max(w_level, h_level); + } + + private static ITextureFormat GetFormat(Texture texture) + { + if (texture == null) + { + return null; + } + + if (texture is not Texture2D texture2D) + { + return null; + } + + return _formats.FirstOrDefault(f => f.Value.CanHandle(texture2D.format)).Value; + } + + private static ITextureFormat GetFormat(Material material) + { + if (material == null || material.mainTexture == null) + { + return null; + } + + return GetFormat(material.mainTexture as Texture2D); + } + + public static EncryptResult EncryptTexture(Texture2D texture, byte[] key, IEncryptor encryptor) + { + if (texture.width % 2 != 0 || texture.height % 2 != 0) + { + Debug.LogErrorFormat("{0} : The texture size must be a multiple of 2!", texture.name); + return new EncryptResult(); + } + + var format = GetFormat(texture); + if (format == null) + { + Debug.LogErrorFormat("{0} is not supported texture format! supported type:DXT1, DXT5, RGB, RGBA", texture.name); + return new EncryptResult(); + } + + return format.Encrypt(texture, key, encryptor); + } + + public static bool IsSupportedFormat(Material material) + { + return GetFormat(material) != null; + } + + public static bool IsSupportedTexture(Texture texture) + { + return GetFormat(texture) != null; + } + + public static bool IsDXTFormat(TextureFormat format) + { + return format == TextureFormat.DXT1 || format == TextureFormat.DXT5; + } + + public static void SetFormatKeywords(Material material) + { + var format = GetFormat(material); + if (format == null) return; + format.SetFormatKeywords(material); + } + + public static void SetFormatKeywords(Material material, Texture texture) + { + var format = GetFormat(texture); + if (format == null) return; + format.SetFormatKeywords(material); + } + + public static (int, int) CalculateOffsets(Texture2D texture) + { + var format = GetFormat(texture); + if (format == null) return (0, 0); + return format.CalculateOffsets(texture); + } + } +} + +#endif diff --git a/Runtime/Scripts/TextureEncryptManager.cs.meta b/Runtime/Scripts/TextureEncryptManager.cs.meta new file mode 100644 index 0000000..a8291da --- /dev/null +++ b/Runtime/Scripts/TextureEncryptManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05268f803de0f0c47a4f086f06cdb2e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/TextureSettings.cs b/Runtime/Scripts/TextureSettings.cs index cba2108..0076dd2 100644 --- a/Runtime/Scripts/TextureSettings.cs +++ b/Runtime/Scripts/TextureSettings.cs @@ -1,59 +1,72 @@ -using System.IO; -using System.Text.RegularExpressions; +#if UNITY_EDITOR using UnityEditor; using UnityEngine; -#if UNITY_EDITOR - -public class TextureSettings +namespace Shell.Protector { - public static void SetRWEnableTexture(Texture2D texture) + public struct TextureImportChange { - if (texture.isReadable) - return; - string path = AssetDatabase.GetAssetPath(texture); - string meta = File.ReadAllText(path + ".meta"); - - meta = Regex.Replace(meta, "isReadable: 0", "isReadable: 1"); - File.WriteAllText(path + ".meta", meta); - - AssetDatabase.Refresh(); + public bool changed; + public bool readWriteChanged; + public bool crunchChanged; + public bool mipmapChanged; } - public static void SetCrunchCompression(Texture2D texture, bool crunch) + + public class TextureSettings { - string path = AssetDatabase.GetAssetPath(texture); - string meta = File.ReadAllText(path + ".meta"); + public static TextureImportChange SetRWEnableTexture(Texture2D texture) + { + return Apply(texture, readable: true, crunch: null, generateMipmaps: null); + } + + public static TextureImportChange SetCrunchCompression(Texture2D texture, bool crunch) + { + return Apply(texture, readable: null, crunch: crunch, generateMipmaps: null); + } + + public static TextureImportChange SetGenerateMipmap(Texture2D texture, bool generate) + { + return Apply(texture, readable: null, crunch: null, generateMipmaps: generate); + } - if (crunch == false) + static TextureImportChange Apply(Texture2D texture, bool? readable, bool? crunch, bool? generateMipmaps) { - if (texture.format == TextureFormat.DXT1Crunched) + TextureImportChange result = new TextureImportChange(); + if (texture == null) + return result; + + string path = AssetDatabase.GetAssetPath(texture); + if (string.IsNullOrEmpty(path)) + return result; + + TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; + if (importer == null) + return result; + + if (readable.HasValue && importer.isReadable != readable.Value) { - int format = 10; - meta = Regex.Replace(meta, "textureFormat: \\d+", "textureFormat: " + format); + importer.isReadable = readable.Value; + result.readWriteChanged = true; } - else if (texture.format == TextureFormat.DXT5Crunched) + + if (crunch.HasValue && importer.crunchedCompression != crunch.Value) { - int format = 12; - meta = Regex.Replace(meta, "textureFormat: \\d+", "textureFormat: " + format); + importer.crunchedCompression = crunch.Value; + result.crunchChanged = true; } - } - int enable = crunch ? 1 : 0; - meta = Regex.Replace(meta, "crunchedCompression: \\d+", "crunchedCompression: " + enable); - File.WriteAllText(path + ".meta", meta); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - public static void SetGenerateMipmap(Texture2D texture, bool generate) - { - string path = AssetDatabase.GetAssetPath(texture); - string meta = File.ReadAllText(path + ".meta"); + if (generateMipmaps.HasValue && importer.mipmapEnabled != generateMipmaps.Value) + { + importer.mipmapEnabled = generateMipmaps.Value; + result.mipmapChanged = true; + } - int enable = generate ? 1 : 0; - meta = Regex.Replace(meta, "enableMipMap: \\d+", "enableMipMap: " + enable); - File.WriteAllText(path + ".meta", meta); + result.changed = result.readWriteChanged || result.crunchChanged || result.mipmapChanged; + if (result.changed) + importer.SaveAndReimport(); - AssetDatabase.Refresh(); + return result; + } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Scripts/VersionManager.cs b/Runtime/Scripts/VersionManager.cs index bbd6819..7cb41ae 100644 --- a/Runtime/Scripts/VersionManager.cs +++ b/Runtime/Scripts/VersionManager.cs @@ -1,113 +1,84 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using System.Collections; -using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; using UnityEngine.Networking; -public class VersionManager : MonoBehaviour +namespace Shell.Protector { - string version = ""; - string download_uri = ""; - string github_version = ""; - - static VersionManager instance; - const string github_url = "https://github.com/Shell4026/ShellProtector"; - const string version_uri = github_url + "/raw/main/version.json"; - string raw_data; - - public static VersionManager GetInstance() + public class VersionManager : MonoBehaviour { - if(instance == null) + string githubVersion = ""; + + static VersionManager instance; + const string githubUrl = "https://github.com/Shell4026/ShellProtector"; + const string versionUri = githubUrl + "/raw/main/version.json"; + + public static VersionManager GetInstance() { - GameObject obj = new GameObject("VersionManager"); - obj.hideFlags = HideFlags.HideAndDontSave; - instance = obj.AddComponent(); + if (instance == null) + { + GameObject obj = new GameObject("VersionManager"); + obj.hideFlags = HideFlags.HideAndDontSave; + instance = obj.AddComponent(); + } + return instance; } - return instance; - } - private string[] ParseVersionJson(string data) - { - Match match = Regex.Match(data, "{ \"latestVersion\": \"(.*?)\", \"downloadPage\": \"(.*?)\" }"); - string version = "", download_uri = ""; - if (match.Success) + private string[] ParseVersionJson(string data) { - version = match.Groups[1].Value; - download_uri = match.Groups[2].Value; + Match match = Regex.Match(data, "{ \"latestVersion\": \"(.*?)\", \"downloadPage\": \"(.*?)\" }"); + string version = "", downloadUri = ""; + if (match.Success) + { + version = match.Groups[1].Value; + downloadUri = match.Groups[2].Value; + } + return new[] { version, downloadUri }; } - return new[]{ version, download_uri }; - } - public string GetVersion() - { - MonoScript monoScript = MonoScript.FromMonoBehaviour(this); - string script_path = Directory.GetParent(AssetDatabase.GetAssetPath(monoScript)).ToString(); - string dir = Path.GetDirectoryName(Path.GetDirectoryName(script_path)); - string data = File.ReadAllText(Path.Combine(dir, "version.json")); - string[] parse = ParseVersionJson(data); - return parse[0]; - } - public string GetGithubVersion() - { - return github_version; - } - public void Refresh() - { - StartCoroutine(LoadData()); - } - private IEnumerator LoadData() - { - using (UnityWebRequest www = UnityWebRequest.Get(version_uri)) + public string GetVersion() { - yield return www.SendWebRequest(); + MonoScript monoScript = MonoScript.FromMonoBehaviour(this); + string scriptPath = Directory.GetParent(AssetDatabase.GetAssetPath(monoScript)).ToString(); + string dir = Path.GetDirectoryName(Path.GetDirectoryName(scriptPath)); - if (!www.isNetworkError) - { - string json_data = www.downloadHandler.text; - string[] parse = ParseVersionJson(json_data); + string data = File.ReadAllText(Path.Combine(dir, "version.json")); + string[] parse = ParseVersionJson(data); + return parse[0]; + } - github_version = parse[0]; - download_uri = parse[1]; - } - else - { - Debug.Log("[ShellProtector]version checking error: " + www.error); - } + public string GetGithubVersion() + { + return githubVersion; } - } - private IEnumerator DownloadNewVersion() - { - if (download_uri == null) + + public void Refresh() { - yield return StartCoroutine(LoadData()); + StartCoroutine(LoadData()); } - while (true) + private IEnumerator LoadData() { - if (download_uri != null) + using (UnityWebRequest www = UnityWebRequest.Get(versionUri)) { - using (UnityWebRequest www = UnityWebRequest.Get(version_uri)) - { - yield return www.SendWebRequest(); + yield return www.SendWebRequest(); - if (!www.isNetworkError) - { - string json_data = www.downloadHandler.text; - string[] parse = ParseVersionJson(json_data); + if (www.result == UnityWebRequest.Result.Success) + { + string jsonData = www.downloadHandler.text; + string[] parse = ParseVersionJson(jsonData); - github_version = parse[0]; - download_uri = parse[1]; - } - else - { - Debug.Log("[ShellProtector]version checking error: " + www.error); - } + githubVersion = parse[0]; + } + else + { + Debug.Log("[ShellProtector]version checking error: " + www.error); } } } } } -#endif \ No newline at end of file +#endif diff --git a/Runtime/Shader.meta b/Runtime/Shader.meta new file mode 100644 index 0000000..2afdcbd --- /dev/null +++ b/Runtime/Shader.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 599f7af837cb58b41b622c0616b8fedf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Shader/Chacha.cginc b/Runtime/Shader/Chacha.cginc new file mode 100644 index 0000000..552af84 --- /dev/null +++ b/Runtime/Shader/Chacha.cginc @@ -0,0 +1,87 @@ +#pragma once +static const uint CHACHA20_ROUNDS = 8; +uint _Nonce0; +uint _Nonce1; +uint _Nonce2; + +uint Rotl32(uint x, int n) +{ + return x << n | (x >> (32 - n)); +} + +void ChaChaQuarterRound(inout uint a, inout uint b, inout uint c, inout uint d) +{ + a += b; d = Rotl32(d ^ a, 16u); + c += d; b = Rotl32(b ^ c, 12u); + a += b; d = Rotl32(d ^ a, 8u); + c += d; b = Rotl32(b ^ c, 7u); +} + +void Chacha20QuarterRound(inout uint state[16], int a, int b, int c, int d) +{ + state[a] += state[b]; state[d] = Rotl32(state[d] ^ state[a], 16); + state[c] += state[d]; state[b] = Rotl32(state[b] ^ state[c], 12); + state[a] += state[b]; state[d] = Rotl32(state[d] ^ state[a], 8); + state[c] += state[d]; state[b] = Rotl32(state[b] ^ state[c], 7); +} + +uint3 ChaCha8KeyStream3(const uint key[4]) +{ + uint x0 = 0x61707865u; + uint x1 = 0x3320646eu; + uint x2 = 0x79622d32u; + uint x3 = 0x6b206574u; + + uint x4 = key[0]; + uint x5 = key[1]; + uint x6 = key[2]; + uint x7 = key[3]; + + uint x8 = key[0]; + uint x9 = key[1]; + uint x10 = key[2]; + uint x11 = key[3]; + + uint x12 = 1u; + uint x13 = _Nonce0; + uint x14 = _Nonce1; + uint x15 = _Nonce2; + + [unroll] + for (uint round = 0u; round < CHACHA20_ROUNDS; round += 2u) + { + // Column round + ChaChaQuarterRound(x0, x4, x8, x12); + ChaChaQuarterRound(x1, x5, x9, x13); + ChaChaQuarterRound(x2, x6, x10, x14); + ChaChaQuarterRound(x3, x7, x11, x15); + + // Diagonal round + ChaChaQuarterRound(x0, x5, x10, x15); + ChaChaQuarterRound(x1, x6, x11, x12); + ChaChaQuarterRound(x2, x7, x8, x13); + ChaChaQuarterRound(x3, x4, x9, x14); + } + + return uint3( + x0 + 0x61707865u, + x1 + 0x3320646eu, + x2 + 0x79622d32u + ); +} + +void Decrypt(inout uint data[2], const uint key[4]) +{ + uint3 stream = ChaCha8KeyStream3(key); + data[0] ^= stream.x; + data[1] ^= stream.y; +} + +void Decrypt(inout uint data[3], const uint key[4]) +{ + uint3 stream = ChaCha8KeyStream3(key); + + data[0] ^= stream.x; + data[1] ^= stream.y; + data[2] ^= stream.z; +} \ No newline at end of file diff --git a/Runtime/Chacha.cginc.meta b/Runtime/Shader/Chacha.cginc.meta similarity index 100% rename from Runtime/Chacha.cginc.meta rename to Runtime/Shader/Chacha.cginc.meta diff --git a/Runtime/Shader/DXT.cginc b/Runtime/Shader/DXT.cginc new file mode 100644 index 0000000..5005faf --- /dev/null +++ b/Runtime/Shader/DXT.cginc @@ -0,0 +1,65 @@ +#pragma once + +#define _SHELL_PROTECTOR_DATA_LENGTH 2 +#define _SHELL_PROTECTOR_INDEX_ALIGNMENT 1 + +int GetBlockIndex(float2 uv, int m) +{ + const float x = frac(uv.x); + const float y = frac(uv.y); + + const uint encW = mw[m + _Woffset]; + const uint encH = mh[m + _Hoffset]; + + return (encW * floor(y * encH)) + floor(x * encW); +} + +void GetData(Texture2D tex1, SamplerState tex0Sampler, inout uint data[2], float2 uv, int m) +{ + int idx = GetBlockIndex(uv, m); + int offset = (idx & 1) == 0 ? 0 : -1; + + float4 pixels[2]; + pixels[0] = tex1.SampleLevel(tex0Sampler, GetUV(idx + 0 + offset, m, _Woffset, _Hoffset), m); + pixels[1] = tex1.SampleLevel(tex0Sampler, GetUV(idx + 1 + offset, m, _Woffset, _Hoffset), m); + + data[0] = ((uint)round(pixels[0].r * 255.0) | ((uint)round(pixels[0].g * 255.0) << 8) | ((uint)round(pixels[0].b * 255.0) << 16) | ((uint)round(pixels[0].a * 255.0) << 24)); + data[1] = ((uint)round(pixels[1].r * 255.0) | ((uint)round(pixels[1].g * 255.0) << 8) | ((uint)round(pixels[1].b * 255.0) << 16) | ((uint)round(pixels[1].a * 255.0) << 24)); +} + +float4 GetPixel(Texture2D tex0, SamplerState tex0Sampler, in uint data[2], float2 uv, int m) +{ + int idx = GetBlockIndex(uv, m); + int offset = (idx & 1) == 0 ? 0 : -1; + + float4 col = tex0.SampleLevel(tex0Sampler, uv, m); + uint r = (data[idx & 1] & 0x000000FF) >> 0; + uint g = (data[idx & 1] & 0x0000FF00) >> 8; + uint b = (data[idx & 1] & 0x00FF0000) >> 16; + uint a = (data[idx & 1] & 0xFF000000) >> 24; + + uint color1 = (r | g << 8); + uint color2 = (b | a << 8); + + uint color1_r = (color1 & 0xF800) >> 11; + color1_r = color1_r << 3 | color1_r >> 2; + uint color1_g = (color1 & 0x7E0) >> 5; + color1_g = color1_g << 2 | color1_g >> 4; + uint color1_b= color1 & 0x1F; + color1_b = color1_b << 3 | color1_b >> 2; + + uint color2_r = (color2 & 0xF800) >> 11; + color2_r = color2_r << 3 | color2_r >> 2; + uint color2_g = (color2 & 0x7E0) >> 5; + color2_g = color2_g << 2 | color2_g >> 4; + uint color2_b= color2 & 0x1F; + color2_b = color2_b << 3 | color2_b >> 2; + + float3 col1 = float3(color1_r / 255.0, color1_g / 255.0, color1_b / 255.0); + float3 col2 = float3(color2_r / 255.0, color2_g / 255.0, color2_b / 255.0); + + float3 result; + result = lerp(col2, col1, color1 > color2 ? col.rgb : 0.5); + + return half4(GammaCorrection(result), col.a); +} \ No newline at end of file diff --git a/Runtime/DecryptChacha.cginc.meta b/Runtime/Shader/DXT.cginc.meta similarity index 75% rename from Runtime/DecryptChacha.cginc.meta rename to Runtime/Shader/DXT.cginc.meta index 65c8587..f9a14de 100644 --- a/Runtime/DecryptChacha.cginc.meta +++ b/Runtime/Shader/DXT.cginc.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 8415b59f27260de40b4bf81e1a40a220 +guid: a9274845e18784e469fcc9181c739619 ShaderIncludeImporter: externalObjects: {} userData: diff --git a/Runtime/Shader/Protector.cginc b/Runtime/Shader/Protector.cginc new file mode 100644 index 0000000..6d3fcbc --- /dev/null +++ b/Runtime/Shader/Protector.cginc @@ -0,0 +1,187 @@ +#pragma once + +#pragma shader_feature_local _SHELL_PROTECTOR_XXTEA +#pragma shader_feature_local _SHELL_PROTECTOR_CHACHA +#pragma shader_feature_local _SHELL_PROTECTOR_FORMAT0 +#pragma shader_feature_local _SHELL_PROTECTOR_FORMAT1 +#pragma shader_feature_local _SHELL_PROTECTOR_RIMLIGHT + +// Unity also compiles a no-keyword variant while importing shaders. +#if !_SHELL_PROTECTOR_XXTEA && !_SHELL_PROTECTOR_CHACHA + #define _SHELL_PROTECTOR_CHACHA +#endif + +// Format keywords +#if !_SHELL_PROTECTOR_FORMAT0 && !_SHELL_PROTECTOR_FORMAT1 + #define _SHELL_PROTECTOR_DXT +#elif !_SHELL_PROTECTOR_FORMAT0 && _SHELL_PROTECTOR_FORMAT1 + #define _SHELL_PROTECTOR_RGBA +#elif _SHELL_PROTECTOR_FORMAT0 && !_SHELL_PROTECTOR_FORMAT1 + #define _SHELL_PROTECTOR_RGB +#else + #error "Unsupported format" +#endif + +static const uint mw[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; +static const uint mh[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; + +float _Key0, _Key1, _Key2, _Key3, _Key4, _Key5, _Key6, _Key7, _Key8, _Key9, _Key10, _Key11, _Key12, _Key13, _Key14, _Key15; + +uint _Woffset; +uint _Hoffset; +uint _HashMagic; + +#include "Utility.cginc" + +// Ciphers +#ifdef _SHELL_PROTECTOR_XXTEA + #include "XXTEA.cginc" +#endif + +#ifdef _SHELL_PROTECTOR_CHACHA + #include "Chacha.cginc" +#endif + +#ifdef _SHELL_PROTECTOR_RGB + #include "RGB.cginc" +#endif + +#ifdef _SHELL_PROTECTOR_RGBA + #include "RGBA.cginc" +#endif + +#ifdef _SHELL_PROTECTOR_DXT + #include "DXT.cginc" +#endif + +void DecryptData(inout uint data[_SHELL_PROTECTOR_DATA_LENGTH], Texture2D tex0, Texture2D tex1, SamplerState tex0Sampler, float2 uv, int m) +{ + #ifdef _SHELL_PROTECTOR_DXT + const int idx = GetBlockIndex(uv, m); +#else + const int idx = GetIndex(uv, m); +#endif + const uint key[4] = + { + ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), + ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), + ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), + ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> _SHELL_PROTECTOR_INDEX_ALIGNMENT) << _SHELL_PROTECTOR_INDEX_ALIGNMENT) + }; +#ifdef _SHELL_PROTECTOR_DXT + GetData(tex1, tex0Sampler, data, uv, m); +#else + GetData(tex0, tex0Sampler, data, uv, m); +#endif + Decrypt(data, key); +} + +float4 DecryptTexture(Texture2D tex0, Texture2D tex1, SamplerState tex0Sampler, float2 uv, int m) +{ + uint data[_SHELL_PROTECTOR_DATA_LENGTH]; + DecryptData(data, tex0, tex1, tex0Sampler, uv, m); + return GetPixel(tex0, tex0Sampler, data, uv, m); +} + +float4 DecryptTextureBox(Texture2D tex0, Texture2D tex1, SamplerState texSampler, float4 texSize, Texture2D mipTex, SamplerState mipSamp, float2 uv) +{ + float4 mipPixel = mipTex.Sample(mipSamp, uv); + + int mip = round(mipPixel.r * 255 / 10); //fucking precision problems + const int m[13] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10 }; // max size 4k + + float4 c00 = DecryptTexture(tex0, tex1, texSampler, uv, m[mip]); + + return c00; +} + +float4 DecryptTextureBilinear(Texture2D tex0, Texture2D tex1, SamplerState texSampler, float4 originalTexSize, Texture2D mipTex, SamplerState mipSamp, float2 uv) +{ + const float4 mipPixel = mipTex.Sample(mipSamp, uv); + const float2 uvUnit = originalTexSize.xy; + //bilinear interpolation + const float2 uvBilinear = uv - 0.5 * uvUnit; + const int mip = round(mipPixel.r * 255 / 10); //fucking precision problems + const int m[13] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10 }; // max size 4k + + const float2 uv00 = uvBilinear + float2(uvUnit.x * 0, uvUnit.y * 0); + const float2 uv10 = uvBilinear + float2(uvUnit.x * 1, uvUnit.y * 0); + const float2 uv01 = uvBilinear + float2(uvUnit.x * 0, uvUnit.y * 1); + const float2 uv11 = uvBilinear + float2(uvUnit.x * 1, uvUnit.y * 1); + +#ifdef _SHELL_PROTECTOR_DXT + const int idx00 = GetBlockIndex(uv00, m[mip]); + const int idx10 = GetBlockIndex(uv10, m[mip]); + const int idx01 = GetBlockIndex(uv01, m[mip]); + const int idx11 = GetBlockIndex(uv11, m[mip]); + if ((idx00 == idx10) && (idx10 == idx01) && (idx01 == idx11)) + { + uint data[_SHELL_PROTECTOR_DATA_LENGTH]; + DecryptData(data, tex0, tex1, texSampler, uv00, m[mip]); + + const float4 c00 = GetPixel(tex0, texSampler, data, uv00, m[mip]); + const float4 c10 = GetPixel(tex0, texSampler, data, uv10, m[mip]); + const float4 c01 = GetPixel(tex0, texSampler, data, uv01, m[mip]); + const float4 c11 = GetPixel(tex0, texSampler, data, uv11, m[mip]); + const float2 f = frac(uvBilinear * originalTexSize.zw); + const float4 c0 = lerp(c00, c10, f.x); + const float4 c1 = lerp(c01, c11, f.x); + const float4 bilinear = lerp(c0, c1, f.y); + return bilinear; + } +#endif + + float4 c00 = DecryptTexture(tex0, tex1, texSampler, uv00, m[mip]); + float4 c10 = DecryptTexture(tex0, tex1, texSampler, uv10, m[mip]); + float4 c01 = DecryptTexture(tex0, tex1, texSampler, uv01, m[mip]); + float4 c11 = DecryptTexture(tex0, tex1, texSampler, uv11, m[mip]); + + float2 f = frac(uvBilinear * originalTexSize.zw); + + float4 c0 = lerp(c00, c10, f.x); + float4 c1 = lerp(c01, c11, f.x); + + float4 bilinear = lerp(c0, c1, f.y); + + return bilinear; +} + +inline uint SimpleHash(int data[16]) +{ + uint hash = 0x811C9DC5u; + hash *= _HashMagic; + + [unroll] + for (int i = 0; i < 16; i++) + { + uint k = (uint)(data[i] & 0xFF); + + k *= 0xcc9e2d51u; + k = (k << 15) | (k >> 17); + k *= 0x1b873593u; + + hash ^= k; + hash = (hash << 13) | (hash >> 19); + hash = hash * 5u + 0xe6546b64u; + } + + hash ^= 16u; + hash ^= (hash >> 16); + hash *= 0x85ebca6bu; + hash ^= (hash >> 13); + hash *= 0xc2b2ae35u; + hash ^= (hash >> 16); + + return hash; +} + +inline bool IsDecrypted() { + const int key[16] = + { + (int)round(_Key0), (int)round(_Key1), (int)round(_Key2), (int)round(_Key3), + (int)round(_Key4), (int)round(_Key5), (int)round(_Key6), (int)round(_Key7), + (int)round(_Key8), (int)round(_Key9), (int)round(_Key10), (int)round(_Key11), + (int)round(_Key12), (int)round(_Key13), (int)round(_Key14), (int)round(_Key15) + }; + return SimpleHash(key) == _PasswordHash; +} diff --git a/Runtime/liltoonProtector/Shaders/Decrypt.cginc.meta b/Runtime/Shader/Protector.cginc.meta similarity index 75% rename from Runtime/liltoonProtector/Shaders/Decrypt.cginc.meta rename to Runtime/Shader/Protector.cginc.meta index 290c859..c108dad 100644 --- a/Runtime/liltoonProtector/Shaders/Decrypt.cginc.meta +++ b/Runtime/Shader/Protector.cginc.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 93face655656f7a4d807abb62b9443c6 +guid: 49cf558e431d30f47bc14bde24eb9d49 ShaderIncludeImporter: externalObjects: {} userData: diff --git a/Runtime/Shader/RGB.cginc b/Runtime/Shader/RGB.cginc new file mode 100644 index 0000000..6f02f95 --- /dev/null +++ b/Runtime/Shader/RGB.cginc @@ -0,0 +1,40 @@ +#pragma once + +#define _SHELL_PROTECTOR_DATA_LENGTH 3 +#define _SHELL_PROTECTOR_INDEX_ALIGNMENT 2 + +int GetIndex(half2 uv, int m) { + half x = frac(uv.x); + half y = frac(uv.y); + + return (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); +} + +void GetData(Texture2D tex1, SamplerState tex0Sampler, inout uint data[3], half2 uv, int m) { + const int pos[4] = { 0, -1, -2, -3 }; + int idx = GetIndex(uv, m); + int offset = pos[idx % 4]; + + float3 pixels[4]; + pixels[0] = tex1.SampleLevel(tex0Sampler, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); + pixels[1] = tex1.SampleLevel(tex0Sampler, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); + pixels[2] = tex1.SampleLevel(tex0Sampler, GetUV(idx + 2 + offset, m, (uint)_Woffset, _Hoffset), m); + pixels[3] = tex1.SampleLevel(tex0Sampler, GetUV(idx + 3 + offset, m, (uint)_Woffset, _Hoffset), m); + + data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[1].r * 255.0f) << 24)); + data[1] = ((uint)round(pixels[1].g * 255.0f) | ((uint)round(pixels[1].b * 255.0f) << 8) | ((uint)round(pixels[2].r * 255.0f) << 16) | ((uint)round(pixels[2].g * 255.0f) << 24)); + data[2] = ((uint)round(pixels[2].b * 255.0f) | ((uint)round(pixels[3].r * 255.0f) << 8) | ((uint)round(pixels[3].g * 255.0f) << 16) | ((uint)round(pixels[3].b * 255.0f) << 24)); +} + +half4 GetPixel(Texture2D tex0, SamplerState tex0Sampler, inout uint data[3], half2 uv, int m) { + const int pos[4] = { 0, -1, -2, -3 }; + int idx = GetIndex(uv, m); + int offset = pos[idx % 4]; + + half r[4] = { (data[0] & 0x000000FF)/255.0f, ((data[0] & 0xFF000000) >> 24)/255.0f, ((data[1] & 0x00FF0000) >> 16)/255.0f, ((data[2] & 0x0000FF00) >> 8)/255.0f }; + half g[4] = { ((data[0] & 0x0000FF00) >> 8)/255.0f, ((data[1] & 0x000000FF) >> 0)/255.0f, ((data[1] & 0xFF000000) >> 24)/255.0f, ((data[2] & 0x00FF0000) >> 16)/255.0f }; + half b[4] = { ((data[0] & 0x00FF0000) >> 16)/255.0f, ((data[1] & 0x0000FF00) >> 8)/255.0f, ((data[2] & 0x000000FF) >> 0)/255.0f, ((data[2] & 0xFF000000) >> 24)/255.0f }; + float3 decrypt = float3(r[idx % 4], g[idx % 4], b[idx % 4]); + + return half4(GammaCorrection(decrypt), 1.0); +} diff --git a/Runtime/liltoonProtector/Shaders/DecryptChacha.cginc.meta b/Runtime/Shader/RGB.cginc.meta similarity index 75% rename from Runtime/liltoonProtector/Shaders/DecryptChacha.cginc.meta rename to Runtime/Shader/RGB.cginc.meta index 0326a0e..e418a56 100644 --- a/Runtime/liltoonProtector/Shaders/DecryptChacha.cginc.meta +++ b/Runtime/Shader/RGB.cginc.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 6d4b1b13948575f48ba55aebb4d099b1 +guid: e28ddfbe363ef0c4da64b16b00ecd55d ShaderIncludeImporter: externalObjects: {} userData: diff --git a/Runtime/Shader/RGBA.cginc b/Runtime/Shader/RGBA.cginc new file mode 100644 index 0000000..3db8984 --- /dev/null +++ b/Runtime/Shader/RGBA.cginc @@ -0,0 +1,34 @@ +#pragma once + +#define _SHELL_PROTECTOR_DATA_LENGTH 2 +#define _SHELL_PROTECTOR_INDEX_ALIGNMENT 1 + +int GetIndex(float2 uv, int m) { + half x = frac(uv.x); + half y = frac(uv.y); + + return (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); +} + +void GetData(Texture2D tex1, SamplerState tex0Sampler, inout uint data[2], float2 uv, int m) { + int idx = GetIndex(uv, m); + int offset = (idx & 1) == 0 ? 0 : -1; + + float4 pixels[2]; + pixels[0] = tex1.SampleLevel(tex0Sampler, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); + pixels[1] = tex1.SampleLevel(tex0Sampler, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); + + data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); + data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); +} + +float4 GetPixel(Texture2D tex0, SamplerState tex0Sampler, inout uint data[2], float2 uv, int m) { + int idx = GetIndex(uv, m); + half r = ((data[idx & 1] & 0x000000FF) >> 0)/255.0f; + half g = ((data[idx & 1] & 0x0000FF00) >> 8)/255.0f; + half b = ((data[idx & 1] & 0x00FF0000) >> 16)/255.0f; + half a = ((data[idx & 1] & 0xFF000000) >> 24)/255.0f; + float4 decrypt = float4(r, g, b, a); + + return float4(GammaCorrection(decrypt.rgb), decrypt.a); +} diff --git a/Runtime/liltoonProtector/Shaders/Chacha.cginc.meta b/Runtime/Shader/RGBA.cginc.meta similarity index 75% rename from Runtime/liltoonProtector/Shaders/Chacha.cginc.meta rename to Runtime/Shader/RGBA.cginc.meta index 903c703..1f34279 100644 --- a/Runtime/liltoonProtector/Shaders/Chacha.cginc.meta +++ b/Runtime/Shader/RGBA.cginc.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: bac1c2f2fa5382f4db5d3eb76b0bbde0 +guid: 29d308e1129c5f447913ca44292e60cd ShaderIncludeImporter: externalObjects: {} userData: diff --git a/Runtime/Shader/Utility.cginc b/Runtime/Shader/Utility.cginc new file mode 100644 index 0000000..7327664 --- /dev/null +++ b/Runtime/Shader/Utility.cginc @@ -0,0 +1,21 @@ +#pragma once + +float3 InverseGammaCorrection(float3 rgb) +{ + float3 result = pow(rgb, 0.454545); + return result; +} + +float3 GammaCorrection(float3 rgb) +{ + //half3 result = pow(rgb, 2.2); + float3 result = rgb * rgb * (rgb * (half)0.2 + (half)0.8); //fast pow + return result; +} + +float2 GetUV(int idx, int m, int woffset = 0, int hoffset = 0) +{ + int w = idx % mw[m + woffset]; + int h = idx / mw[m + woffset]; + return float2((float)w/mw[m + woffset], (float)h/mh[m + hoffset]); +} diff --git a/Runtime/Shader/Utility.cginc.meta b/Runtime/Shader/Utility.cginc.meta new file mode 100644 index 0000000..02950d4 --- /dev/null +++ b/Runtime/Shader/Utility.cginc.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4f490184e4fcfe648ac53cb7f0f74bb3 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XXTEA.cginc b/Runtime/Shader/XXTEA.cginc similarity index 70% rename from Runtime/XXTEA.cginc rename to Runtime/Shader/XXTEA.cginc index 2395f12..a4fbd99 100644 --- a/Runtime/XXTEA.cginc +++ b/Runtime/Shader/XXTEA.cginc @@ -1,18 +1,23 @@ #pragma once static const uint Delta = 0x9e3779b9; -static const uint ROUNDS = 6; -static const uint SUM = Delta * ROUNDS; +uint _Rounds; -void XXTEADecrypt(inout uint data[3], const uint key[4]) +uint GetXXTEARounds(uint n) +{ + return _Rounds == 0 ? 6 + 52 / n : _Rounds; +} + +void Decrypt(inout uint data[3], const uint key[4]) { static const uint n = 3; uint v0, v1, sum; uint p, e; - sum = SUM; + uint rounds = GetXXTEARounds(n); + sum = Delta * rounds; v0 = data[0]; - for(int i = 0; i < ROUNDS; ++i) + for(uint i = 0; i < rounds; ++i) { e = (sum >> 2) & 3; for (p = n-1; p > 0; p--) @@ -27,16 +32,18 @@ void XXTEADecrypt(inout uint data[3], const uint key[4]) sum -= Delta; } } -void XXTEADecrypt(inout uint data[2], const uint key[4]) + +void Decrypt(inout uint data[2], const uint key[4]) { static const uint n = 2; uint v0, v1, sum; uint p, e; - sum = SUM; + uint rounds = GetXXTEARounds(n); + sum = Delta * rounds; v0 = data[0]; - for(int i = 0; i < ROUNDS; ++i) + for(uint i = 0; i < rounds; ++i) { e = (sum >> 2) & 3; for (p = n-1; p > 0; p--) @@ -50,4 +57,4 @@ void XXTEADecrypt(inout uint data[2], const uint key[4]) v0 = data[0]; sum -= Delta; } -} \ No newline at end of file +} diff --git a/Runtime/XXTEA.cginc.meta b/Runtime/Shader/XXTEA.cginc.meta similarity index 100% rename from Runtime/XXTEA.cginc.meta rename to Runtime/Shader/XXTEA.cginc.meta diff --git a/Runtime/liltoonProtector/Shaders/Chacha.cginc b/Runtime/liltoonProtector/Shaders/Chacha.cginc deleted file mode 100644 index 3aba812..0000000 --- a/Runtime/liltoonProtector/Shaders/Chacha.cginc +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once -static const uint CHACHA20_ROUNDS = 8; -uint _Nonce0; -uint _Nonce1; -uint _Nonce2; - -uint Rotl32(uint x, int n) -{ - return x << n | (x >> (32 - n)); -} - -void Chacha20QuarterRound(inout uint state[16], int a, int b, int c, int d) -{ - state[a] += state[b]; state[d] = Rotl32(state[d] ^ state[a], 16); - state[c] += state[d]; state[b] = Rotl32(state[b] ^ state[c], 12); - state[a] += state[b]; state[d] = Rotl32(state[d] ^ state[a], 8); - state[c] += state[d]; state[b] = Rotl32(state[b] ^ state[c], 7); -} - -void Chacha20XOR(inout uint data[2], const uint key[4]) -{ - uint4 state[4]; - state[0] = uint4(0x61707865, 0x3320646e, 0x79622d32, 0x6b206574); - state[1] = uint4(key[0], key[1], key[2], key[3]); - state[2] = state[1]; - state[3] = uint4(1, _Nonce0, _Nonce1, _Nonce2); - - uint block[16]; - //block - [unroll] - int i = 0; - for(i = 0; i < 4; ++i) - { - block[i * 4 + 0] = state[i].x; - block[i * 4 + 1] = state[i].y; - block[i * 4 + 2] = state[i].z; - block[i * 4 + 3] = state[i].w; - } - [unroll] - for (i = 0; i < CHACHA20_ROUNDS; i += 2) - { - Chacha20QuarterRound(block, 0, 4, 8, 12); - Chacha20QuarterRound(block, 1, 5, 9, 13); - Chacha20QuarterRound(block, 2, 6, 10, 14); - Chacha20QuarterRound(block, 3, 7, 11, 15); - Chacha20QuarterRound(block, 0, 5, 10, 15); - Chacha20QuarterRound(block, 1, 6, 11, 12); - Chacha20QuarterRound(block, 2, 7, 8, 13); - Chacha20QuarterRound(block, 3, 4, 9, 14); - } - [unroll] - for(i = 0; i < 4; ++i) - { - block[i * 4 + 0] += state[i].x; - block[i * 4 + 1] += state[i].y; - block[i * 4 + 2] += state[i].z; - block[i * 4 + 3] += state[i].w; - } - // - data[0] ^= block[0]; - data[1] ^= block[1]; -} - -void Chacha20XOR(inout uint data[3], const uint key[4]) -{ - uint4 state[4]; - state[0] = uint4(0x61707865, 0x3320646e, 0x79622d32, 0x6b206574); - state[1] = uint4(key[0], key[1], key[2], key[3]); - state[2] = state[1]; - state[3] = uint4(1, _Nonce0, _Nonce1, _Nonce2); - - uint block[16]; - //block - int i = 0; - [unroll] - for(i = 0; i < 4; ++i) - { - block[i * 4 + 0] = state[i].x; - block[i * 4 + 1] = state[i].y; - block[i * 4 + 2] = state[i].z; - block[i * 4 + 3] = state[i].w; - } - [unroll] - for (i = 0; i < CHACHA20_ROUNDS; i += 2) - { - Chacha20QuarterRound(block, 0, 4, 8, 12); - Chacha20QuarterRound(block, 1, 5, 9, 13); - Chacha20QuarterRound(block, 2, 6, 10, 14); - Chacha20QuarterRound(block, 3, 7, 11, 15); - Chacha20QuarterRound(block, 0, 5, 10, 15); - Chacha20QuarterRound(block, 1, 6, 11, 12); - Chacha20QuarterRound(block, 2, 7, 8, 13); - Chacha20QuarterRound(block, 3, 4, 9, 14); - } - [unroll] - for(i = 0; i < 4; ++i) - { - block[i * 4 + 0] += state[i].x; - block[i * 4 + 1] += state[i].y; - block[i * 4 + 2] += state[i].z; - block[i * 4 + 3] += state[i].w; - } - // - data[0] ^= block[0]; - data[1] ^= block[1]; - data[2] ^= block[2]; -} \ No newline at end of file diff --git a/Runtime/liltoonProtector/Shaders/Decrypt.cginc b/Runtime/liltoonProtector/Shaders/Decrypt.cginc deleted file mode 100644 index 7de41a0..0000000 --- a/Runtime/liltoonProtector/Shaders/Decrypt.cginc +++ /dev/null @@ -1,166 +0,0 @@ -#pragma once -#include "XXTEA.cginc" - -static const uint mw[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; -static const uint mh[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; - -float _Key0, _Key1, _Key2, _Key3, _Key4, _Key5, _Key6, _Key7, _Key8, _Key9, _Key10, _Key11, _Key12, _Key13, _Key14, _Key15; - -uint _Woffset; -uint _Hoffset; - -half3 InverseGammaCorrection(half3 rgb) -{ - half3 result = pow(rgb, 0.454545); - return result; -} -half3 GammaCorrection(half3 rgb) -{ - //half3 result = pow(rgb, 2.2); - half3 result = rgb * rgb * (rgb * (half)0.2 + (half)0.8); //fast pow - return result; -} - -half2 GetUV(int idx, int m, int woffset = 0, int hoffset = 0) -{ - int w = idx % mw[m + woffset]; - int h = idx / mw[m + woffset]; - return half2((half)w/mw[m + woffset], (half)h/mh[m + hoffset]); -} - -half4 DecryptTexture(half2 uv, int m) -{ - half x = frac(uv.x); - half y = frac(uv.y); - - int idx = (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 2) << 2) - }; - - half3 pixels[4]; - - const int pos[4] = { 0, -1, -2, -3 }; - int offset = pos[idx % 4]; - pixels[0] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[2] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 2 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[3] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 3 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[3] = { 0, 0, 0 }; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[1].r * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].g * 255.0f) | ((uint)round(pixels[1].b * 255.0f) << 8) | ((uint)round(pixels[2].r * 255.0f) << 16) | ((uint)round(pixels[2].g * 255.0f) << 24)); - data[2] = ((uint)round(pixels[2].b * 255.0f) | ((uint)round(pixels[3].r * 255.0f) << 8) | ((uint)round(pixels[3].g * 255.0f) << 16) | ((uint)round(pixels[3].b * 255.0f) << 24)); - - XXTEADecrypt(data, key); - - half r[4] = { (data[0] & 0x000000FF)/255.0f, ((data[0] & 0xFF000000) >> 24)/255.0f, ((data[1] & 0x00FF0000) >> 16)/255.0f, ((data[2] & 0x0000FF00) >> 8)/255.0f }; - half g[4] = { ((data[0] & 0x0000FF00) >> 8)/255.0f, ((data[1] & 0x000000FF) >> 0)/255.0f, ((data[1] & 0xFF000000) >> 24)/255.0f, ((data[2] & 0x00FF0000) >> 16)/255.0f }; - half b[4] = { ((data[0] & 0x00FF0000) >> 16)/255.0f, ((data[1] & 0x0000FF00) >> 8)/255.0f, ((data[2] & 0x000000FF) >> 0)/255.0f, ((data[2] & 0xFF000000) >> 24)/255.0f }; - half3 decrypt = half3(r[idx % 4], g[idx % 4], b[idx % 4]); - - return half4(GammaCorrection(decrypt), 1.0); -} -half4 DecryptTextureRGBA(half2 uv, int m) -{ - half x = frac(uv.x); - half y = frac(uv.y); - - int idx = (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 1) << 1) - }; - - half4 pixels[2]; - - int offset = (idx & 1) == 0 ? 0 : -1; - pixels[0] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[2] = { 0, 0 }; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); - - XXTEADecrypt(data, key); - - half r = ((data[idx & 1] & 0x000000FF) >> 0)/255.0f; - half g = ((data[idx & 1] & 0x0000FF00) >> 8)/255.0f; - half b = ((data[idx & 1] & 0x00FF0000) >> 16)/255.0f; - half a = ((data[idx & 1] & 0xFF000000) >> 24)/255.0f; - half4 decrypt = half4(r, g, b, a); - - return half4(GammaCorrection(decrypt.rgb), decrypt.a); -} -half4 DecryptTextureDXT(half2 uv, int m) -{ - half2 frac_uv = frac(uv.xy); - half x = frac_uv.x; - half y = frac_uv.y; - - half4 col = _EncryptTex0.SampleLevel(sampler_EncryptTex0, uv, m); - - uint w = mw[m + (uint)_Woffset]; - uint h = mh[m + _Hoffset]; - - int idx = (w * floor(y * h)) + floor(x * w); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 1) << 1) - }; - - int offset = (idx & 1) == 0 ? 0 : -1; - - half4 pixels[2]; - pixels[0] = _EncryptTex1.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex1.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[2]; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); - - XXTEADecrypt(data, key); - - uint r = (data[idx & 1] & 0x000000FF) >> 0; - uint g = (data[idx & 1] & 0x0000FF00) >> 8; - uint b = (data[idx & 1] & 0x00FF0000) >> 16; - uint a = (data[idx & 1] & 0xFF000000) >> 24; - - uint color1 = (r | g << 8); - uint color2 = (b | a << 8); - - uint color1_r = (color1 & 0xF800) >> 11; - color1_r = color1_r << 3 | color1_r >> 2; - uint color1_g = (color1 & 0x7E0) >> 5; - color1_g = color1_g << 2 | color1_g >> 4; - uint color1_b= color1 & 0x1F; - color1_b = color1_b << 3 | color1_b >> 2; - - uint color2_r = (color2 & 0xF800) >> 11; - color2_r = color2_r << 3 | color2_r >> 2; - uint color2_g = (color2 & 0x7E0) >> 5; - color2_g = color2_g << 2 | color2_g >> 4; - uint color2_b= color2 & 0x1F; - color2_b = color2_b << 3 | color2_b >> 2; - - half3 col1 = half3(color1_r / (half)255, color1_g / (half)255, color1_b / (half)255); - half3 col2 = half3(color2_r / (half)255, color2_g / (half)255, color2_b / (half)255); - - half3 result; - result = lerp(col2, col1, color1 > color2 ? col.rgb : 0.5); - - return half4(GammaCorrection(result), col.a); -} \ No newline at end of file diff --git a/Runtime/liltoonProtector/Shaders/DecryptChacha.cginc b/Runtime/liltoonProtector/Shaders/DecryptChacha.cginc deleted file mode 100644 index 3264d5c..0000000 --- a/Runtime/liltoonProtector/Shaders/DecryptChacha.cginc +++ /dev/null @@ -1,168 +0,0 @@ -#pragma once -#include "Chacha.cginc" - -static const uint mw[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; -static const uint mh[13] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }; - -float _Key0, _Key1, _Key2, _Key3, _Key4, _Key5, _Key6, _Key7, _Key8, _Key9, _Key10, _Key11, _Key12, _Key13, _Key14, _Key15; - -uint _Woffset; -uint _Hoffset; - -half3 InverseGammaCorrection(half3 rgb) -{ - half3 result = pow(rgb, 0.454545); - return result; -} -half3 GammaCorrection(half3 rgb) -{ - //half3 result = pow(rgb, 2.2); - half3 result = rgb * rgb * (rgb * (half)0.2 + (half)0.8); //fast pow - return result; -} - -half2 GetUV(int idx, int m, int woffset = 0, int hoffset = 0) -{ - int w = idx % mw[m + woffset]; - int h = idx / mw[m + woffset]; - return half2((half)w/mw[m + woffset], (half)h/mh[m + hoffset]); -} - -half4 DecryptTexture(half2 uv, int m) -{ - half x = frac(uv.x); - half y = frac(uv.y); - - int idx = (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 2) << 2) - }; - - half3 pixels[4]; - - const int pos[4] = { 0, -1, -2, -3 }; - int offset = pos[idx % 4]; - pixels[0] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[2] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 2 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[3] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 3 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[3] = { 0, 0, 0 }; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[1].r * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].g * 255.0f) | ((uint)round(pixels[1].b * 255.0f) << 8) | ((uint)round(pixels[2].r * 255.0f) << 16) | ((uint)round(pixels[2].g * 255.0f) << 24)); - data[2] = ((uint)round(pixels[2].b * 255.0f) | ((uint)round(pixels[3].r * 255.0f) << 8) | ((uint)round(pixels[3].g * 255.0f) << 16) | ((uint)round(pixels[3].b * 255.0f) << 24)); - - Chacha20XOR(data, key); - - half r[4] = { (data[0] & 0x000000FF)/255.0f, ((data[0] & 0xFF000000) >> 24)/255.0f, ((data[1] & 0x00FF0000) >> 16)/255.0f, ((data[2] & 0x0000FF00) >> 8)/255.0f }; - half g[4] = { ((data[0] & 0x0000FF00) >> 8)/255.0f, ((data[1] & 0x000000FF) >> 0)/255.0f, ((data[1] & 0xFF000000) >> 24)/255.0f, ((data[2] & 0x00FF0000) >> 16)/255.0f }; - half b[4] = { ((data[0] & 0x00FF0000) >> 16)/255.0f, ((data[1] & 0x0000FF00) >> 8)/255.0f, ((data[2] & 0x000000FF) >> 0)/255.0f, ((data[2] & 0xFF000000) >> 24)/255.0f }; - half3 decrypt = half3(r[idx % 4], g[idx % 4], b[idx % 4]); - - return half4(GammaCorrection(decrypt), 1.0); -} -half4 DecryptTextureRGBA(half2 uv, int m) -{ - half x = frac(uv.x); - half y = frac(uv.y); - - int idx = (mw[m + (uint)_Woffset] * floor(y * mh[m + _Hoffset])) + floor(x * mw[m + (uint)_Woffset]); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 1) << 1) - }; - - half4 pixels[2]; - - int offset = (idx & 1) == 0 ? 0 : -1; - pixels[0] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex0.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[2] = { 0, 0 }; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); - - Chacha20XOR(data, key); - - half r = ((data[idx & 1] & 0x000000FF) >> 0)/255.0f; - half g = ((data[idx & 1] & 0x0000FF00) >> 8)/255.0f; - half b = ((data[idx & 1] & 0x00FF0000) >> 16)/255.0f; - half a = ((data[idx & 1] & 0xFF000000) >> 24)/255.0f; - half4 decrypt = half4(r, g, b, a); - - return half4(GammaCorrection(decrypt.rgb), decrypt.a); -} -half4 DecryptTextureDXT(half2 uv, int m) -{ - half2 frac_uv = frac(uv.xy); - half x = frac_uv.x; - half y = frac_uv.y; - - int miplv = 0; - - half4 col = _EncryptTex0.SampleLevel(sampler_EncryptTex0, uv, m); - - uint w = mw[m + (uint)_Woffset]; - uint h = mh[m + _Hoffset]; - - int idx = (w * floor(y * h)) + floor(x * w); - - const uint key[4] = - { - ((uint)round(_Key0) | ((uint)round(_Key1) << 8) | ((uint)round(_Key2) << 16) | ((uint)round(_Key3) << 24)), - ((uint)round(_Key4) | ((uint)round(_Key5) << 8) | ((uint)round(_Key6) << 16) | ((uint)round(_Key7) << 24)), - ((uint)round(_Key8) | ((uint)round(_Key9) << 8) | ((uint)round(_Key10) << 16) | ((uint)round(_Key11) << 24)), - ((uint)round(_Key12) | ((uint)round(_Key13) << 8) | ((uint)round(_Key14) << 16) | ((uint)round(_Key15) << 24)) ^ (uint)((idx >> 1) << 1) - }; - - int offset = (idx & 1) == 0 ? 0 : -1; - - half4 pixels[2]; - pixels[0] = _EncryptTex1.SampleLevel(sampler_EncryptTex0, GetUV(idx + 0 + offset, m, (uint)_Woffset, _Hoffset), m); - pixels[1] = _EncryptTex1.SampleLevel(sampler_EncryptTex0, GetUV(idx + 1 + offset, m, (uint)_Woffset, _Hoffset), m); - - uint data[2]; - data[0] = ((uint)round(pixels[0].r * 255.0f) | ((uint)round(pixels[0].g * 255.0f) << 8) | ((uint)round(pixels[0].b * 255.0f) << 16) | ((uint)round(pixels[0].a * 255.0f) << 24)); - data[1] = ((uint)round(pixels[1].r * 255.0f) | ((uint)round(pixels[1].g * 255.0f) << 8) | ((uint)round(pixels[1].b * 255.0f) << 16) | ((uint)round(pixels[1].a * 255.0f) << 24)); - - Chacha20XOR(data, key); - - uint r = (data[idx & 1] & 0x000000FF) >> 0; - uint g = (data[idx & 1] & 0x0000FF00) >> 8; - uint b = (data[idx & 1] & 0x00FF0000) >> 16; - uint a = (data[idx & 1] & 0xFF000000) >> 24; - - uint color1 = (r | g << 8); - uint color2 = (b | a << 8); - - uint color1_r = (color1 & 0xF800) >> 11; - color1_r = color1_r << 3 | color1_r >> 2; - uint color1_g = (color1 & 0x7E0) >> 5; - color1_g = color1_g << 2 | color1_g >> 4; - uint color1_b= color1 & 0x1F; - color1_b = color1_b << 3 | color1_b >> 2; - - uint color2_r = (color2 & 0xF800) >> 11; - color2_r = color2_r << 3 | color2_r >> 2; - uint color2_g = (color2 & 0x7E0) >> 5; - color2_g = color2_g << 2 | color2_g >> 4; - uint color2_b= color2 & 0x1F; - color2_b = color2_b << 3 | color2_b >> 2; - - half3 col1 = half3(color1_r / (half)255, color1_g / (half)255, color1_b / (half)255); - half3 col2 = half3(color2_r / (half)255, color2_g / (half)255, color2_b / (half)255); - - half3 result; - result = lerp(col2, col1, color1 > color2 ? col.rgb : 0.5); - - return half4(GammaCorrection(result), col.a); -} \ No newline at end of file diff --git a/Runtime/liltoonProtector/Shaders/XXTEA.cginc b/Runtime/liltoonProtector/Shaders/XXTEA.cginc deleted file mode 100644 index 2395f12..0000000 --- a/Runtime/liltoonProtector/Shaders/XXTEA.cginc +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -static const uint Delta = 0x9e3779b9; -static const uint ROUNDS = 6; -static const uint SUM = Delta * ROUNDS; - -void XXTEADecrypt(inout uint data[3], const uint key[4]) -{ - static const uint n = 3; - uint v0, v1, sum; - uint p, e; - - sum = SUM; - - v0 = data[0]; - for(int i = 0; i < ROUNDS; ++i) - { - e = (sum >> 2) & 3; - for (p = n-1; p > 0; p--) - { - v1 = data[p - 1]; - data[p] -= (((v1 >> 5 ^ v0 << 2) + (v0 >> 3 ^ v1 << 4)) ^ ((sum ^ v0) + (key[(p & 3) ^ e] ^ v1))); - v0 = data[p]; - } - v1 = data[n - 1]; - data[0] -= (((v1 >> 5 ^ v0 << 2) + (v0 >> 3 ^ v1 << 4)) ^ ((sum ^ v0) + (key[(p & 3) ^ e] ^ v1))); - v0 = data[0]; - sum -= Delta; - } -} -void XXTEADecrypt(inout uint data[2], const uint key[4]) -{ - static const uint n = 2; - uint v0, v1, sum; - uint p, e; - - sum = SUM; - - v0 = data[0]; - for(int i = 0; i < ROUNDS; ++i) - { - e = (sum >> 2) & 3; - for (p = n-1; p > 0; p--) - { - v1 = data[p - 1]; - data[p] -= (((v1 >> 5 ^ v0 << 2) + (v0 >> 3 ^ v1 << 4)) ^ ((sum ^ v0) + (key[(p & 3) ^ e] ^ v1))); - v0 = data[p]; - } - v1 = data[n - 1]; - data[0] -= (((v1 >> 5 ^ v0 << 2) + (v0 >> 3 ^ v1 << 4)) ^ ((sum ^ v0) + (key[(p & 3) ^ e] ^ v1))); - v0 = data[0]; - sum -= Delta; - } -} \ No newline at end of file diff --git a/Runtime/liltoonProtector/Shaders/XXTEA.cginc.meta b/Runtime/liltoonProtector/Shaders/XXTEA.cginc.meta deleted file mode 100644 index fc960bc..0000000 --- a/Runtime/liltoonProtector/Shaders/XXTEA.cginc.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 0b885182c32e5244a819063762959709 -ShaderIncludeImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/liltoonProtector/Shaders/custom.hlsl b/Runtime/liltoonProtector/Shaders/custom.hlsl index 921fd50..b772f18 100644 --- a/Runtime/liltoonProtector/Shaders/custom.hlsl +++ b/Runtime/liltoonProtector/Shaders/custom.hlsl @@ -1,68 +1,36 @@ //---------------------------------------------------------------------------------------------------------------------- // Macro +#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \ + int isDecrypted : TEXCOORD##id0; + +#define LIL_CUSTOM_VERT_COPY \ + output.isDecrypted = IsDecrypted(); + #define LIL_CUSTOM_PROPERTIES\ half4 _EncryptTex0_TexelSize;\ - fixed _fallback; + int _PasswordHash; // Custom textures #define LIL_CUSTOM_TEXTURES\ - SAMPLER(_MipTex); \ - TEXTURE2D(_EncryptTex0); \ - SAMPLER(sampler_EncryptTex0); \ - TEXTURE2D(_EncryptTex1); - -#ifndef _FORMAT0 - #ifndef _FORMAT1 - #define DECRYPT DecryptTextureDXT - #else - #define DECRYPT DecryptTextureRGBA - #endif -#else - #ifndef _FORMAT1 - #define DECRYPT DecryptTexture - #endif -#endif + TEXTURE2D(_MipTex);\ + TEXTURE2D(_EncryptTex0);\ + TEXTURE2D(_EncryptTex1);\ + SAMPLER(sampler_MipTex);\ + SAMPLER(sampler_EncryptTex0); #ifdef _POINT #define CODE\ - half4 mip_texture = tex2D(_MipTex, fd.uvMain);\ - int mip = round(mip_texture.r * 255 / 10);\ - int m[13] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10 };\ - \ - half4 c00 = DECRYPT(fd.uvMain, m[mip]);\ - \ - fd.col = c00; + fd.col = DecryptTextureBox(_EncryptTex0, _EncryptTex1, sampler_EncryptTex0, _EncryptTex0_TexelSize, _MipTex, sampler_MipTex, fd.uvMain); #else #define CODE\ - half4 mip_texture = tex2D(_MipTex, fd.uvMain);\ - \ - half2 uv_unit = _EncryptTex0_TexelSize.xy;\ - half2 uv_bilinear = fd.uvMain - 0.5 * uv_unit;\ - int mip = round(mip_texture.r * 255 / 10);\ - static const int m[13] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10 };\ - \ - half4 c00 = DECRYPT(uv_bilinear + half2(uv_unit.x * 0, uv_unit.y * 0), m[mip]);\ - half4 c10 = DECRYPT(uv_bilinear + half2(uv_unit.x * 1, uv_unit.y * 0), m[mip]);\ - half4 c01 = DECRYPT(uv_bilinear + half2(uv_unit.x * 0, uv_unit.y * 1), m[mip]);\ - half4 c11 = DECRYPT(uv_bilinear + half2(uv_unit.x * 1, uv_unit.y * 1), m[mip]);\ - \ - half2 f = frac(uv_bilinear * _EncryptTex0_TexelSize.zw);\ - \ - half4 c0 = lerp(c00, c10, f.x);\ - half4 c1 = lerp(c01, c11, f.x);\ - \ - half4 bilinear = lerp(c0, c1, f.y);\ - \ - fd.col = bilinear;\ - LIL_APPLY_MAIN_TONECORRECTION\ - fd.col *= _Color; + fd.col = DecryptTextureBilinear(_EncryptTex0, _EncryptTex1, sampler_EncryptTex0, _EncryptTex0_TexelSize, _MipTex, sampler_MipTex, fd.uvMain); #endif #define OVERRIDE_MAIN\ LIL_GET_MAIN_TEX\ UNITY_BRANCH\ - if(_fallback == 1)\ + if(input.isDecrypted == 0)\ {\ LIL_APPLY_MAIN_TONECORRECTION\ fd.col *= _Color;\ @@ -70,12 +38,14 @@ else\ {\ CODE\ + LIL_APPLY_MAIN_TONECORRECTION\ + fd.col *= _Color;\ } #define OVERRIDE_MATCAP \ - lilGetMatCap(fd, _MipTex); + lilGetMatCap(fd, sampler_MipTex); -#if defined(_LIMLIGHT_ENCRYPTED) +#if defined(_SHELL_PROTECTOR_RIMLIGHT) #if defined(LIL_LITE) #define OVERRIDE_RIMLIGHT \ lilGetRim(fd); diff --git a/Runtime/liltoonProtector/Shaders/custom_insert.hlsl b/Runtime/liltoonProtector/Shaders/custom_insert.hlsl index dd93694..a9ab7b1 100644 --- a/Runtime/liltoonProtector/Shaders/custom_insert.hlsl +++ b/Runtime/liltoonProtector/Shaders/custom_insert.hlsl @@ -1,15 +1,2 @@ -#pragma shader_feature_local _XXTEA -#pragma shader_feature_local _FORMAT0 -#pragma shader_feature_local _FORMAT1 -#pragma shader_feature_local _POINT -#pragma shader_feature_local _LIMLIGHT_ENCRYPTED - -//format = format1 | format0 << 1 -//format 00:DXT, 01:RGB, 10:RGBA - -#ifdef _XXTEA - #include "Decrypt.cginc" -#else - #include "DecryptChacha.cginc" -#endif -#include "UnityCG.cginc" \ No newline at end of file +#include "../../Shader/Protector.cginc" +#include "UnityCG.cginc" diff --git a/Runtime/liltoonProtector/Shaders/lilCustomShaderProperties.lilblock b/Runtime/liltoonProtector/Shaders/lilCustomShaderProperties.lilblock index 3fdb2a1..82ab85e 100644 --- a/Runtime/liltoonProtector/Shaders/lilCustomShaderProperties.lilblock +++ b/Runtime/liltoonProtector/Shaders/lilCustomShaderProperties.lilblock @@ -17,9 +17,11 @@ _Key13 ("key13", float) = 0 _Key14 ("key14", float) = 0 _Key15 ("key15", float) = 0 - [MaterialToggle] _fallback("Fallback", float) = 0 _Woffset ("Woffset", integer) = 0 _Hoffset ("Hoffset", integer) = 0 _Nonce0 ("Nonce", integer) = 0 _Nonce1 ("Nonce", integer) = 0 - _Nonce2 ("Nonce", integer) = 0 \ No newline at end of file + _Nonce2 ("Nonce", integer) = 0 + _Rounds ("Rounds", integer) = 0 + _PasswordHash ("PasswordHash", integer) = 0 + _HashMagic ("HashMagic", integer) = 0 diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..bd006a6 --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: da102c537c144ecbb10c5c7edb29f733 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor.meta b/Tests/Editor.meta new file mode 100644 index 0000000..c3359d6 --- /dev/null +++ b/Tests/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 956601edd67e49dc84b71d3072016f1f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Gpu.meta b/Tests/Editor/Gpu.meta new file mode 100644 index 0000000..f24d1d0 --- /dev/null +++ b/Tests/Editor/Gpu.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f33a865bba384e6dab3f2c5d95587d3b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Gpu/GpuTextureDecryptionTests.cs b/Tests/Editor/Gpu/GpuTextureDecryptionTests.cs new file mode 100644 index 0000000..4344806 --- /dev/null +++ b/Tests/Editor/Gpu/GpuTextureDecryptionTests.cs @@ -0,0 +1,187 @@ +#if UNITY_EDITOR +using System; +using NUnit.Framework; +using UnityEngine; + +namespace Shell.Protector.Tests.Gpu +{ + public class GpuTextureDecryptionTests + { + private const int Size = 16; + private static readonly byte[] KeyBytes = KeyGenerator.MakeKeyBytes("password", "pass", 12); + private Material referenceMaterial; + private Material decryptMaterial; + private Texture2D mipTexture; + + [SetUp] + public void SetUp() + { + Shader shader = Shader.Find("Hidden/GpuDecryptTest"); + Assert.That(shader, Is.Not.Null, "GPU decrypt test shader was not imported."); + Assert.That(shader.isSupported, Is.True, "GPU decrypt test shader is not supported or failed to compile."); + + referenceMaterial = new Material(shader); + decryptMaterial = new Material(shader); + mipTexture = new Texture2D(1, 1, TextureFormat.RGB24, false, true); + mipTexture.SetPixel(0, 0, Color.black); + mipTexture.filterMode = FilterMode.Point; + mipTexture.Apply(false, false); + } + + [TearDown] + public void TearDown() + { + UnityEngine.Object.DestroyImmediate(referenceMaterial); + UnityEngine.Object.DestroyImmediate(decryptMaterial); + UnityEngine.Object.DestroyImmediate(mipTexture); + } + + [TestCase(TextureFormat.RGB24, false, false)] + [TestCase(TextureFormat.RGB24, false, true)] + [TestCase(TextureFormat.RGBA32, true, false)] + [TestCase(TextureFormat.RGBA32, true, true)] + [TestCase(TextureFormat.DXT1, false, false)] + [TestCase(TextureFormat.DXT1, false, true)] + [TestCase(TextureFormat.DXT5, true, false)] + [TestCase(TextureFormat.DXT5, true, true)] + public void XxteaEncryptedTexture_DecryptsToOriginalGpuSample(TextureFormat format, bool alpha, bool bilinear) + { + XXTEA xxtea = new XXTEA { Rounds = 20 }; + + AssertDecryptsToOriginalGpuSample(format, alpha, bilinear, xxtea, material => + { + material.SetInteger("_Rounds", 20); + }); + } + + [TestCase(TextureFormat.RGB24, false, false)] + [TestCase(TextureFormat.RGB24, false, true)] + [TestCase(TextureFormat.RGBA32, true, false)] + [TestCase(TextureFormat.RGBA32, true, true)] + [TestCase(TextureFormat.DXT1, false, false)] + [TestCase(TextureFormat.DXT1, false, true)] + [TestCase(TextureFormat.DXT5, true, false)] + [TestCase(TextureFormat.DXT5, true, true)] + public void ChachaEncryptedTexture_DecryptsToOriginalGpuSample(TextureFormat format, bool alpha, bool bilinear) + { + Chacha20 chacha = new Chacha20(); + for (int i = 0; i < chacha.Nonce.Length; i++) + chacha.Nonce[i] = (byte)i; + + AssertDecryptsToOriginalGpuSample(format, alpha, bilinear, chacha, material => + { + uint[] nonce = chacha.GetNonceUint3(); + material.SetInteger("_Nonce0", unchecked((int)nonce[0])); + material.SetInteger("_Nonce1", unchecked((int)nonce[1])); + material.SetInteger("_Nonce2", unchecked((int)nonce[2])); + }); + } + + private void AssertDecryptsToOriginalGpuSample(TextureFormat format, bool alpha, bool bilinear, IEncryptor encryptor, Action configureCipher) + { + Texture2D original = TestAssetScope.CreatePatternTexture(Size, Size, format, alpha); + original.filterMode = FilterMode.Point; + original.wrapMode = TextureWrapMode.Repeat; + + EncryptResult encrypted = TextureEncryptManager.EncryptTexture(original, KeyBytes, encryptor); + FinalizeTexture(encrypted.Texture1); + FinalizeTexture(encrypted.Texture2); + + ConfigureReferenceMaterial(original); + ConfigureDecryptMaterial(original, encrypted, encryptor, configureCipher); + + Color32[] reference = Render(referenceMaterial, original, 0, Size, Size); + Color32[] decrypted = Render(decryptMaterial, Texture2D.blackTexture, bilinear ? 2 : 1, Size, Size); + + AssertPixelsEqual(reference, decrypted, format, encryptor.Keyword, bilinear); + } + + private void ConfigureReferenceMaterial(Texture2D original) + { + referenceMaterial.SetTexture("_MainTex", original); + } + + private void ConfigureDecryptMaterial(Texture2D original, EncryptResult encrypted, IEncryptor encryptor, Action configureCipher) + { + decryptMaterial.shaderKeywords = Array.Empty(); + decryptMaterial.mainTexture = original; + decryptMaterial.SetTexture("_EncryptTex0", encrypted.Texture1); + decryptMaterial.SetTexture("_EncryptTex1", encrypted.Texture2 != null ? encrypted.Texture2 : Texture2D.blackTexture); + decryptMaterial.SetTexture("_MipTex", mipTexture); + + for (int i = 0; i < KeyBytes.Length; i++) + decryptMaterial.SetFloat("_Key" + i, KeyBytes[i]); + + var offsets = TextureEncryptManager.CalculateOffsets(original); + decryptMaterial.SetInteger("_Woffset", offsets.Item1); + decryptMaterial.SetInteger("_Hoffset", offsets.Item2); + decryptMaterial.SetInteger("_HashMagic", unchecked((int)0x12345678u)); + decryptMaterial.SetInteger("_PasswordHash", unchecked((int)KeyGenerator.SimpleHash(KeyBytes, 0x12345678u))); + + TextureEncryptManager.SetFormatKeywords(decryptMaterial); + decryptMaterial.EnableKeyword(encryptor.Keyword); + configureCipher(decryptMaterial); + } + + private static void FinalizeTexture(Texture2D texture) + { + if (texture == null) + return; + + texture.filterMode = FilterMode.Point; + texture.wrapMode = TextureWrapMode.Repeat; + texture.Apply(false, false); + } + + private static Color32[] Render(Material material, Texture source, int pass, int width, int height) + { + RenderTexture target = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear); + target.filterMode = FilterMode.Point; + target.Create(); + + RenderTexture previous = RenderTexture.active; + Texture2D readback = new Texture2D(width, height, TextureFormat.RGBA32, false, true); + try + { + Graphics.Blit(source, target, material, pass); + RenderTexture.active = target; + readback.ReadPixels(new Rect(0, 0, width, height), 0, 0); + readback.Apply(false, false); + return readback.GetPixels32(); + } + finally + { + RenderTexture.active = previous; + target.Release(); + UnityEngine.Object.DestroyImmediate(target); + UnityEngine.Object.DestroyImmediate(readback); + } + } + + private static void AssertPixelsEqual(Color32[] expected, Color32[] actual, TextureFormat format, string keyword, bool bilinear) + { + Assert.That(actual.Length, Is.EqualTo(expected.Length)); + for (int i = 0; i < expected.Length; i++) + { + if (!expected[i].Equals(actual[i])) + { + Assert.Fail( + "{0} {1} {2} pixel {3} differs. Expected RGBA({4},{5},{6},{7}), Actual RGBA({8},{9},{10},{11})", + format, + keyword, + bilinear ? "bilinear" : "box", + i, + expected[i].r, + expected[i].g, + expected[i].b, + expected[i].a, + actual[i].r, + actual[i].g, + actual[i].b, + actual[i].a); + } + } + } + } +} +#endif diff --git a/Tests/Editor/Gpu/GpuTextureDecryptionTests.cs.meta b/Tests/Editor/Gpu/GpuTextureDecryptionTests.cs.meta new file mode 100644 index 0000000..8d51d52 --- /dev/null +++ b/Tests/Editor/Gpu/GpuTextureDecryptionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e68ab58df2a4a15802eecb0ccd45afb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Gpu/Shaders.meta b/Tests/Editor/Gpu/Shaders.meta new file mode 100644 index 0000000..3f4444f --- /dev/null +++ b/Tests/Editor/Gpu/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 95fb310bf78a4821867f6d7b494e3b1c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Gpu/Shaders/Hidden.meta b/Tests/Editor/Gpu/Shaders/Hidden.meta new file mode 100644 index 0000000..1b00bf5 --- /dev/null +++ b/Tests/Editor/Gpu/Shaders/Hidden.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b41acc594bfe4585ad33ad4fc6c6b9c9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Gpu/Shaders/Hidden/GpuDecryptTest.shader b/Tests/Editor/Gpu/Shaders/Hidden/GpuDecryptTest.shader new file mode 100644 index 0000000..c702905 --- /dev/null +++ b/Tests/Editor/Gpu/Shaders/Hidden/GpuDecryptTest.shader @@ -0,0 +1,120 @@ +Shader "Hidden/GpuDecryptTest" +{ + Properties + { + _MainTex ("Main", 2D) = "white" {} + _MipTex ("Mip", 2D) = "black" {} + _EncryptTex0 ("Encrypted0", 2D) = "white" {} + _EncryptTex1 ("Encrypted1", 2D) = "white" {} + _Key0 ("key0", Float) = 0 + _Key1 ("key1", Float) = 0 + _Key2 ("key2", Float) = 0 + _Key3 ("key3", Float) = 0 + _Key4 ("key4", Float) = 0 + _Key5 ("key5", Float) = 0 + _Key6 ("key6", Float) = 0 + _Key7 ("key7", Float) = 0 + _Key8 ("key8", Float) = 0 + _Key9 ("key9", Float) = 0 + _Key10 ("key10", Float) = 0 + _Key11 ("key11", Float) = 0 + _Key12 ("key12", Float) = 0 + _Key13 ("key13", Float) = 0 + _Key14 ("key14", Float) = 0 + _Key15 ("key15", Float) = 0 + _Woffset ("Woffset", Integer) = 0 + _Hoffset ("Hoffset", Integer) = 0 + _HashMagic ("HashMagic", Integer) = 0 + _PasswordHash ("PasswordHash", Integer) = 0 + _Nonce0 ("Nonce0", Integer) = 0 + _Nonce1 ("Nonce1", Integer) = 0 + _Nonce2 ("Nonce2", Integer) = 0 + _Rounds ("Rounds", Integer) = 0 + } + + SubShader + { + Cull Off ZWrite Off ZTest Always + + Pass + { + Name "Reference" + CGPROGRAM + #pragma vertex vert_img + #pragma fragment frag + #include "UnityCG.cginc" + + sampler2D _MainTex; + + half3 ShellProtectorGammaCorrection(half3 rgb) + { + return rgb * rgb * (rgb * (half)0.2 + (half)0.8); + } + + half4 frag(v2f_img i) : SV_Target + { + half4 color = tex2D(_MainTex, i.uv); + return half4(ShellProtectorGammaCorrection(color.rgb), color.a); + } + ENDCG + } + + Pass + { + Name "DecryptBox" + CGPROGRAM + #pragma target 5.0 + #pragma vertex vert_img + #pragma fragment frag + #pragma shader_feature_local _SHELL_PROTECTOR_XXTEA + #pragma shader_feature_local _SHELL_PROTECTOR_CHACHA + #pragma shader_feature_local _SHELL_PROTECTOR_FORMAT0 + #pragma shader_feature_local _SHELL_PROTECTOR_FORMAT1 + #include "UnityCG.cginc" + + Texture2D _EncryptTex0; + Texture2D _EncryptTex1; + Texture2D _MipTex; + SamplerState point_repeat_sampler; + half4 _EncryptTex0_TexelSize; + int _PasswordHash; + + #include "../../../../../Runtime/Shader/Protector.cginc" + + half4 frag(v2f_img i) : SV_Target + { + return DecryptTextureBox(_EncryptTex0, _EncryptTex1, point_repeat_sampler, _EncryptTex0_TexelSize, _MipTex, point_repeat_sampler, i.uv); + } + ENDCG + } + + Pass + { + Name "DecryptBilinear" + CGPROGRAM + #pragma target 5.0 + #pragma vertex vert_img + #pragma fragment frag + #pragma shader_feature_local _SHELL_PROTECTOR_XXTEA + #pragma shader_feature_local _SHELL_PROTECTOR_CHACHA + #pragma shader_feature_local _SHELL_PROTECTOR_FORMAT0 + #pragma shader_feature_local _SHELL_PROTECTOR_FORMAT1 + #include "UnityCG.cginc" + + Texture2D _EncryptTex0; + Texture2D _EncryptTex1; + Texture2D _MipTex; + SamplerState point_repeat_sampler; + half4 _EncryptTex0_TexelSize; + int _PasswordHash; + + #include "../../../../../Runtime/Shader/Protector.cginc" + + half4 frag(v2f_img i) : SV_Target + { + return DecryptTextureBilinear(_EncryptTex0, _EncryptTex1, point_repeat_sampler, _EncryptTex0_TexelSize, _MipTex, point_repeat_sampler, i.uv); + } + ENDCG + } + } +} diff --git a/Runtime/Decrypt.cginc.meta b/Tests/Editor/Gpu/Shaders/Hidden/GpuDecryptTest.shader.meta similarity index 53% rename from Runtime/Decrypt.cginc.meta rename to Tests/Editor/Gpu/Shaders/Hidden/GpuDecryptTest.shader.meta index 8a22f0d..a22a4b8 100644 --- a/Runtime/Decrypt.cginc.meta +++ b/Tests/Editor/Gpu/Shaders/Hidden/GpuDecryptTest.shader.meta @@ -1,9 +1,9 @@ fileFormatVersion: 2 -guid: 6ccf8c747e8388f42b93c42f1f2bd7ab +guid: 0b3b4eb99cd246df801a92489f283798 ShaderImporter: externalObjects: {} defaultTextures: [] nonModifiableTextures: [] - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Integration.meta b/Tests/Editor/Integration.meta new file mode 100644 index 0000000..86b4b3f --- /dev/null +++ b/Tests/Editor/Integration.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 40d63da9abf2483bade6abd9464d8691 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Integration/PipelineTests.cs b/Tests/Editor/Integration/PipelineTests.cs new file mode 100644 index 0000000..17e2562 --- /dev/null +++ b/Tests/Editor/Integration/PipelineTests.cs @@ -0,0 +1,406 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using Shell.Protector; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDKBase; + +namespace Shell.Protector.Tests.Integration +{ + public class PipelineTests + { + private readonly List sceneObjects = new List(); + + [SetUp] + public void SetUp() + { + TestAssetScope.Reset(); + } + + [TearDown] + public void TearDown() + { + TestAssetScope.DestroyObjects(sceneObjects); + TestAssetScope.DeleteGeneratedRoot(); + TestAssetScope.DeleteDefaultGeneratedRoot(); + } + + [Test] + public void ManualEncrypt_CreatesEncryptedAvatarAndRewritesProtectionAssets() + { + Fixture fixture = CreateFixture("Manual"); + + GameObject encryptedAvatar = fixture.Protector.Encrypt(false); + sceneObjects.Add(encryptedAvatar); + + Assert.That(encryptedAvatar, Is.Not.Null); + Assert.That(encryptedAvatar, Is.Not.SameAs(fixture.Avatar)); + Assert.That(encryptedAvatar.name, Does.Contain("_encrypted")); + Assert.That(fixture.Avatar.activeSelf, Is.False); + Assert.That(encryptedAvatar.GetComponentInChildren(true), Is.Null); + Assert.That(encryptedAvatar.GetComponentInChildren(true), Is.Not.Null); + + AssertEncryptedRenderer(encryptedAvatar, fixture.Material); + AssertGeneratedPaths(encryptedAvatar, fixture.Material, TestAssetScope.GeneratedRoot); + AssertFxController(encryptedAvatar); + AssertExpressionParameters(encryptedAvatar); + AssertBlendShapeWasObfuscated(encryptedAvatar); + AssertAnimationMaterialWasRewritten(encryptedAvatar, fixture.Material); + } + + [Test] + public void ManualEncrypt_DuplicatesNonFxControllerBeforeObfuscatingBlendShapes() + { + Fixture fixture = CreateFixture("ControllerIsolation"); + VRCAvatarDescriptor originalDescriptor = fixture.Avatar.GetComponent(); + AnimationClip originalClip = CreateBlendShapeClip("ControllerIsolation"); + AnimatorController originalController = CreateBlendShapeController("ControllerIsolation", originalClip); + originalDescriptor.baseAnimationLayers[0] = new VRCAvatarDescriptor.CustomAnimLayer + { + type = VRCAvatarDescriptor.AnimLayerType.Base, + isDefault = false, + animatorController = originalController + }; + + GameObject encryptedAvatar = fixture.Protector.Encrypt(false); + sceneObjects.Add(encryptedAvatar); + + AnimatorController sourceController = originalDescriptor.baseAnimationLayers[0].animatorController as AnimatorController; + AnimatorController encryptedController = encryptedAvatar.GetComponent() + .baseAnimationLayers[0].animatorController as AnimatorController; + AnimationClip sourceClip = GetFirstClip(sourceController); + AnimationClip encryptedClip = GetFirstClip(encryptedController); + + Assert.That(sourceController, Is.SameAs(originalController)); + Assert.That(encryptedController, Is.Not.SameAs(originalController)); + Assert.That(sourceClip, Is.SameAs(originalClip)); + Assert.That(encryptedClip, Is.Not.SameAs(originalClip)); + Assert.That(AnimationUtility.GetCurveBindings(sourceClip).Single().propertyName, Is.EqualTo("blendShape.Smile")); + Assert.That(AnimationUtility.GetCurveBindings(encryptedClip).Single().propertyName, Does.StartWith("blendShape.")); + Assert.That(AnimationUtility.GetCurveBindings(encryptedClip).Single().propertyName, Is.Not.EqualTo("blendShape.Smile")); + } + + [Test] + public void InPlaceEncrypt_RewritesOriginalAvatarWhenNdmfStyleStepsRun() + { + Fixture fixture = CreateFixture("InPlace"); + + GameObject avatar = fixture.Protector.Encrypt(true); + fixture.Protector.ReplaceMaterials(avatar); + fixture.Protector.RemoveDuplicatedTextures(avatar); + fixture.Protector.SetAnimations(avatar, false); + fixture.Protector.ObfuscateBlendShape(avatar, false); + fixture.Protector.ChangeMaterialsInAnims(avatar, false); + fixture.Protector.CleanComponent(avatar); + + Assert.That(avatar, Is.SameAs(fixture.Avatar)); + Assert.That(avatar.activeSelf, Is.True); + Assert.That(avatar.GetComponentInChildren(true), Is.Null); + Assert.That(avatar.GetComponentInChildren(true), Is.Null); + + AssertEncryptedRenderer(avatar, fixture.Material); + AssertGeneratedPaths(avatar, fixture.Material, TestAssetScope.GeneratedRoot); + AssertFxController(avatar); + AssertExpressionParameters(avatar); + AssertBlendShapeWasObfuscated(avatar); + AssertAnimationMaterialWasRewritten(avatar, fixture.Material); + } + + [Test] + public void DefaultAssetDir_UsesGeneratedRootAndFolderGuids() + { + Fixture fixture = CreateFixture("Default", null); + + GameObject encryptedAvatar = fixture.Protector.Encrypt(false); + sceneObjects.Add(encryptedAvatar); + + string avatarName = encryptedAvatar.name.Replace("_encrypted", ""); + AssertGeneratedPaths(encryptedAvatar, fixture.Material, TestAssetScope.DefaultGeneratedRoot); + AssertOutputFoldersHaveGuids(TestAssetScope.DefaultGeneratedRoot, avatarName); + } + + [Test] + public void AddKeyLayer_DoesNotDuplicateShellProtectorParametersOrLayers() + { + string controllerDir = TestAssetScope.GeneratedRoot + "/Repeat"; + TestAssetScope.EnsureFolder(controllerDir); + AnimatorController controller = AnimatorController.CreateAnimatorControllerAtPath(controllerDir + "/fx.controller"); + string animationDir = "Assets/ShellProtector/Runtime/Animations"; + + AnimatorManager.AddKeyLayer(controller, animationDir, 4, 1, 3.0f); + AnimatorManager.AddKeyLayer(controller, animationDir, 4, 1, 3.0f); + + Assert.That(controller.layers.Count(l => l.name == "ShellProtector"), Is.EqualTo(1)); + Assert.That(controller.parameters.Count(p => p.name == "key_weight"), Is.EqualTo(1)); + Assert.That(controller.parameters.Count(p => p.name == ParameterManager.GetKeyName(0)), Is.EqualTo(1)); + Assert.That(controller.parameters.Count(p => p.name == ParameterManager.GetSyncLockName(true)), Is.EqualTo(1)); + Assert.That(controller.parameters.Count(p => p.name == ParameterManager.GetSyncSwitchName(0, true)), Is.EqualTo(1)); + } + + private Fixture CreateFixture(string name, string assetDir = TestAssetScope.GeneratedRoot) + { + Texture2D texture = TestAssetScope.CreatePatternTexture(128, 128, TextureFormat.RGBA32, true); + texture.name = name + "Texture"; + string texturePath = TestAssetScope.CreateAsset(texture, name + "/texture.asset"); + texture = AssetDatabase.LoadAssetAtPath(texturePath); + + Shader lilToon = AssetDatabase.LoadAssetAtPath("Packages/jp.lilxyzw.liltoon/Shader/lts.shader"); + Assert.That(lilToon, Is.Not.Null, "lilToon lts.shader is required for the integration fixture."); + + Material material = new Material(lilToon); + material.name = name + "Material"; + material.mainTexture = texture; + string materialPath = TestAssetScope.CreateAsset(material, name + "/material.mat"); + material = AssetDatabase.LoadAssetAtPath(materialPath); + + Mesh mesh = TestAssetScope.CreateBlendShapeQuadMesh(); + string meshPath = TestAssetScope.CreateAsset(mesh, name + "/mesh.asset"); + mesh = AssetDatabase.LoadAssetAtPath(meshPath); + + AnimationClip materialClip = CreateMaterialClip(name, material); + AnimatorController fx = CreateFxController(name, materialClip); + ScriptableObject parameters = CreateExpressionParameters(name); + + GameObject avatar = new GameObject(name + "Avatar"); + sceneObjects.Add(avatar); + + GameObject body = new GameObject("Body"); + body.transform.SetParent(avatar.transform, false); + SkinnedMeshRenderer renderer = body.AddComponent(); + renderer.sharedMesh = mesh; + renderer.sharedMaterial = material; + + VRCAvatarDescriptor descriptor = avatar.AddComponent(); + VrcExpressionParametersTestUtil.SetDescriptorParameters(descriptor, parameters); + descriptor.VisemeBlendShapes = Enumerable.Repeat(string.Empty, (int)VRC_AvatarDescriptor.Viseme.Count).ToArray(); + var eyeSettings = descriptor.customEyeLookSettings; + eyeSettings.eyelidsBlendshapes = new int[0]; + descriptor.customEyeLookSettings = eyeSettings; + descriptor.baseAnimationLayers = new VRCAvatarDescriptor.CustomAnimLayer[5]; + descriptor.baseAnimationLayers[4] = new VRCAvatarDescriptor.CustomAnimLayer + { + type = VRCAvatarDescriptor.AnimLayerType.FX, + isDefault = false, + animatorController = fx + }; + + ShellProtector protector = avatar.AddComponent(); + protector.Descriptor = descriptor; + if (assetDir != null) + protector.AssetDir = assetDir; + SetSerializedField(protector, "_gameObjectList", new List { avatar }); + SetSerializedField(protector, "_algorithm", 1); + SetSerializedField(protector, "_filter", 0); + SetSerializedField(protector, "_fallback", 5); + SetSerializedField(protector, "_keySize", 12); + SetSerializedField(protector, "_syncSize", 1); + protector.Init(); + + return new Fixture + { + Avatar = avatar, + Protector = protector, + Material = material + }; + } + + private static AnimationClip CreateMaterialClip(string name, Material material) + { + AnimationClip clip = new AnimationClip(); + clip.name = name + "MaterialSwap"; + EditorCurveBinding binding = EditorCurveBinding.PPtrCurve("Body", typeof(SkinnedMeshRenderer), "m_Materials.Array.data[0]"); + AnimationUtility.SetObjectReferenceCurve(clip, binding, new[] + { + new ObjectReferenceKeyframe { time = 0f, value = material } + }); + string path = TestAssetScope.CreateAsset(clip, name + "/materialSwap.anim"); + return AssetDatabase.LoadAssetAtPath(path); + } + + private static AnimationClip CreateBlendShapeClip(string name) + { + AnimationClip clip = new AnimationClip(); + clip.name = name + "BlendShape"; + EditorCurveBinding binding = new EditorCurveBinding + { + path = "Body", + type = typeof(SkinnedMeshRenderer), + propertyName = "blendShape.Smile" + }; + AnimationUtility.SetEditorCurve(clip, binding, AnimationCurve.Constant(0f, 1f, 100f)); + string path = TestAssetScope.CreateAsset(clip, name + "/blendShape.anim"); + return AssetDatabase.LoadAssetAtPath(path); + } + + private static AnimatorController CreateBlendShapeController(string name, AnimationClip clip) + { + string path = TestAssetScope.GeneratedRoot + "/" + name + "/base.controller"; + TestAssetScope.EnsureFolder(TestAssetScope.GeneratedRoot + "/" + name); + AnimatorController controller = AnimatorController.CreateAnimatorControllerAtPath(path); + AnimatorControllerLayer layer = controller.layers[0]; + AnimatorState state = layer.stateMachine.AddState("BlendShape"); + state.motion = clip; + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + return AssetDatabase.LoadAssetAtPath(path); + } + + private static AnimationClip GetFirstClip(AnimatorController controller) + { + return controller.layers[0].stateMachine.states[0].state.motion as AnimationClip; + } + + private static AnimatorController CreateFxController(string name, AnimationClip materialClip) + { + string path = TestAssetScope.GeneratedRoot + "/" + name + "/fx.controller"; + TestAssetScope.EnsureFolder(TestAssetScope.GeneratedRoot + "/" + name); + AnimatorController controller = AnimatorController.CreateAnimatorControllerAtPath(path); + AnimatorControllerLayer layer = controller.layers[0]; + AnimatorState state = layer.stateMachine.AddState("MaterialSwap"); + state.motion = materialClip; + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + return AssetDatabase.LoadAssetAtPath(path); + } + + private static ScriptableObject CreateExpressionParameters(string name) + { + ScriptableObject parameters = VrcExpressionParametersTestUtil.Create( + name + "Params", + new VrcExpressionParametersTestUtil.ParameterSpec + { + Name = "existing", + Saved = false, + NetworkSynced = false, + ValueType = "Bool", + DefaultValue = 0f + }); + string path = TestAssetScope.CreateAsset(parameters, name + "/params.asset"); + return AssetDatabase.LoadAssetAtPath(path); + } + + private static void AssertEncryptedRenderer(GameObject avatar, Material originalMaterial) + { + SkinnedMeshRenderer renderer = avatar.transform.Find("Body").GetComponent(); + Material encryptedMaterial = renderer.sharedMaterial; + + Assert.That(encryptedMaterial, Is.Not.Null); + Assert.That(encryptedMaterial, Is.Not.SameAs(originalMaterial)); + Assert.That(encryptedMaterial.name, Does.Contain("_encrypted")); + Assert.That(encryptedMaterial.mainTexture, Is.Not.SameAs(originalMaterial.mainTexture)); + Assert.That(encryptedMaterial.GetTexture("_EncryptTex0"), Is.Not.Null); + Assert.That(encryptedMaterial.GetTexture("_MipTex"), Is.Not.Null); + Assert.That(encryptedMaterial.GetTag("VRCFallback", false), Is.EqualTo("Unlit")); + Assert.That(encryptedMaterial.IsKeywordEnabled("_SHELL_PROTECTOR_CHACHA"), Is.True); + Assert.That(AssetDatabase.GetAssetPath(encryptedMaterial), Does.StartWith(TestAssetScope.GeneratedRoot)); + } + + private static void AssertGeneratedPaths(GameObject avatar, Material originalMaterial, string expectedRoot) + { + string avatarRoot = expectedRoot + "/" + avatar.name.Replace("_encrypted", ""); + Material encryptedMaterial = avatar.transform.Find("Body").GetComponent().sharedMaterial; + Texture2D encryptedTexture = encryptedMaterial.GetTexture("_EncryptTex0") as Texture2D; + Texture2D mipTexture = encryptedMaterial.GetTexture("_MipTex") as Texture2D; + string materialPath = AssetDatabase.GetAssetPath(encryptedMaterial).Replace('\\', '/'); + string texturePath = AssetDatabase.GetAssetPath(encryptedTexture).Replace('\\', '/'); + string mipPath = AssetDatabase.GetAssetPath(mipTexture).Replace('\\', '/'); + + Assert.That(materialPath, Does.StartWith(avatarRoot + "/Mat/")); + Assert.That(texturePath, Does.StartWith(avatarRoot + "/Tex/")); + Assert.That(mipPath, Does.StartWith(avatarRoot + "/Tex/")); + Assert.That(materialPath, Does.Contain(originalMaterial.name)); + Assert.That(materialPath, Does.Not.Contain(originalMaterial.GetInstanceID().ToString())); + Assert.That(materialPath, Does.Not.StartWith("Assets/ShellProtector/Runtime/")); + } + + private static void AssertOutputFoldersHaveGuids(string root, string avatarName) + { + string avatarRoot = root + "/" + avatarName; + foreach (string folderName in new[] { "Tex", "Mat", "Shader", "Anim", "Mesh" }) + { + string folderPath = avatarRoot + "/" + folderName; + string guid = AssetDatabase.AssetPathToGUID(folderPath); + Assert.That(guid, Is.Not.Empty, folderPath); + Assert.That(AssetDatabase.GUIDToAssetPath(guid).Replace('\\', '/'), Is.EqualTo(folderPath)); + } + } + + private static void AssertFxController(GameObject avatar) + { + AnimatorController fx = ShellProtector.GetFx(avatar); + + Assert.That(fx, Is.Not.Null); + Assert.That(fx.layers.Select(l => l.name), Does.Contain("ShellProtector")); + Assert.That(fx.layers.Select(l => l.name), Does.Contain("ShellProtectorDemux")); + Assert.That(fx.parameters.Select(p => p.name), Does.Contain("SHELL_PROTECTOR_key0")); + Assert.That(fx.parameters.Select(p => p.name), Does.Contain("encrypt_lock")); + } + + private static void AssertExpressionParameters(GameObject avatar) + { + VRCAvatarDescriptor descriptor = avatar.GetComponent(); + ScriptableObject parameters = VrcExpressionParametersTestUtil.GetDescriptorParameters(descriptor); + VrcExpressionParametersTestUtil.ParameterSnapshot[] snapshots = VrcExpressionParametersTestUtil.Read(parameters).ToArray(); + + Assert.That(parameters, Is.Not.Null); + Assert.That(snapshots.Select(p => p.Name), Does.Contain("SHELL_PROTECTOR_key11")); + Assert.That(snapshots.Select(p => p.Name), Does.Contain("encrypt_lock")); + Assert.That(AssetDatabase.GetAssetPath(parameters), Does.StartWith(TestAssetScope.GeneratedRoot)); + } + + private static void AssertBlendShapeWasObfuscated(GameObject avatar) + { + SkinnedMeshRenderer renderer = avatar.transform.Find("Body").GetComponent(); + + Assert.That(renderer.sharedMesh.blendShapeCount, Is.EqualTo(1)); + Assert.That(renderer.sharedMesh.GetBlendShapeName(0), Is.Not.EqualTo("Smile")); + Assert.That(AssetDatabase.GetAssetPath(renderer.sharedMesh), Does.StartWith(TestAssetScope.GeneratedRoot)); + } + + private static void AssertAnimationMaterialWasRewritten(GameObject avatar, Material originalMaterial) + { + AnimatorController fx = ShellProtector.GetFx(avatar); + Material encryptedMaterial = avatar.transform.Find("Body").GetComponent().sharedMaterial; + List clips = new List(); + + foreach (AnimatorControllerLayer layer in fx.layers) + CollectClips(layer.stateMachine, clips); + + Assert.That(clips.Count, Is.GreaterThan(0)); + Assert.That(clips.Any(clip => AnimatorManager.IsMaterialInClip(clip, originalMaterial)), Is.False); + Assert.That(clips.Any(clip => AnimatorManager.IsMaterialInClip(clip, encryptedMaterial)), Is.True); + } + + private static void CollectClips(AnimatorStateMachine stateMachine, List clips) + { + foreach (ChildAnimatorState state in stateMachine.states) + { + if (state.state.motion is AnimationClip clip) + clips.Add(clip); + } + + foreach (ChildAnimatorStateMachine child in stateMachine.stateMachines) + CollectClips(child.stateMachine, clips); + } + + private static void SetSerializedField(ShellProtector protector, string fieldName, T value) + { + FieldInfo field = typeof(ShellProtector).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(field, Is.Not.Null, fieldName); + field.SetValue(protector, value); + } + + private class Fixture + { + public GameObject Avatar; + public ShellProtector Protector; + public Material Material; + } + } +} +#endif diff --git a/Tests/Editor/Integration/PipelineTests.cs.meta b/Tests/Editor/Integration/PipelineTests.cs.meta new file mode 100644 index 0000000..31ebd24 --- /dev/null +++ b/Tests/Editor/Integration/PipelineTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 717ea5bafb884615a36c02079d25342f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Integration/SupportedShaderRenderingTests.cs b/Tests/Editor/Integration/SupportedShaderRenderingTests.cs new file mode 100644 index 0000000..7f0944e --- /dev/null +++ b/Tests/Editor/Integration/SupportedShaderRenderingTests.cs @@ -0,0 +1,490 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using Shell.Protector; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; +using UnityEngine.Rendering; +using VRC.SDK3.Avatars.Components; +using VRC.SDKBase; +using Object = UnityEngine.Object; + +namespace Shell.Protector.Tests.Integration +{ + public class SupportedShaderRenderingTests + { + private const int TextureSize = 128; + private const int RenderSize = TextureSize; + private const int RenderLayer = 30; + private const byte ChannelTolerance = 16; + private const double AverageChannelTolerance = 5.0; + private const double CorruptedAverageChannelDifference = 20.0; + private readonly List sceneObjects = new List(); + + [SetUp] + public void SetUp() + { + TestAssetScope.Reset(); + } + + [TearDown] + public void TearDown() + { + TestAssetScope.DestroyObjects(sceneObjects); + TestAssetScope.DeleteGeneratedRoot(); + TestAssetScope.DeleteDefaultGeneratedRoot(); + } + + [Test] + public void LilToonEncryptedMaterial_RendersLikeOriginal() + { + Shader shader = FindSupportedShader("lilToon"); + Fixture fixture = CreateFixture("LilToonSmoke", shader); + Color32[] reference = RenderMaterial(fixture.Material); + + GameObject encryptedAvatar = fixture.Protector.Encrypt(false); + sceneObjects.Add(encryptedAvatar); + + Material encryptedMaterial = GetBodyMaterial(encryptedAvatar); + AssertEncryptedMaterial(encryptedMaterial); + Assert.That(AssetDatabase.GetAssetPath(encryptedMaterial.shader).Replace('\\', '/'), Does.Contain("liltoonProtector/Shaders")); + + Color32[] actual = RenderMaterial(encryptedMaterial); + AssertRenderedRgbClose(reference, actual, "lilToon"); + } + + [Test] + public void LilToonEncryptedMaterial_WithTamperedKey_RendersCorruptedOutput() + { + Shader shader = FindSupportedShader("lilToon"); + Fixture fixture = CreateFixture("LilToonTamperedKey", shader); + Color32[] reference = RenderMaterial(fixture.Material); + + GameObject encryptedAvatar = fixture.Protector.Encrypt(false); + sceneObjects.Add(encryptedAvatar); + + Material encryptedMaterial = GetBodyMaterial(encryptedAvatar); + AssertEncryptedMaterial(encryptedMaterial); + Assert.That(AssetDatabase.GetAssetPath(encryptedMaterial.shader).Replace('\\', '/'), Does.Contain("liltoonProtector/Shaders")); + + AssertTamperedKeyRendersCorruptedOutput(encryptedMaterial, reference, "lilToon"); + } + + [Test] + public void PoiyomiEncryptedMaterial_RendersLikeOriginal() + { + Shader shader = FindSupportedShader(".poiyomi/Poiyomi Toon"); + Fixture fixture = CreateFixture("PoiyomiSmoke", shader); + Color32[] reference = RenderMaterial(fixture.Material); + + GameObject encryptedAvatar = fixture.Protector.Encrypt(false); + sceneObjects.Add(encryptedAvatar); + + Material encryptedMaterial = GetBodyMaterial(encryptedAvatar); + AssertEncryptedMaterial(encryptedMaterial); + AssertPoiyomiShaderWasInjected(encryptedMaterial.shader); + + Color32[] actual = RenderMaterial(encryptedMaterial); + AssertRenderedRgbClose(reference, actual, "Poiyomi"); + } + + [Test] + public void PoiyomiEncryptedMaterial_WithTamperedKey_RendersCorruptedOutput() + { + Shader shader = FindSupportedShader(".poiyomi/Poiyomi Toon"); + Fixture fixture = CreateFixture("PoiyomiTamperedKey", shader); + Color32[] reference = RenderMaterial(fixture.Material); + + GameObject encryptedAvatar = fixture.Protector.Encrypt(false); + sceneObjects.Add(encryptedAvatar); + + Material encryptedMaterial = GetBodyMaterial(encryptedAvatar); + AssertEncryptedMaterial(encryptedMaterial); + AssertPoiyomiShaderWasInjected(encryptedMaterial.shader); + + AssertTamperedKeyRendersCorruptedOutput(encryptedMaterial, reference, "Poiyomi"); + } + + private Fixture CreateFixture(string name, Shader shader) + { + Texture2D texture = CreateSrgbPatternTexture(TextureSize, TextureSize, true); + texture.name = name + "Texture"; + string texturePath = TestAssetScope.CreateAsset(texture, name + "/texture.asset"); + texture = AssetDatabase.LoadAssetAtPath(texturePath); + + Material material = new Material(shader); + material.name = name + "Material"; + material.mainTexture = texture; + if (material.HasProperty("_Color")) + material.SetColor("_Color", Color.white); + + string materialPath = TestAssetScope.CreateAsset(material, name + "/material.mat"); + material = AssetDatabase.LoadAssetAtPath(materialPath); + + Mesh mesh = TestAssetScope.CreateBlendShapeQuadMesh(); + string meshPath = TestAssetScope.CreateAsset(mesh, name + "/mesh.asset"); + mesh = AssetDatabase.LoadAssetAtPath(meshPath); + + AnimatorController fx = CreateFxController(name); + ScriptableObject parameters = CreateExpressionParameters(name); + + GameObject avatar = new GameObject(name + "Avatar"); + sceneObjects.Add(avatar); + + GameObject body = new GameObject("Body"); + body.transform.SetParent(avatar.transform, false); + SkinnedMeshRenderer renderer = body.AddComponent(); + renderer.sharedMesh = mesh; + renderer.sharedMaterial = material; + + VRCAvatarDescriptor descriptor = avatar.AddComponent(); + VrcExpressionParametersTestUtil.SetDescriptorParameters(descriptor, parameters); + descriptor.VisemeBlendShapes = Enumerable.Repeat(string.Empty, (int)VRC_AvatarDescriptor.Viseme.Count).ToArray(); + var eyeSettings = descriptor.customEyeLookSettings; + eyeSettings.eyelidsBlendshapes = new int[0]; + descriptor.customEyeLookSettings = eyeSettings; + descriptor.baseAnimationLayers = new VRCAvatarDescriptor.CustomAnimLayer[5]; + descriptor.baseAnimationLayers[4] = new VRCAvatarDescriptor.CustomAnimLayer + { + type = VRCAvatarDescriptor.AnimLayerType.FX, + isDefault = false, + animatorController = fx + }; + + ShellProtector protector = avatar.AddComponent(); + protector.Descriptor = descriptor; + protector.AssetDir = TestAssetScope.GeneratedRoot; + SetSerializedField(protector, "_gameObjectList", new List { avatar }); + SetSerializedField(protector, "_algorithm", (int)ShellProtectorAlgorithm.Chacha); + SetSerializedField(protector, "_filter", (int)ShellProtectorTextureFilter.Bilinear); + SetSerializedField(protector, "_fallback", (int)ShellProtectorFallback.Size32); + SetSerializedField(protector, "_keySize", 12); + SetSerializedField(protector, "_syncSize", 1); + protector.Init(); + + return new Fixture + { + Avatar = avatar, + Protector = protector, + Material = material + }; + } + + private static Shader FindSupportedShader(string shaderName) + { + Shader shader = Shader.Find(shaderName); + if (shader == null) + Assert.Ignore(shaderName + " shader was not found."); + if (!shader.isSupported) + Assert.Ignore(shaderName + " shader is not supported or failed to compile."); + return shader; + } + + private static Texture2D CreateSrgbPatternTexture(int width, int height, bool alpha) + { + Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, true, false); + Color32[] pixels = new Color32[width * height]; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + byte r = (byte)((x * 17 + y * 3) & 0xFF); + byte g = (byte)((x * 5 + y * 29) & 0xFF); + byte b = (byte)((x * 11 + y * 7 + 31) & 0xFF); + byte a = alpha ? (byte)(64 + ((x * 13 + y * 19) & 0x7F)) : (byte)255; + pixels[y * width + x] = new Color32(r, g, b, a); + } + } + + texture.SetPixels32(pixels); + texture.Apply(true, false); + texture.filterMode = FilterMode.Bilinear; + texture.wrapMode = TextureWrapMode.Repeat; + return texture; + } + + private static AnimatorController CreateFxController(string name) + { + string dir = TestAssetScope.GeneratedRoot + "/" + name; + TestAssetScope.EnsureFolder(dir); + AnimatorController controller = AnimatorController.CreateAnimatorControllerAtPath(dir + "/fx.controller"); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + return AssetDatabase.LoadAssetAtPath(dir + "/fx.controller"); + } + + private static ScriptableObject CreateExpressionParameters(string name) + { + ScriptableObject parameters = VrcExpressionParametersTestUtil.Create( + name + "Params", + new VrcExpressionParametersTestUtil.ParameterSpec + { + Name = "existing", + Saved = false, + NetworkSynced = false, + ValueType = "Bool", + DefaultValue = 0f + }); + string path = TestAssetScope.CreateAsset(parameters, name + "/params.asset"); + return AssetDatabase.LoadAssetAtPath(path); + } + + private static Material GetBodyMaterial(GameObject avatar) + { + Assert.That(avatar, Is.Not.Null); + Transform body = avatar.transform.Find("Body"); + Assert.That(body, Is.Not.Null); + SkinnedMeshRenderer renderer = body.GetComponent(); + Assert.That(renderer, Is.Not.Null); + Assert.That(renderer.sharedMaterial, Is.Not.Null); + return renderer.sharedMaterial; + } + + private static void AssertEncryptedMaterial(Material material) + { + Assert.That(material, Is.Not.Null); + Assert.That(material.shader, Is.Not.Null); + Assert.That(material.shader.isSupported, Is.True, "Encrypted shader should be supported: " + material.shader.name); + Assert.That(material.GetTexture(ShaderProperties.EncryptTexture0), Is.Not.Null); + Assert.That(material.GetTexture(ShaderProperties.MipTexture), Is.Not.Null); + Assert.That(material.IsKeywordEnabled(ShaderProperties.ChachaKeyword), Is.True, "Encrypted material should enable Chacha keyword."); + Assert.That(material.IsKeywordEnabled(ShaderProperties.Format1Keyword), Is.True, "Encrypted material should enable RGBA format keyword."); + Assert.That(material.IsKeywordEnabled(ShaderProperties.Format0Keyword), Is.False, "Encrypted material should not enable RGB format keyword."); + } + + private static void AssertPoiyomiShaderWasInjected(Shader shader) + { + string path = AssetDatabase.GetAssetPath(shader).Replace('\\', '/'); + Assert.That(path, Does.StartWith(TestAssetScope.GeneratedRoot)); + string shaderData = File.ReadAllText(path); + Assert.That(shaderData, Does.Contain("//ShellProtect")); + Assert.That(shaderData, Does.Contain("Protector.cginc")); + Assert.That(shaderData, Does.Contain(ShaderProperties.EncryptTexture0)); + Assert.That(shaderData, Does.Contain(ShaderProperties.MipTexture)); + } + + private static void AssertTamperedKeyRendersCorruptedOutput(Material encryptedMaterial, Color32[] reference, string label) + { + Material fallbackProbe = null; + Material tamperedProbe = null; + + try + { + fallbackProbe = CreateFallbackProbe(encryptedMaterial); + tamperedProbe = CreateTamperedKeyProbe(encryptedMaterial); + + Color32[] fallback = RenderMaterial(fallbackProbe); + Color32[] tampered = RenderMaterial(tamperedProbe); + + AssertRenderedRgbDifferent(reference, tampered, label + " tampered output should differ from the original"); + AssertRenderedRgbDifferent(fallback, tampered, label + " tampered output should differ from fallback"); + } + finally + { + if (fallbackProbe != null) + Object.DestroyImmediate(fallbackProbe); + if (tamperedProbe != null) + Object.DestroyImmediate(tamperedProbe); + } + } + + private static Material CreateFallbackProbe(Material encryptedMaterial) + { + Material fallbackProbe = new Material(encryptedMaterial); + fallbackProbe.name = encryptedMaterial.name + "FallbackProbe"; + fallbackProbe.SetInteger(ShaderProperties.PasswordHash, fallbackProbe.GetInteger(ShaderProperties.PasswordHash) ^ 0x5A5A5A5A); + return fallbackProbe; + } + + private static Material CreateTamperedKeyProbe(Material encryptedMaterial) + { + Material tamperedProbe = new Material(encryptedMaterial); + tamperedProbe.name = encryptedMaterial.name + "TamperedKeyProbe"; + + byte[] key = ReadKeyBytes(tamperedProbe); + key[0] = (byte)(key[0] ^ 0x5A); + ApplyKeyBytes(tamperedProbe, key); + return tamperedProbe; + } + + private static byte[] ReadKeyBytes(Material material) + { + byte[] key = new byte[16]; + for (int i = 0; i < key.Length; i++) + key[i] = (byte)Mathf.RoundToInt(material.GetFloat(ShaderProperties.KeyPrefix + i)); + return key; + } + + private static void ApplyKeyBytes(Material material, byte[] key) + { + for (int i = 0; i < key.Length; i++) + material.SetFloat(ShaderProperties.KeyPrefix + i, key[i]); + + uint hashMagic = unchecked((uint)material.GetInteger(ShaderProperties.HashMagic)); + uint passwordHash = KeyGenerator.SimpleHash(key, hashMagic); + material.SetInteger(ShaderProperties.PasswordHash, unchecked((int)passwordHash)); + } + + private static Color32[] RenderMaterial(Material material) + { + GameObject subject = null; + GameObject cameraObject = null; + GameObject lightObject = null; + Camera camera = null; + RenderTexture target = null; + Texture2D readback = null; + RenderTexture previous = RenderTexture.active; + AmbientMode previousAmbientMode = RenderSettings.ambientMode; + Color previousAmbientLight = RenderSettings.ambientLight; + + try + { + subject = GameObject.CreatePrimitive(PrimitiveType.Quad); + subject.layer = RenderLayer; + subject.transform.position = Vector3.zero; + subject.transform.localScale = new Vector3(1.1f, 1.1f, 1f); + Object.DestroyImmediate(subject.GetComponent()); + MeshRenderer renderer = subject.GetComponent(); + renderer.sharedMaterial = material; + + cameraObject = new GameObject("ShellProtectorRenderCamera"); + camera = cameraObject.AddComponent(); + cameraObject.transform.position = new Vector3(0f, 0f, -2f); + cameraObject.transform.rotation = Quaternion.identity; + camera.orthographic = true; + camera.orthographicSize = 0.55f; + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = Color.black; + camera.cullingMask = 1 << RenderLayer; + camera.nearClipPlane = 0.1f; + camera.farClipPlane = 10f; + + lightObject = new GameObject("ShellProtectorRenderLight"); + lightObject.layer = RenderLayer; + Light light = lightObject.AddComponent(); + light.type = LightType.Directional; + light.color = Color.white; + light.intensity = 1f; + light.cullingMask = 1 << RenderLayer; + lightObject.transform.rotation = Quaternion.Euler(35f, 30f, 0f); + + RenderSettings.ambientMode = AmbientMode.Flat; + RenderSettings.ambientLight = Color.white; + + target = new RenderTexture(RenderSize, RenderSize, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear); + target.Create(); + camera.targetTexture = target; + camera.Render(); + + RenderTexture.active = target; + readback = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBA32, false, true); + readback.ReadPixels(new Rect(0, 0, RenderSize, RenderSize), 0, 0); + readback.Apply(false, false); + return readback.GetPixels32(); + } + finally + { + RenderSettings.ambientMode = previousAmbientMode; + RenderSettings.ambientLight = previousAmbientLight; + RenderTexture.active = previous; + + if (camera != null) + camera.targetTexture = null; + if (target != null) + target.Release(); + if (target != null) + Object.DestroyImmediate(target); + if (readback != null) + Object.DestroyImmediate(readback); + if (subject != null) + Object.DestroyImmediate(subject); + if (cameraObject != null) + Object.DestroyImmediate(cameraObject); + if (lightObject != null) + Object.DestroyImmediate(lightObject); + } + } + + private static void AssertRenderedRgbClose(Color32[] expected, Color32[] actual, string label) + { + Assert.That(actual.Length, Is.EqualTo(expected.Length)); + + int compared = 0; + long totalDifference = 0; + for (int y = 8; y < RenderSize - 8; y++) + { + for (int x = 8; x < RenderSize - 8; x++) + { + int i = y * RenderSize + x; + AssertChannelClose(expected[i].r, actual[i].r, label, i, "r"); + AssertChannelClose(expected[i].g, actual[i].g, label, i, "g"); + AssertChannelClose(expected[i].b, actual[i].b, label, i, "b"); + totalDifference += AbsDiff(expected[i].r, actual[i].r); + totalDifference += AbsDiff(expected[i].g, actual[i].g); + totalDifference += AbsDiff(expected[i].b, actual[i].b); + compared += 3; + } + } + + double averageDifference = (double)totalDifference / compared; + Assert.That(averageDifference, Is.LessThanOrEqualTo(AverageChannelTolerance), label + " average RGB difference"); + } + + private static void AssertRenderedRgbDifferent(Color32[] expected, Color32[] actual, string label) + { + Assert.That(actual.Length, Is.EqualTo(expected.Length)); + double averageDifference = CalculateAverageRgbDifference(expected, actual); + Assert.That(averageDifference, Is.GreaterThanOrEqualTo(CorruptedAverageChannelDifference), label + " average RGB difference"); + } + + private static double CalculateAverageRgbDifference(Color32[] expected, Color32[] actual) + { + long totalDifference = 0; + int compared = 0; + for (int y = 8; y < RenderSize - 8; y++) + { + for (int x = 8; x < RenderSize - 8; x++) + { + int i = y * RenderSize + x; + totalDifference += AbsDiff(expected[i].r, actual[i].r); + totalDifference += AbsDiff(expected[i].g, actual[i].g); + totalDifference += AbsDiff(expected[i].b, actual[i].b); + compared += 3; + } + } + + return (double)totalDifference / compared; + } + + private static void AssertChannelClose(byte expected, byte actual, string label, int pixel, string channel) + { + int difference = AbsDiff(expected, actual); + if (difference > ChannelTolerance) + Assert.Fail("{0} pixel {1} channel {2} differs. Expected {3}, Actual {4}, Difference {5}", label, pixel, channel, expected, actual, difference); + } + + private static int AbsDiff(byte a, byte b) + { + return a > b ? a - b : b - a; + } + + private static void SetSerializedField(ShellProtector protector, string fieldName, T value) + { + FieldInfo field = typeof(ShellProtector).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(field, Is.Not.Null, fieldName); + field.SetValue(protector, value); + } + + private class Fixture + { + public GameObject Avatar; + public ShellProtector Protector; + public Material Material; + } + } +} +#endif diff --git a/Tests/Editor/Integration/SupportedShaderRenderingTests.cs.meta b/Tests/Editor/Integration/SupportedShaderRenderingTests.cs.meta new file mode 100644 index 0000000..cedea1d --- /dev/null +++ b/Tests/Editor/Integration/SupportedShaderRenderingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a99c1715f3c49afbaf0060f76031833 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Shared.meta b/Tests/Editor/Shared.meta new file mode 100644 index 0000000..de2c6bc --- /dev/null +++ b/Tests/Editor/Shared.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dedafe08f345437d8ba765b4997456a5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Shared/TestAssetScope.cs b/Tests/Editor/Shared/TestAssetScope.cs new file mode 100644 index 0000000..e1068d7 --- /dev/null +++ b/Tests/Editor/Shared/TestAssetScope.cs @@ -0,0 +1,183 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace Shell.Protector.Tests +{ + internal static class TestAssetScope + { + public const string GeneratedRoot = "Assets/ShellProtector/Tests/__Generated"; + public const string DefaultGeneratedRoot = "Assets/ShellProtector/Generated"; + + public static void Reset() + { + DeleteGeneratedRoot(); + DeleteDefaultGeneratedRoot(); + EnsureFolder(GeneratedRoot); + } + + public static void DeleteGeneratedRoot() + { + if (AssetDatabase.IsValidFolder(GeneratedRoot)) + { + AssetDatabase.DeleteAsset(GeneratedRoot); + AssetDatabase.Refresh(); + } + } + + public static void DeleteDefaultGeneratedRoot() + { + if (AssetDatabase.IsValidFolder(DefaultGeneratedRoot)) + { + AssetDatabase.DeleteAsset(DefaultGeneratedRoot); + AssetDatabase.Refresh(); + } + } + + public static void EnsureFolder(string assetPath) + { + string normalized = assetPath.Replace('\\', '/').TrimEnd('/'); + string[] parts = normalized.Split('/'); + if (parts.Length == 0 || parts[0] != "Assets") + throw new ArgumentException("Generated test assets must live under Assets.", nameof(assetPath)); + + string current = "Assets"; + for (int i = 1; i < parts.Length; i++) + { + string next = current + "/" + parts[i]; + if (!AssetDatabase.IsValidFolder(next)) + AssetDatabase.CreateFolder(current, parts[i]); + current = next; + } + } + + public static string CreateAsset(T asset, string relativePath) where T : UnityEngine.Object + { + EnsureFolder(GeneratedRoot); + string path = GeneratedRoot + "/" + relativePath.Replace('\\', '/'); + EnsureFolder(Path.GetDirectoryName(path).Replace('\\', '/')); + AssetDatabase.CreateAsset(asset, path); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + return path; + } + + public static Texture2D CreatePatternTexture(int width, int height, TextureFormat format, bool alpha) + { + Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, true, true); + Color32[] pixels = new Color32[width * height]; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + byte r = (byte)((x * 17 + y * 3) & 0xFF); + byte g = (byte)((x * 5 + y * 29) & 0xFF); + byte b = (byte)((x * 11 + y * 7 + 31) & 0xFF); + byte a = alpha ? (byte)(64 + ((x * 13 + y * 19) & 0x7F)) : (byte)255; + pixels[y * width + x] = new Color32(r, g, b, a); + } + } + if (format == TextureFormat.DXT1 || format == TextureFormat.DXT5) + FillDxtStablePattern(pixels, width, height, alpha); + + texture.SetPixels32(pixels); + texture.Apply(true, false); + + if (format == TextureFormat.RGB24) + { + Texture2D rgb = new Texture2D(width, height, TextureFormat.RGB24, true, true); + rgb.SetPixels32(texture.GetPixels32()); + rgb.Apply(true, false); + UnityEngine.Object.DestroyImmediate(texture); + return rgb; + } + + if (format == TextureFormat.RGBA32) + return texture; + + TextureFormat uncompressedFormat = format == TextureFormat.DXT1 ? TextureFormat.RGB24 : TextureFormat.RGBA32; + Texture2D dxt = new Texture2D(width, height, uncompressedFormat, true, true); + dxt.SetPixels32(texture.GetPixels32()); + dxt.Apply(true, false); + dxt.Compress(format == TextureFormat.DXT5); + dxt.Apply(true, false); + UnityEngine.Object.DestroyImmediate(texture); + return dxt; + } + + private static void FillDxtStablePattern(Color32[] pixels, int width, int height, bool alpha) + { + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int block = (x / 4) + (y / 4) * (width / 4); + Color32 c0 = new Color32( + Expand5((block * 3 + 4) & 31), + Expand6((block * 5 + 9) & 63), + Expand5((block * 7 + 13) & 31), + alpha ? (byte)96 : (byte)255); + Color32 c1 = new Color32( + Expand5((block * 11 + 17) & 31), + Expand6((block * 13 + 21) & 63), + Expand5((block * 15 + 25) & 31), + alpha ? (byte)192 : (byte)255); + pixels[y * width + x] = ((x + y) & 1) == 0 ? c0 : c1; + } + } + } + + private static byte Expand5(int value) + { + return (byte)((value * 255 + 15) / 31); + } + + private static byte Expand6(int value) + { + return (byte)((value * 255 + 31) / 63); + } + + public static Mesh CreateBlendShapeQuadMesh() + { + Mesh mesh = new Mesh(); + mesh.name = "ShellProtectorTestMesh"; + mesh.vertices = new[] + { + new Vector3(-0.5f, -0.5f, 0f), + new Vector3(0.5f, -0.5f, 0f), + new Vector3(-0.5f, 0.5f, 0f), + new Vector3(0.5f, 0.5f, 0f) + }; + mesh.uv = new[] + { + new Vector2(0f, 0f), + new Vector2(1f, 0f), + new Vector2(0f, 1f), + new Vector2(1f, 1f) + }; + mesh.triangles = new[] { 0, 2, 1, 2, 3, 1 }; + mesh.RecalculateNormals(); + + Vector3[] deltaVertices = new Vector3[mesh.vertexCount]; + Vector3[] deltaNormals = new Vector3[mesh.vertexCount]; + Vector3[] deltaTangents = new Vector3[mesh.vertexCount]; + for (int i = 0; i < deltaVertices.Length; i++) + deltaVertices[i] = new Vector3(0f, 0.01f * (i + 1), 0f); + mesh.AddBlendShapeFrame("Smile", 100f, deltaVertices, deltaNormals, deltaTangents); + return mesh; + } + + public static void DestroyObjects(IEnumerable objects) + { + foreach (UnityEngine.Object obj in objects) + { + if (obj != null) + UnityEngine.Object.DestroyImmediate(obj); + } + } + } +} +#endif diff --git a/Tests/Editor/Shared/TestAssetScope.cs.meta b/Tests/Editor/Shared/TestAssetScope.cs.meta new file mode 100644 index 0000000..236dbb9 --- /dev/null +++ b/Tests/Editor/Shared/TestAssetScope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 714444c3456449f9b92ad7aa96653347 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Shared/VrcExpressionParametersTestUtil.cs b/Tests/Editor/Shared/VrcExpressionParametersTestUtil.cs new file mode 100644 index 0000000..419b91c --- /dev/null +++ b/Tests/Editor/Shared/VrcExpressionParametersTestUtil.cs @@ -0,0 +1,220 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Shell.Protector; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.Components; + +namespace Shell.Protector.Tests +{ + internal static class VrcExpressionParametersTestUtil + { + public struct ParameterSpec + { + public string Name; + public bool Saved; + public bool NetworkSynced; + public string ValueType; + public float DefaultValue; + } + + public struct ParameterSnapshot + { + public string Name; + public bool Saved; + public bool NetworkSynced; + public string ValueType; + public float DefaultValue; + } + + private static Type expressionParametersType; + private static Type parameterType; + private static Type valueTypeEnum; + + public static ScriptableObject Create(string name, params ParameterSpec[] specs) + { + ScriptableObject parameters = ScriptableObject.CreateInstance(ExpressionParametersType); + parameters.name = name; + SetParameters(parameters, specs); + return parameters; + } + + public static ScriptableObject AddKeyParameter(ScriptableObject parameters, int keyLength, int syncSize) + { + MethodInfo method = typeof(ParameterManager).GetMethod( + "AddKeyParameter", + BindingFlags.Public | BindingFlags.Static); + AssertMethod(method, "ParameterManager.AddKeyParameter"); + return (ScriptableObject)method.Invoke(null, new object[] { parameters, keyLength, syncSize }); + } + + public static IReadOnlyList Read(ScriptableObject parameters) + { + object[] values = GetParameterArray(parameters); + return values.Select(ReadParameter).ToArray(); + } + + public static void SetDescriptorParameters(VRCAvatarDescriptor descriptor, ScriptableObject parameters) + { + FieldInfo field = typeof(VRCAvatarDescriptor).GetField("expressionParameters"); + if (field != null) + { + field.SetValue(descriptor, parameters); + return; + } + + PropertyInfo property = typeof(VRCAvatarDescriptor).GetProperty("expressionParameters"); + if (property != null) + { + property.SetValue(descriptor, parameters); + return; + } + + SerializedObject serializedDescriptor = new SerializedObject(descriptor); + SerializedProperty serializedProperty = serializedDescriptor.FindProperty("expressionParameters"); + if (serializedProperty == null) + throw new MissingMemberException(typeof(VRCAvatarDescriptor).FullName, "expressionParameters"); + + serializedProperty.objectReferenceValue = parameters; + serializedDescriptor.ApplyModifiedPropertiesWithoutUndo(); + } + + public static ScriptableObject GetDescriptorParameters(VRCAvatarDescriptor descriptor) + { + FieldInfo field = typeof(VRCAvatarDescriptor).GetField("expressionParameters"); + if (field != null) + return (ScriptableObject)field.GetValue(descriptor); + + PropertyInfo property = typeof(VRCAvatarDescriptor).GetProperty("expressionParameters"); + if (property != null) + return (ScriptableObject)property.GetValue(descriptor); + + SerializedObject serializedDescriptor = new SerializedObject(descriptor); + SerializedProperty serializedProperty = serializedDescriptor.FindProperty("expressionParameters"); + return serializedProperty == null ? null : serializedProperty.objectReferenceValue as ScriptableObject; + } + + private static Type ExpressionParametersType + { + get + { + expressionParametersType ??= FindType("VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionParameters"); + return expressionParametersType; + } + } + + private static Type ParameterType + { + get + { + parameterType ??= ExpressionParametersType.GetNestedType("Parameter"); + return parameterType; + } + } + + private static Type ValueTypeEnum + { + get + { + valueTypeEnum ??= ExpressionParametersType.GetNestedType("ValueType"); + return valueTypeEnum; + } + } + + private static void SetParameters(ScriptableObject parameters, ParameterSpec[] specs) + { + Array values = Array.CreateInstance(ParameterType, specs.Length); + for (int i = 0; i < specs.Length; i++) + { + object parameter = Activator.CreateInstance(ParameterType); + SetMember(parameter, "name", specs[i].Name); + SetMember(parameter, "saved", specs[i].Saved); + SetMember(parameter, "networkSynced", specs[i].NetworkSynced); + SetMember(parameter, "valueType", Enum.Parse(ValueTypeEnum, specs[i].ValueType)); + SetMember(parameter, "defaultValue", specs[i].DefaultValue); + values.SetValue(parameter, i); + } + + SetMember(parameters, "parameters", values); + } + + private static object[] GetParameterArray(ScriptableObject parameters) + { + object value = GetMember(parameters, "parameters"); + if (value is Array array) + { + object[] values = new object[array.Length]; + array.CopyTo(values, 0); + return values; + } + + return Array.Empty(); + } + + private static ParameterSnapshot ReadParameter(object parameter) + { + return new ParameterSnapshot + { + Name = (string)GetMember(parameter, "name"), + Saved = (bool)GetMember(parameter, "saved"), + NetworkSynced = (bool)GetMember(parameter, "networkSynced"), + ValueType = GetMember(parameter, "valueType").ToString(), + DefaultValue = (float)GetMember(parameter, "defaultValue") + }; + } + + private static Type FindType(string fullName) + { + Type type = AppDomain.CurrentDomain.GetAssemblies() + .Select(assembly => assembly.GetType(fullName, false)) + .FirstOrDefault(candidate => candidate != null); + + if (type == null) + throw new TypeLoadException(fullName); + + return type; + } + + private static object GetMember(object target, string name) + { + FieldInfo field = target.GetType().GetField(name); + if (field != null) + return field.GetValue(target); + + PropertyInfo property = target.GetType().GetProperty(name); + if (property != null) + return property.GetValue(target); + + throw new MissingMemberException(target.GetType().FullName, name); + } + + private static void SetMember(object target, string name, object value) + { + FieldInfo field = target.GetType().GetField(name); + if (field != null) + { + field.SetValue(target, value); + return; + } + + PropertyInfo property = target.GetType().GetProperty(name); + if (property != null) + { + property.SetValue(target, value); + return; + } + + throw new MissingMemberException(target.GetType().FullName, name); + } + + private static void AssertMethod(MethodInfo method, string name) + { + if (method == null) + throw new MissingMethodException(name); + } + } +} +#endif diff --git a/Tests/Editor/Shared/VrcExpressionParametersTestUtil.cs.meta b/Tests/Editor/Shared/VrcExpressionParametersTestUtil.cs.meta new file mode 100644 index 0000000..a0b638b --- /dev/null +++ b/Tests/Editor/Shared/VrcExpressionParametersTestUtil.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8f6bb62a6034c6d95f74464e80520b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Shell.Protector.Editor.Tests.asmdef b/Tests/Editor/Shell.Protector.Editor.Tests.asmdef new file mode 100644 index 0000000..3e551a9 --- /dev/null +++ b/Tests/Editor/Shell.Protector.Editor.Tests.asmdef @@ -0,0 +1,28 @@ +{ + "name": "shell.protector.Editor.Tests", + "rootNamespace": "Shell.Protector.Tests", + "references": [ + "shell.protector", + "VRC.SDK3A", + "VRC.SDK3A.Editor", + "VRC.SDKBase.Editor", + "VRC.SDKBase" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [ + "VRCSDK3A.dll", + "VRCSDKBase.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "noEngineReferences": false +} diff --git a/Tests/Editor/Shell.Protector.Editor.Tests.asmdef.meta b/Tests/Editor/Shell.Protector.Editor.Tests.asmdef.meta new file mode 100644 index 0000000..8afbb34 --- /dev/null +++ b/Tests/Editor/Shell.Protector.Editor.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ea291350a3b04158a0d418640566cfdb +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Unit.meta b/Tests/Editor/Unit.meta new file mode 100644 index 0000000..b6c1717 --- /dev/null +++ b/Tests/Editor/Unit.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e05b0aeb969149a2b286ea2bbc0f5660 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Unit/CryptoTests.cs b/Tests/Editor/Unit/CryptoTests.cs new file mode 100644 index 0000000..ffb13c1 --- /dev/null +++ b/Tests/Editor/Unit/CryptoTests.cs @@ -0,0 +1,95 @@ +#if UNITY_EDITOR +using System.Linq; +using NUnit.Framework; + +namespace Shell.Protector.Tests.Unit +{ + public class CryptoTests + { + [Test] + public void MakeKeyBytes_ReturnsStablePasswordKey() + { + byte[] key = KeyGenerator.MakeKeyBytes("password", "pass", 12); + + Assert.That(ToHex(key), Is.EqualTo("70617373a72e839d8da3b9806b18c877")); + } + + [Test] + public void MakeKeyBytes_UsesFixedPasswordPrefix() + { + byte[] key = KeyGenerator.MakeKeyBytes("fixed", "user", 12); + + Assert.That(key, Has.Length.EqualTo(16)); + Assert.That(key[0], Is.EqualTo((byte)'f')); + Assert.That(key[1], Is.EqualTo((byte)'i')); + } + + [Test] + public void SimpleHash_ReturnsStableHash() + { + byte[] key = KeyGenerator.MakeKeyBytes("password", "pass", 12); + + Assert.That(KeyGenerator.SimpleHash(key, 0x12345678u), Is.EqualTo(0x94f301a9u)); + } + + [Test] + public void GenerateRandomString_UsesRequestedLengthAndAllowedCharacters() + { + const string allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+=|\\/?.>,<~`\'\" "; + + string value = KeyGenerator.GenerateRandomString(64); + + Assert.That(value, Has.Length.EqualTo(64)); + Assert.That(value.All(c => allowedChars.IndexOf(c) >= 0), Is.True); + Assert.That(KeyGenerator.GenerateRandomString(0), Is.Empty); + } + + [Test] + public void Xxtea_EncryptsStableVector_AndRoundTrips() + { + XXTEA xxtea = new XXTEA(); + uint[] data = { 0x01234567u, 0x89abcdefu, 0xfedcba98u }; + uint[] key = { 0x00112233u, 0x44556677u, 0x8899aabbu, 0xccddeeffu }; + + uint[] encrypted = xxtea.Encrypt(data, key); + + Assert.That(encrypted, Is.EqualTo(new[] { 0x6d601d05u, 0x17b6001eu, 0x9547b398u })); + Assert.That(xxtea.Decrypt(encrypted, key), Is.EqualTo(data)); + } + + [Test] + public void Xxtea_WithExplicitShellProtectorRounds_RoundTrips() + { + XXTEA xxtea = new XXTEA { Rounds = 20 }; + uint[] data = { 0x10203040u, 0x50607080u }; + uint[] key = { 0x00112233u, 0x44556677u, 0x8899aabbu, 0xccddeeffu }; + + uint[] encrypted = xxtea.Encrypt(data, key); + + Assert.That(encrypted, Is.Not.EqualTo(data)); + Assert.That(xxtea.Decrypt(encrypted, key), Is.EqualTo(data)); + } + + [Test] + public void Chacha20_EncryptsStableVector_AndRoundTrips() + { + Chacha20 chacha = new Chacha20(); + for (int i = 0; i < chacha.Nonce.Length; i++) + chacha.Nonce[i] = (byte)i; + + uint[] data = { 0x00010203u, 0x04050607u, 0x08090a0bu, 0x0c0d0e0fu }; + uint[] key = { 0x00112233u, 0x44556677u, 0x8899aabbu, 0xccddeeffu }; + + uint[] encrypted = chacha.Encrypt(data, key); + + Assert.That(encrypted, Is.EqualTo(new[] { 0xed4198d8u, 0x7ca6601fu, 0xde134dedu, 0x76b88629u })); + Assert.That(chacha.Decrypt(encrypted, key), Is.EqualTo(data)); + } + + private static string ToHex(byte[] bytes) + { + return string.Concat(bytes.Select(b => b.ToString("x2"))); + } + } +} +#endif diff --git a/Tests/Editor/Unit/CryptoTests.cs.meta b/Tests/Editor/Unit/CryptoTests.cs.meta new file mode 100644 index 0000000..044be9c --- /dev/null +++ b/Tests/Editor/Unit/CryptoTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ec88e66595dc43e685a88fba8239c55e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Unit/ParameterManagerTests.cs b/Tests/Editor/Unit/ParameterManagerTests.cs new file mode 100644 index 0000000..2a4b776 --- /dev/null +++ b/Tests/Editor/Unit/ParameterManagerTests.cs @@ -0,0 +1,70 @@ +#if UNITY_EDITOR +using System.Linq; +using NUnit.Framework; +using UnityEngine; + +namespace Shell.Protector.Tests.Unit +{ + public class ParameterManagerTests + { + [Test] + public void AddKeyParameter_UsesLegacyNamesForSyncSizeOne() + { + ScriptableObject original = CreateBaseParameters(); + + ScriptableObject result = VrcExpressionParametersTestUtil.AddKeyParameter(original, 12, 1); + VrcExpressionParametersTestUtil.ParameterSnapshot[] parameters = VrcExpressionParametersTestUtil.Read(result).ToArray(); + + Assert.That(result.name, Is.EqualTo("BaseParams_encrypted")); + Assert.That(parameters.Length, Is.EqualTo(19)); + AssertParameter(parameters, "existing", false, false, "Bool"); + AssertParameter(parameters, "encrypt_lock", true, true, "Bool"); + AssertParameter(parameters, "pkey", true, true, "Float"); + AssertParameter(parameters, "encrypt_switch0", true, true, "Bool"); + AssertParameter(parameters, "encrypt_switch3", true, true, "Bool"); + AssertParameter(parameters, "SHELL_PROTECTOR_key11", false, false, "Float"); + Assert.That(parameters.Any(p => p.Name == "SHELL_PROTECTOR_saved_key0"), Is.False); + } + + [Test] + public void AddKeyParameter_UsesPrefixedNamesForMultiSync() + { + ScriptableObject original = CreateBaseParameters(); + + ScriptableObject result = VrcExpressionParametersTestUtil.AddKeyParameter(original, 12, 3); + VrcExpressionParametersTestUtil.ParameterSnapshot[] parameters = VrcExpressionParametersTestUtil.Read(result).ToArray(); + + Assert.That(parameters.Length, Is.EqualTo(31)); + AssertParameter(parameters, "SHELL_PROTECTOR_sync_lock", true, true, "Bool"); + AssertParameter(parameters, "SHELL_PROTECTOR_synced_key0", true, true, "Float"); + AssertParameter(parameters, "SHELL_PROTECTOR_synced_key2", true, true, "Float"); + AssertParameter(parameters, "SHELL_PROTECTOR_sync_switch1", true, true, "Bool"); + AssertParameter(parameters, "SHELL_PROTECTOR_key11", false, false, "Float"); + AssertParameter(parameters, "SHELL_PROTECTOR_saved_key11", true, false, "Float"); + } + + private static ScriptableObject CreateBaseParameters() + { + return VrcExpressionParametersTestUtil.Create( + "BaseParams", + new VrcExpressionParametersTestUtil.ParameterSpec + { + Name = "existing", + Saved = false, + NetworkSynced = false, + ValueType = "Bool", + DefaultValue = 0f + }); + } + + private static void AssertParameter(VrcExpressionParametersTestUtil.ParameterSnapshot[] parameters, string name, bool saved, bool networkSynced, string valueType) + { + VrcExpressionParametersTestUtil.ParameterSnapshot parameter = parameters.Single(p => p.Name == name); + Assert.That(parameter.Saved, Is.EqualTo(saved), name); + Assert.That(parameter.NetworkSynced, Is.EqualTo(networkSynced), name); + Assert.That(parameter.ValueType, Is.EqualTo(valueType), name); + Assert.That(parameter.DefaultValue, Is.EqualTo(0f), name); + } + } +} +#endif diff --git a/Tests/Editor/Unit/ParameterManagerTests.cs.meta b/Tests/Editor/Unit/ParameterManagerTests.cs.meta new file mode 100644 index 0000000..52bdfc8 --- /dev/null +++ b/Tests/Editor/Unit/ParameterManagerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96bd17070d9a4ca1a8140a1f0715d419 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Unit/TextureEncryptManagerTests.cs b/Tests/Editor/Unit/TextureEncryptManagerTests.cs new file mode 100644 index 0000000..c586d74 --- /dev/null +++ b/Tests/Editor/Unit/TextureEncryptManagerTests.cs @@ -0,0 +1,100 @@ +#if UNITY_EDITOR +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Shell.Protector.Tests.Unit +{ + public class TextureEncryptManagerTests + { + [TestCase(TextureFormat.RGB24, true)] + [TestCase(TextureFormat.RGBA32, true)] + [TestCase(TextureFormat.DXT1, false)] + [TestCase(TextureFormat.DXT5, true)] + public void SupportsExpectedTextureFormats(TextureFormat format, bool alpha) + { + Texture2D texture = TestAssetScope.CreatePatternTexture(16, 16, format, alpha); + + Assert.That(TextureEncryptManager.IsSupportedTexture(texture), Is.True); + } + + [Test] + public void CalculatesOffsetsPerFormat() + { + Texture2D rgb = TestAssetScope.CreatePatternTexture(16, 16, TextureFormat.RGB24, false); + Texture2D rgba = TestAssetScope.CreatePatternTexture(16, 16, TextureFormat.RGBA32, true); + Texture2D dxt = TestAssetScope.CreatePatternTexture(16, 16, TextureFormat.DXT1, false); + + Assert.That(TextureEncryptManager.CalculateOffsets(rgb), Is.EqualTo((8, 8))); + Assert.That(TextureEncryptManager.CalculateOffsets(rgba), Is.EqualTo((8, 8))); + Assert.That(TextureEncryptManager.CalculateOffsets(dxt), Is.EqualTo((10, 10))); + } + + [Test] + public void GeneratesMipReferenceTexture() + { + Texture2D mip = TextureEncryptManager.GenerateRefMipmap(16, 16, false); + + Assert.That(mip.width, Is.EqualTo(16)); + Assert.That(mip.height, Is.EqualTo(16)); + Assert.That(mip.GetPixels32(0)[0].r, Is.EqualTo(0)); + Assert.That(mip.GetPixels32(1)[0].r, Is.EqualTo(10)); + } + + [Test] + public void GeneratesFallbackTexture() + { + Texture2D original = TestAssetScope.CreatePatternTexture(128, 128, TextureFormat.RGBA32, true); + + Texture2D fallback = TextureEncryptManager.GenerateFallback(original, 32); + + Assert.That(fallback, Is.Not.Null); + Assert.That(fallback.width, Is.EqualTo(32)); + Assert.That(fallback.height, Is.EqualTo(32)); + Assert.That(fallback.format, Is.EqualTo(TextureFormat.DXT5)); + Assert.That(fallback.filterMode, Is.EqualTo(FilterMode.Point)); + } + + [TestCase(TextureFormat.RGB24, true, TextureFormat.RGB24)] + [TestCase(TextureFormat.RGBA32, true, TextureFormat.RGBA32)] + [TestCase(TextureFormat.DXT1, false, TextureFormat.DXT1)] + [TestCase(TextureFormat.DXT5, true, TextureFormat.DXT5)] + public void EncryptTextureCreatesExpectedResultTextures(TextureFormat sourceFormat, bool alpha, TextureFormat encryptedFormat) + { + Texture2D texture = TestAssetScope.CreatePatternTexture(16, 16, sourceFormat, alpha); + XXTEA xxtea = new XXTEA { Rounds = 20 }; + byte[] key = KeyGenerator.MakeKeyBytes("password", "pass", 12); + + EncryptResult result = TextureEncryptManager.EncryptTexture(texture, key, xxtea); + + Assert.That(result.Texture1, Is.Not.Null); + Assert.That(result.Texture1.format, Is.EqualTo(encryptedFormat)); + if (TextureEncryptManager.IsDXTFormat(sourceFormat)) + { + Assert.That(result.Texture2, Is.Not.Null); + Assert.That(result.Texture2.format, Is.EqualTo(TextureFormat.RGBA32)); + } + else + { + Assert.That(result.Texture2, Is.Null); + } + } + + [TestCase(15, 16)] + [TestCase(16, 15)] + public void EncryptTextureRejectsTextureWhenEitherDimensionIsOdd(int width, int height) + { + Texture2D texture = TestAssetScope.CreatePatternTexture(width, height, TextureFormat.RGBA32, true); + texture.name = "OddTexture"; + XXTEA xxtea = new XXTEA { Rounds = 20 }; + byte[] key = KeyGenerator.MakeKeyBytes("password", "pass", 12); + + LogAssert.Expect(LogType.Error, "OddTexture : The texture size must be a multiple of 2!"); + EncryptResult result = TextureEncryptManager.EncryptTexture(texture, key, xxtea); + + Assert.That(result.Texture1, Is.Null); + Assert.That(result.Texture2, Is.Null); + } + } +} +#endif diff --git a/Tests/Editor/Unit/TextureEncryptManagerTests.cs.meta b/Tests/Editor/Unit/TextureEncryptManagerTests.cs.meta new file mode 100644 index 0000000..08dc32c --- /dev/null +++ b/Tests/Editor/Unit/TextureEncryptManagerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0da572332c494b3a98efcf6ed43a0540 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 8dfe9d2..b609102 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "shell.protector", "displayName": "Shell Protector", - "version": "2.4.5", + "version": "2.6.3", "unity": "2022.3", "description": "Texture encryption for VRChat. Save avatars from ripping", "author": { diff --git a/version.json b/version.json index 6b15fc5..3c35077 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{ "latestVersion": "2.4.5", "downloadPage": "/releases/tag/2.4.5" } \ No newline at end of file +{ "latestVersion": "2.6.3", "downloadPage": "/releases/tag/2.6.3" } \ No newline at end of file