diff --git a/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageEditorWindow.cs b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageEditorWindow.cs new file mode 100644 index 0000000..539bff0 --- /dev/null +++ b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageEditorWindow.cs @@ -0,0 +1,362 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditorInternal; + +namespace JLChnToZ.VRC.VVMW.I18N.Editors { + public class LanguageEditorWindow : EditorWindow { + public LanguageManager LanguageManager { get; private set; } + readonly Dictionary + defaultLanguageMap = new Dictionary(), + currentLanguageMap = new Dictionary(); + Vector2 langViewPosition, langKeyPosition; + readonly List + langList = new List(), + allKeysList = new List(); + readonly HashSet allKeys = new HashSet(), allTimeZones = new HashSet(), defaultTimeZones = new HashSet(); + ReorderableList langListSelect, langKeySelect; + LanguageEntry selectedEntry, selectedDefaultEntry; + GUIContent textContent; + string addLanguageTempString = "", addLanguageKeyTempString = "", addLanguageTempValueString = "", addTimeZoneTempString = ""; + GUIStyle wrapTextAreaStyle, wrapBoldTextAreaStyle; + + public static LanguageEditorWindow Open(LanguageManager languageManager) { + if (languageManager == null) return null; + var window = GetWindow(); + window.LanguageManager = languageManager; + window.titleContent = new GUIContent("Language Editor"); + window.Show(); + window.RefreshAll(); + return window; + } + + void Awake() { + textContent = new GUIContent(); + langListSelect = new ReorderableList(langList, typeof(string), false, false, false, true) { + showDefaultBackground = false, + drawElementCallback = DrawLangList, + onSelectCallback = OnLangSelect, + onCanRemoveCallback = CanRemoveLang, + onRemoveCallback = OnRemoveLang, + elementHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing, + headerHeight = 0, + }; + langKeySelect = new ReorderableList(allKeysList, typeof((string, string)), false, false, false, true) { + showDefaultBackground = false, + drawElementCallback = DrawLangKeys, + onCanRemoveCallback = CanRemoveKey, + onRemoveCallback = OnRemoveKey, + elementHeightCallback = MeasureLangListElementHeight, + headerHeight = 0, + }; + wrapTextAreaStyle = new GUIStyle(EditorStyles.textArea) { + wordWrap = true, + }; + wrapBoldTextAreaStyle = new GUIStyle(wrapTextAreaStyle) { + fontStyle = FontStyle.Bold, + }; + } + + void OnDisable() { + Save(); + LanguageManager = null; + } + + void RefreshAll() { + defaultLanguageMap.Clear(); + currentLanguageMap.Clear(); + langList.Clear(); + allKeysList.Clear(); + allKeys.Clear(); + allTimeZones.Clear(); + defaultTimeZones.Clear(); + if (LanguageManager == null) return; + using (var so = new SerializedObject(LanguageManager)) { + Load(so.FindProperty("languageJsonFiles")); + var additionalJson = so.FindProperty("languageJson"); + if (!string.IsNullOrEmpty(additionalJson.stringValue)) + LanguageManagerUnifier.ParseFromJson( + additionalJson.stringValue, null, null, allKeys, currentLanguageMap + ); + } + RefreshIndexes(); + } + + public void RefreshJsonLists() { + if (LanguageManager == null) return; + using (var so = new SerializedObject(LanguageManager)) + Load(so.FindProperty("languageJsonFiles")); + RefreshIndexes(processCurrentMap: true); + OnLangSelect(); + } + + void Load(SerializedProperty langPacks) { + defaultLanguageMap.Clear(); + allKeys.Clear(); + var keyStack = new List(); + for (int i = 0, count = langPacks.arraySize; i < count; i++) { + var textAsset = langPacks.GetArrayElementAtIndex(i).objectReferenceValue as TextAsset; + if (textAsset != null) + LanguageManagerUnifier.ParseFromJson( + textAsset.text, keyStack, null, allKeys, defaultLanguageMap + ); + } + } + + void RefreshIndexes(bool processDefaultMap = false, bool processCurrentMap = false) { + var langs = new HashSet(defaultLanguageMap.Keys); + langs.UnionWith(currentLanguageMap.Keys); + langList.Clear(); + langList.AddRange(langs); + if (processDefaultMap) + foreach (var lang in defaultLanguageMap.Values) + allKeys.UnionWith(lang.languages.Keys); + if (processCurrentMap) + foreach (var lang in currentLanguageMap.Values) + allKeys.UnionWith(lang.languages.Keys); + allKeysList.Clear(); + allKeysList.AddRange(allKeys); + } + + void Save() { + if (LanguageManager == null) return; + using (var so = new SerializedObject(LanguageManager)) { + var additionalJson = so.FindProperty("languageJson"); + var keyStack = new List(); + additionalJson.stringValue = LanguageManagerUnifier.WriteToJson(currentLanguageMap, prettyPrint: true).Trim(); + so.ApplyModifiedProperties(); + } + } + + void OnGUI() { + using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { + using (var changed = new EditorGUI.ChangeCheckScope()) { + LanguageManager = EditorGUILayout.ObjectField(LanguageManager, typeof(LanguageManager), true) as LanguageManager; + if (changed.changed) RefreshAll(); + } + if (GUILayout.Button("Reload", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) RefreshAll(); + if (GUILayout.Button("Save", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) Save(); + GUILayout.FlexibleSpace(); + } + using (new EditorGUILayout.HorizontalScope()) { + using (var vert = new EditorGUILayout.VerticalScope(GUILayout.MaxWidth(Mathf.Min(400, position.width / 2)))) { + if (LanguageManager == null) + EditorGUILayout.HelpBox("Please select a Language Handler first.", MessageType.Info); + else { + using (var scroll = new EditorGUILayout.ScrollViewScope(langViewPosition, GUI.skin.box)) { + langViewPosition = scroll.scrollPosition; + langListSelect.DoLayoutList(); + GUILayout.FlexibleSpace(); + } + using (new EditorGUILayout.HorizontalScope()) { + addLanguageTempString = EditorGUILayout.TextField("Add Language", addLanguageTempString); + using (new EditorGUI.DisabledScope(string.IsNullOrEmpty(addLanguageTempString))) + if (GUILayout.Button("Add", GUILayout.ExpandWidth(false))) { + if (currentLanguageMap.ContainsKey(addLanguageTempString)) + EditorUtility.DisplayDialog("Error", "Language already exists.", "OK"); + else { + currentLanguageMap.Add(addLanguageTempString, selectedEntry = new LanguageEntry()); + langList.Add(addLanguageTempString); + addLanguageTempString = ""; + OnLangSelect(langList.Count - 1); + } + } + } + } + } + using (var vert = new EditorGUILayout.VerticalScope()) { + if (selectedEntry == null && selectedDefaultEntry == null) + EditorGUILayout.HelpBox("Please select a language first.", MessageType.Info); + else { + using (var scroll = new EditorGUILayout.ScrollViewScope(langKeyPosition, GUI.skin.box)) { + langKeyPosition = scroll.scrollPosition; + var name = selectedEntry?.name ?? selectedDefaultEntry?.name; + using (var changed = new EditorGUI.ChangeCheckScope()) { + name = EditorGUILayout.TextField("Native Language Name", name); + if (changed.changed) GetOrCreateLanguageEntry().name = name; + } + var vrcName = selectedEntry?.vrcName ?? selectedDefaultEntry?.vrcName; + using (var changed = new EditorGUI.ChangeCheckScope()) { + vrcName = EditorGUILayout.TextField("VRChat Language Name", vrcName); + if (changed.changed) GetOrCreateLanguageEntry().vrcName = vrcName; + } + DrawTimeZones(); + EditorGUILayout.Space(); + langKeySelect.DoLayoutList(); + GUILayout.FlexibleSpace(); + } + using (new EditorGUILayout.HorizontalScope()) { + addLanguageKeyTempString = EditorGUILayout.TextField(addLanguageKeyTempString, GUILayout.Width(EditorGUIUtility.labelWidth)); + addLanguageTempValueString = EditorGUILayout.TextArea(addLanguageTempValueString, wrapTextAreaStyle); + using (new EditorGUI.DisabledScope(string.IsNullOrEmpty(addLanguageKeyTempString))) + if (GUILayout.Button("Add", GUILayout.ExpandWidth(false))) { + if (selectedEntry != null && selectedEntry.languages.ContainsKey(addLanguageKeyTempString)) + EditorUtility.DisplayDialog("Error", "Key already exists.", "OK"); + else { + GetOrCreateLanguageEntry().languages.Add(addLanguageKeyTempString, addLanguageTempValueString); + allKeys.Add(addLanguageKeyTempString); + addLanguageKeyTempString = ""; + addLanguageTempValueString = ""; + } + } + } + } + } + } + } + + LanguageEntry GetOrCreateLanguageEntry() { + if (selectedEntry != null) return selectedEntry; + if (selectedDefaultEntry != null) { + selectedEntry = new LanguageEntry(); + currentLanguageMap.Add(langList[langListSelect.index], selectedEntry); + return selectedEntry; + } + return null; + } + + void DrawLangList(Rect rect, int index, bool isActive, bool isFocused) { + var value = langList[index]; + EditorGUI.LabelField(rect, value, currentLanguageMap.ContainsKey(value) ? EditorStyles.boldLabel : EditorStyles.label); + } + + void OnLangSelect(ReorderableList list) { + selectedEntry = null; + selectedDefaultEntry = null; + var key = langList[list.index]; + allTimeZones.Clear(); + defaultTimeZones.Clear(); + if (currentLanguageMap.TryGetValue(key, out LanguageEntry entry)) { + selectedEntry = entry; + allTimeZones.UnionWith(entry.timezones); + } + if (defaultLanguageMap.TryGetValue(key, out entry)) { + selectedDefaultEntry = entry; + allTimeZones.UnionWith(entry.timezones); + defaultTimeZones.UnionWith(entry.timezones); + } + } + + void OnLangSelect(int index = -1) { + if (index < 0) index = langListSelect.index; + OnLangSelect(langListSelect); + langListSelect.index = index; + } + + bool CanRemoveLang(ReorderableList list) => + list.index >= 0 && + list.index < langList.Count && + currentLanguageMap.ContainsKey(langList[list.index]); + + void OnRemoveLang(ReorderableList list) { + var key = langList[list.index]; + if (currentLanguageMap.ContainsKey(key)) currentLanguageMap.Remove(key); + OnLangSelect(); + } + + void DrawTimeZones() { + using (new EditorGUILayout.HorizontalScope()) { + EditorGUILayout.LabelField("Time Zones", GUILayout.Width(EditorGUIUtility.labelWidth)); + foreach (var tz in allTimeZones) + using (new EditorGUI.DisabledScope(defaultTimeZones.Contains(tz))) { + textContent.text = tz; + EditorGUILayout.LabelField(textContent, GUILayout.Width(EditorStyles.label.CalcSize(textContent).x)); + if (GUILayout.Button("-", GUILayout.ExpandWidth(false))) { + allTimeZones.Remove(tz); + if (selectedEntry != null) selectedEntry.timezones.Remove(tz); + break; + } + } + GUILayout.FlexibleSpace(); + addTimeZoneTempString = EditorGUILayout.TextField(addTimeZoneTempString, GUILayout.ExpandWidth(false)); + if (GUILayout.Button("+", GUILayout.ExpandWidth(false))) { + if (!allTimeZones.Add(addTimeZoneTempString)) + EditorUtility.DisplayDialog("Error", "Time Zone already exists.", "OK"); + else { + GetOrCreateLanguageEntry().timezones.Add(addTimeZoneTempString); + addTimeZoneTempString = ""; + } + } + } + } + + void DrawLangKeys(Rect rect, int index, bool isActive, bool isFocused) { + var key = allKeysList[index]; + var value = GetValue(key, out bool isModified); + var keyRect = rect; + keyRect.width = EditorGUIUtility.labelWidth; + keyRect.height = EditorGUIUtility.singleLineHeight; + using (var changed = new EditorGUI.ChangeCheckScope()) { + var newKey = EditorGUI.TextField(keyRect, key); + if (changed.changed) { + if (selectedEntry != null) { + if (selectedEntry.languages.ContainsKey(newKey)) + EditorUtility.DisplayDialog("Error", "Key already exists.", "OK"); + else { + selectedEntry.languages.Remove(key); + selectedEntry.languages.Add(newKey, value); + } + } else + GetOrCreateLanguageEntry().languages.Add(key, value); + OnLangSelect(); + } + } + var valueRect = rect; + valueRect.xMin = keyRect.xMax; + valueRect.height -= EditorGUIUtility.standardVerticalSpacing; + using (var changed = new EditorGUI.ChangeCheckScope()) { + value = EditorGUI.TextArea(valueRect, value, isModified ? wrapBoldTextAreaStyle : wrapTextAreaStyle); + if (changed.changed) { + if (selectedEntry != null) + selectedEntry.languages[key] = value; + else + GetOrCreateLanguageEntry().languages.Add(key, value); + OnLangSelect(); + } + } + } + + float MeasureLangListElementHeight(int index) { + var key = allKeysList[index]; + textContent.text = GetValue(key, out bool isModified); + return (isModified ? wrapBoldTextAreaStyle : wrapTextAreaStyle).CalcHeight(textContent, EditorGUIUtility.currentViewWidth - EditorGUIUtility.labelWidth) + EditorGUIUtility.standardVerticalSpacing; + } + + bool CanRemoveKey(ReorderableList list) => + list.index >= 0 && + list.index < allKeysList.Count && + selectedEntry != null && + selectedEntry.languages.ContainsKey(allKeysList[list.index]); + + void OnRemoveKey(ReorderableList list) { + var key = allKeysList[list.index]; + if (selectedEntry != null && selectedEntry.languages.Remove(key)) { + bool isRemains = false; + foreach (var lang in defaultLanguageMap.Values) + if (lang.languages.ContainsKey(key)) { + isRemains = true; + break; + } + if (!isRemains) + foreach (var lang in currentLanguageMap.Values) + if (lang.languages.ContainsKey(key)) { + isRemains = true; + break; + } + if (!isRemains) allKeys.Remove(key); + } + OnLangSelect(); + } + + string GetValue(string key, out bool isModified) { + if (selectedEntry != null && selectedEntry.languages.TryGetValue(key, out string value)) { + isModified = true; + return value; + } + isModified = false; + if (selectedDefaultEntry != null && selectedDefaultEntry.languages.TryGetValue(key, out value)) + return value; + return ""; + } + } +} \ No newline at end of file diff --git a/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageEditorWindow.cs.meta b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageEditorWindow.cs.meta new file mode 100644 index 0000000..1a333ae --- /dev/null +++ b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8dcdb4666802fa6419e709d9d66e5c7c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerEditor.cs b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerEditor.cs index f363c56..3ce9794 100644 --- a/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerEditor.cs +++ b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerEditor.cs @@ -1,271 +1,55 @@ -using System.Text; -using System.Linq; -using System.Collections.Generic; -using System.Reflection; using UnityEngine; -using UnityEngine.SceneManagement; using UnityEditor; -using UnityEditor.Build; -using UnityEditor.Build.Reporting; -using VRC.Udon; using UdonSharpEditor; using JLChnToZ.VRC.VVMW.Editors; -using VVMW.ThirdParties.LitJson; - -using static UnityEngine.Object; +using UnityEditorInternal; namespace JLChnToZ.VRC.VVMW.I18N.Editors { [CustomEditor(typeof(LanguageManager))] public class LanguageManagerEditor : VVMWEditorBase { static GUIContent textContent; - SerializedProperty languageJsonFiles; - SerializedProperty languageJson; + SerializedProperty languageJsonFiles, languageJson; + LanguageEditorWindow openedWindow; + ReorderableListUtils languageJsonFilesList; + GUIStyle wrappedTextAreaStyle; + bool showJson = false; protected override void OnEnable() { base.OnEnable(); if (textContent == null) textContent = new GUIContent(); + if (wrappedTextAreaStyle == null) + wrappedTextAreaStyle = new GUIStyle(EditorStyles.textArea) { + wordWrap = true + }; languageJsonFiles = serializedObject.FindProperty("languageJsonFiles"); languageJson = serializedObject.FindProperty("languageJson"); + languageJsonFilesList = new ReorderableListUtils(languageJsonFiles); } public override void OnInspectorGUI() { base.OnInspectorGUI(); if (UdonSharpGUI.DrawDefaultUdonSharpBehaviourHeader(target, drawScript: false)) return; - serializedObject.Update(); - EditorGUILayout.PropertyField(languageJsonFiles, true); - EditorGUILayout.PropertyField(languageJson); - serializedObject.ApplyModifiedProperties(); + using (new EditorGUI.DisabledScope(openedWindow != null)) + if (GUILayout.Button("Open Language Editor")) + openedWindow = LanguageEditorWindow.Open(target as LanguageManager); EditorGUILayout.Space(); - EditorGUILayout.HelpBox("Do not add or reference other components or children in this game object, as it will be manupulated on build and may cause unexpected behaviour.", MessageType.Info); - } - } - - // Resolve and group all language managers into one game object while building - public class LanguageManagerUnifier : IProcessSceneWithReport { - LanguageManager unifiedLanguageManager; - UdonBehaviour unifiedLanguageManagerUdon; - HashSet languageManagers = new HashSet(); - Dictionary backingUdonBehaviours = new Dictionary(); - List jsonTexts = new List(); - - public int callbackOrder => 0; - - public void OnProcessScene(Scene scene, BuildReport report) { - var roots = scene.GetRootGameObjects(); - unifiedLanguageManager = null; - languageManagers.Clear(); - jsonTexts.Clear(); - GatherAllLanguages(roots); - CombineJsons(); - RemapLanguageManager(roots); - RemoveLanguageManagers(); - } - - void GatherAllLanguages(GameObject[] roots) { - foreach (var languageManager in roots.SelectMany(x => x.GetComponentsInChildren(true))) { - if (unifiedLanguageManager == null && languageManager.tag != "EditorOnly") { - unifiedLanguageManager = languageManager; - unifiedLanguageManagerUdon = UdonSharpEditorUtility.GetBackingUdonBehaviour(languageManager); - } - languageManagers.Add(languageManager); - using (var so = new SerializedObject(languageManager)) { - var languagePack = so.FindProperty("languageJsonFiles"); - for (int i = 0, count = languagePack.arraySize; i < count; i++) { - var textAsset = languagePack.GetArrayElementAtIndex(i).objectReferenceValue as TextAsset; - if (textAsset != null) jsonTexts.Add(textAsset.text); - } - var additionalJson = so.FindProperty("languageJson"); - if (!string.IsNullOrEmpty(additionalJson.stringValue)) - jsonTexts.Add(additionalJson.stringValue); - } - } - } - - void CombineJsons() { - var langMap = new Dictionary(); - var defaultLanguageMapping = new Dictionary(); - var keyStack = new List(); - var allLanguageKeys = new HashSet(); - foreach (var json in jsonTexts) { - var reader = new JsonReader(json); - keyStack.Clear(); - LanguageEntry currentEntry = null; - while (reader.Read()) - switch (reader.Token) { - case JsonToken.ObjectStart: - switch (keyStack.Count) { - case 1: - if (keyStack[0] is string key && !langMap.TryGetValue(key, out currentEntry)) - langMap[key] = currentEntry = new LanguageEntry(); - break; - } - keyStack.Add(null); - break; - case JsonToken.ArrayStart: - keyStack.Add(0); - break; - case JsonToken.ObjectEnd: - case JsonToken.ArrayEnd: - keyStack.RemoveAt(keyStack.Count - 1); - break; - case JsonToken.PropertyName: - keyStack[keyStack.Count - 1] = reader.Value; - break; - case JsonToken.String: - switch (keyStack.Count) { - case 2: { - if (currentEntry != null && keyStack[1] is string key) { - var strValue = (string)reader.Value; - switch (key) { - case "_name": - currentEntry.name = strValue; - break; - case "_vrcname": - currentEntry.vrcName = strValue; - break; - case "_timezone": - currentEntry.timezones.Add(strValue); - break; - default: - currentEntry.languages[key] = strValue; - allLanguageKeys.Add(key); - if (!defaultLanguageMapping.ContainsKey(key)) - defaultLanguageMapping[key] = strValue; - break; - } - } - break; - } - case 3: { - if (currentEntry != null && keyStack[1] is string key && key == "_timezone" && keyStack[2] is int) - currentEntry.timezones.Add(reader.Value.ToString()); - break; - } - } - goto default; - default: - if (keyStack.Count > 0 && keyStack[keyStack.Count - 1] is int index) - keyStack[keyStack.Count - 1] = index + 1; - break; - } + serializedObject.Update(); + using (var changed = new EditorGUI.ChangeCheckScope()) { + languageJsonFilesList.list.DoLayoutList(); + if (changed.changed && openedWindow != null) openedWindow.RefreshJsonLists(); } - var sb = new StringBuilder(); - var jsonWriter = new JsonWriter(sb); - jsonWriter.WriteObjectStart(); - foreach (var kv in langMap) { - jsonWriter.WritePropertyName(kv.Key); - jsonWriter.WriteObjectStart(); - var lang = kv.Value; - if (!string.IsNullOrEmpty(lang.name)) { - jsonWriter.WritePropertyName("_name"); - jsonWriter.Write(lang.name); - } - if (!string.IsNullOrEmpty(lang.vrcName)) { - jsonWriter.WritePropertyName("_vrcname"); - jsonWriter.Write(lang.vrcName); - } - if (lang.timezones.Count > 0) { - jsonWriter.WritePropertyName("_timezone"); - if (lang.timezones.Count == 1) - jsonWriter.Write(lang.timezones[0]); - else { - jsonWriter.WriteArrayStart(); - foreach (var timezone in lang.timezones) jsonWriter.Write(timezone); - jsonWriter.WriteArrayEnd(); - } - } - foreach (var langEntry in lang.languages) { - jsonWriter.WritePropertyName(langEntry.Key); - jsonWriter.Write(langEntry.Value); + if (openedWindow == null || openedWindow.LanguageManager != target) openedWindow = null; + if (showJson = EditorGUILayout.Foldout(showJson, languageJson.displayName, true)) { + textContent.text = languageJson.stringValue; + var height = wrappedTextAreaStyle.CalcHeight(textContent, EditorGUIUtility.currentViewWidth); + var rect = EditorGUILayout.GetControlRect(false, height); + using (var propScope = new EditorGUI.PropertyScope(rect, textContent, languageJson)) + using (var changeScope = new EditorGUI.ChangeCheckScope()) { + var newJson = EditorGUI.TextArea(rect, languageJson.stringValue, wrappedTextAreaStyle); + if (changeScope.changed) languageJson.stringValue = newJson; } - foreach (var defaultLang in defaultLanguageMapping) - if (!lang.languages.ContainsKey(defaultLang.Key)) { - jsonWriter.WritePropertyName(defaultLang.Key); - jsonWriter.Write(defaultLang.Value); - } - jsonWriter.WriteObjectEnd(); } - jsonWriter.WriteObjectEnd(); - using (var so = new SerializedObject(unifiedLanguageManager)) { - var jsonRefs = so.FindProperty("languageJsonFiles"); - jsonRefs.ClearArray(); - var languageJson = so.FindProperty("languageJson"); - languageJson.stringValue = sb.ToString(); - so.ApplyModifiedPropertiesWithoutUndo(); - } - UdonSharpEditorUtility.CopyProxyToUdon(unifiedLanguageManager); - jsonTexts.Clear(); - } - - void RemapLanguageManager(GameObject[] roots) { - foreach (var ub in roots.SelectMany(x => x.GetComponentsInChildren(true))) { - if (UdonSharpEditorUtility.IsUdonSharpBehaviour(ub)) { - var usharpBehaviour = UdonSharpEditorUtility.GetProxyBehaviour(ub); - bool hasModified = false; - using (var so = new SerializedObject(usharpBehaviour)) { - var iterator = so.GetIterator(); - while (iterator.NextVisible(true)) { - if (iterator.propertyType != SerializedPropertyType.ObjectReference) continue; - if (iterator.objectReferenceValue is UdonBehaviour udon) { - if (udon == unifiedLanguageManagerUdon) continue; - iterator.objectReferenceValue = unifiedLanguageManagerUdon; - hasModified = true; - } else { - if (iterator.objectReferenceValue is LanguageManager languageManager && languageManager != null) { - // We find a language manager reference, further logic appears after the if-else block - } else if (iterator.objectReferenceValue == null) { - // If the reference is null, we first firgure out what is the field type of the property. - var field = usharpBehaviour.GetType().GetField(iterator.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy); - if (field == null || !typeof(LanguageManager).IsAssignableFrom(field.FieldType)) continue; - languageManager = field.GetValue(usharpBehaviour) as LanguageManager; - } else - continue; // Anything else, skip - if (unifiedLanguageManager == languageManager) - continue; - iterator.objectReferenceValue = unifiedLanguageManager; - hasModified = true; - } - } - if (hasModified) so.ApplyModifiedPropertiesWithoutUndo(); - } - if (hasModified) UdonSharpEditorUtility.CopyProxyToUdon(usharpBehaviour); - } else { - var programSource = ub.programSource; - if (programSource == null) continue; - var serializedProgramAsset = programSource.SerializedProgramAsset; - if (serializedProgramAsset == null) continue; - var program = serializedProgramAsset.RetrieveProgram(); - if (program == null) continue; - var symbolTable = program.SymbolTable; - if (symbolTable == null) continue; - foreach (var symbolName in symbolTable.GetSymbols()) { - if (!ub.TryGetProgramVariable(symbolName, out var variable)) continue; - if (!(variable is UdonBehaviour udon)) continue; - if (!backingUdonBehaviours.TryGetValue(udon, out var languageManager)) - continue; - if (languageManager == unifiedLanguageManager) - continue; - ub.SetProgramVariable(symbolName, unifiedLanguageManager); - } - } - } - } - - void RemoveLanguageManagers() { - foreach (var languageManager in languageManagers) { - if (languageManager == unifiedLanguageManager) continue; - var udon = UdonSharpEditorUtility.GetBackingUdonBehaviour(languageManager); - DestroyImmediate(languageManager); - if (udon != null) DestroyImmediate(udon); - } - languageManagers.Clear(); - } - - class LanguageEntry { - public string name; - public string vrcName; - public readonly List timezones = new List(); - public readonly Dictionary languages = new Dictionary(); + serializedObject.ApplyModifiedProperties(); } } } \ No newline at end of file diff --git a/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerUnifier.cs b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerUnifier.cs new file mode 100644 index 0000000..65d500a --- /dev/null +++ b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerUnifier.cs @@ -0,0 +1,271 @@ +using System.Text; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using VRC.Udon; +using UdonSharpEditor; +using VVMW.ThirdParties.LitJson; + +using static UnityEngine.Object; + +namespace JLChnToZ.VRC.VVMW.I18N.Editors { + // Resolve and group all language managers into one game object while building + public class LanguageManagerUnifier : IProcessSceneWithReport { + LanguageManager unifiedLanguageManager; + UdonBehaviour unifiedLanguageManagerUdon; + HashSet languageManagers = new HashSet(); + Dictionary backingUdonBehaviours = new Dictionary(); + List jsonTexts = new List(); + + public int callbackOrder => 0; + + public static Dictionary ParseFromJson( + string json, + List keyStack = null, + Dictionary defaultLanguageMapping = null, + HashSet allLanguageKeys = null, + Dictionary langMap = null + ) { + if (langMap == null) langMap = new Dictionary(); + if (keyStack == null) keyStack = new List(); + else keyStack.Clear(); + var reader = new JsonReader(json); + LanguageEntry currentEntry = null; + while (reader.Read()) + switch (reader.Token) { + case JsonToken.ObjectStart: + switch (keyStack.Count) { + case 1: + if (keyStack[0] is string key && !langMap.TryGetValue(key, out currentEntry)) + langMap[key] = currentEntry = new LanguageEntry(); + break; + } + keyStack.Add(null); + break; + case JsonToken.ArrayStart: + keyStack.Add(0); + break; + case JsonToken.ObjectEnd: + case JsonToken.ArrayEnd: + keyStack.RemoveAt(keyStack.Count - 1); + break; + case JsonToken.PropertyName: + keyStack[keyStack.Count - 1] = reader.Value; + break; + case JsonToken.String: + switch (keyStack.Count) { + case 2: { + if (currentEntry != null && keyStack[1] is string key) { + var strValue = (string)reader.Value; + switch (key) { + case "_name": + currentEntry.name = strValue; + break; + case "_vrclang": + currentEntry.vrcName = strValue; + break; + case "_timezone": + currentEntry.timezones.Add(strValue); + break; + default: + currentEntry.languages[key] = strValue; + allLanguageKeys?.Add(key); + if (defaultLanguageMapping != null && + !defaultLanguageMapping.ContainsKey(key)) + defaultLanguageMapping[key] = strValue; + break; + } + } + break; + } + case 3: { + if (currentEntry != null && keyStack[1] is string key && key == "_timezone" && keyStack[2] is int) + currentEntry.timezones.Add(reader.Value.ToString()); + break; + } + } + goto default; + default: + if (keyStack.Count > 0 && keyStack[keyStack.Count - 1] is int index) + keyStack[keyStack.Count - 1] = index + 1; + break; + } + return langMap; + } + + public static string WriteToJson( + Dictionary langMap, + Dictionary defaultLanguageMapping = null, + bool prettyPrint = false + ) { + var sb = new StringBuilder(); + var jsonWriter = new JsonWriter(sb) { + PrettyPrint = prettyPrint, + }; + jsonWriter.WriteObjectStart(); + foreach (var kv in langMap) { + jsonWriter.WritePropertyName(kv.Key); + jsonWriter.WriteObjectStart(); + var lang = kv.Value; + if (!string.IsNullOrEmpty(lang.name)) { + jsonWriter.WritePropertyName("_name"); + jsonWriter.Write(lang.name); + } + if (!string.IsNullOrEmpty(lang.vrcName)) { + jsonWriter.WritePropertyName("_vrclang"); + jsonWriter.Write(lang.vrcName); + } + if (lang.timezones.Count > 0) { + jsonWriter.WritePropertyName("_timezone"); + if (lang.timezones.Count == 1) + jsonWriter.Write(lang.timezones[0]); + else { + jsonWriter.WriteArrayStart(); + foreach (var timezone in lang.timezones) jsonWriter.Write(timezone); + jsonWriter.WriteArrayEnd(); + } + } + foreach (var langEntry in lang.languages) { + jsonWriter.WritePropertyName(langEntry.Key); + jsonWriter.Write(langEntry.Value); + } + if (defaultLanguageMapping != null) + foreach (var defaultLang in defaultLanguageMapping) + if (!lang.languages.ContainsKey(defaultLang.Key)) { + jsonWriter.WritePropertyName(defaultLang.Key); + jsonWriter.Write(defaultLang.Value); + } + jsonWriter.WriteObjectEnd(); + } + jsonWriter.WriteObjectEnd(); + return sb.ToString(); + } + + public void OnProcessScene(Scene scene, BuildReport report) { + var roots = scene.GetRootGameObjects(); + unifiedLanguageManager = null; + languageManagers.Clear(); + jsonTexts.Clear(); + GatherAllLanguages(roots); + CombineJsons(); + RemapLanguageManager(roots); + RemoveLanguageManagers(); + } + + void GatherAllLanguages(GameObject[] roots) { + foreach (var languageManager in roots.SelectMany(x => x.GetComponentsInChildren(true))) { + if (unifiedLanguageManager == null && languageManager.tag != "EditorOnly") { + unifiedLanguageManager = languageManager; + unifiedLanguageManagerUdon = UdonSharpEditorUtility.GetBackingUdonBehaviour(languageManager); + } + languageManagers.Add(languageManager); + using (var so = new SerializedObject(languageManager)) { + var languagePack = so.FindProperty("languageJsonFiles"); + for (int i = 0, count = languagePack.arraySize; i < count; i++) { + var textAsset = languagePack.GetArrayElementAtIndex(i).objectReferenceValue as TextAsset; + if (textAsset != null) jsonTexts.Add(textAsset.text); + } + var additionalJson = so.FindProperty("languageJson"); + if (!string.IsNullOrEmpty(additionalJson.stringValue)) + jsonTexts.Add(additionalJson.stringValue); + } + } + } + + void CombineJsons() { + var langMap = new Dictionary(); + var defaultLanguageMapping = new Dictionary(); + var keyStack = new List(); + var allLanguageKeys = new HashSet(); + foreach (var json in jsonTexts) + ParseFromJson(json, keyStack, defaultLanguageMapping, allLanguageKeys, langMap); + var combinedJson = WriteToJson(langMap, defaultLanguageMapping); + using (var so = new SerializedObject(unifiedLanguageManager)) { + var jsonRefs = so.FindProperty("languageJsonFiles"); + jsonRefs.ClearArray(); + var languageJson = so.FindProperty("languageJson"); + languageJson.stringValue = combinedJson; + so.ApplyModifiedPropertiesWithoutUndo(); + } + UdonSharpEditorUtility.CopyProxyToUdon(unifiedLanguageManager); + jsonTexts.Clear(); + } + + void RemapLanguageManager(GameObject[] roots) { + foreach (var ub in roots.SelectMany(x => x.GetComponentsInChildren(true))) { + if (UdonSharpEditorUtility.IsUdonSharpBehaviour(ub)) { + var usharpBehaviour = UdonSharpEditorUtility.GetProxyBehaviour(ub); + bool hasModified = false; + using (var so = new SerializedObject(usharpBehaviour)) { + var iterator = so.GetIterator(); + while (iterator.NextVisible(true)) { + if (iterator.propertyType != SerializedPropertyType.ObjectReference) continue; + if (iterator.objectReferenceValue is UdonBehaviour udon) { + if (udon == unifiedLanguageManagerUdon) continue; + iterator.objectReferenceValue = unifiedLanguageManagerUdon; + hasModified = true; + } else { + if (iterator.objectReferenceValue is LanguageManager languageManager && languageManager != null) { + // We find a language manager reference, further logic appears after the if-else block + } else if (iterator.objectReferenceValue == null) { + // If the reference is null, we first firgure out what is the field type of the property. + var field = usharpBehaviour.GetType().GetField(iterator.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + if (field == null || !typeof(LanguageManager).IsAssignableFrom(field.FieldType)) continue; + languageManager = field.GetValue(usharpBehaviour) as LanguageManager; + } else + continue; // Anything else, skip + if (unifiedLanguageManager == languageManager) + continue; + iterator.objectReferenceValue = unifiedLanguageManager; + hasModified = true; + } + } + if (hasModified) so.ApplyModifiedPropertiesWithoutUndo(); + } + if (hasModified) UdonSharpEditorUtility.CopyProxyToUdon(usharpBehaviour); + } else { + var programSource = ub.programSource; + if (programSource == null) continue; + var serializedProgramAsset = programSource.SerializedProgramAsset; + if (serializedProgramAsset == null) continue; + var program = serializedProgramAsset.RetrieveProgram(); + if (program == null) continue; + var symbolTable = program.SymbolTable; + if (symbolTable == null) continue; + foreach (var symbolName in symbolTable.GetSymbols()) { + if (!ub.TryGetProgramVariable(symbolName, out var variable)) continue; + if (!(variable is UdonBehaviour udon)) continue; + if (!backingUdonBehaviours.TryGetValue(udon, out var languageManager)) + continue; + if (languageManager == unifiedLanguageManager) + continue; + ub.SetProgramVariable(symbolName, unifiedLanguageManager); + } + } + } + } + + void RemoveLanguageManagers() { + foreach (var languageManager in languageManagers) { + if (languageManager == unifiedLanguageManager) continue; + var udon = UdonSharpEditorUtility.GetBackingUdonBehaviour(languageManager); + DestroyImmediate(languageManager); + if (udon != null) DestroyImmediate(udon); + } + languageManagers.Clear(); + } + + } + + public class LanguageEntry { + public string name; + public string vrcName; + public readonly List timezones = new List(); + public readonly Dictionary languages = new Dictionary(); + } +} \ No newline at end of file diff --git a/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerUnifier.cs.meta b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerUnifier.cs.meta new file mode 100644 index 0000000..7279775 --- /dev/null +++ b/Packages/idv.jlchntoz.vvmw/Editor/I18N/LanguageManagerUnifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43eeb011e3220364db933973b3a54be0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: