using System; using System.IO; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Reflection; using System.Collections.Generic; using System.Collections; using UnityEngine; using Newtonsoft.Json.Linq; using System.Linq; using Sirenix.OdinInspector; using UnityEngine.Serialization; public class GoogleSheetManager : MonoBehaviour { [FormerlySerializedAs("isAccessGoogleSheet")] [Tooltip("true: google sheet, false: local json")] [SerializeField] private bool _isAccessGoogleSheet = true; [FormerlySerializedAs("googleSheetUrl")] [Tooltip("Google sheet appsscript webapp url")] [SerializeField] private string _googleSheetUrl; [FormerlySerializedAs("availSheets")] [Tooltip("Google sheet avail sheet tabs. seperate `/`. For example `Sheet1/Sheet2`")] [SerializeField] private string _availSheets = "Sheet1/Sheet2"; [FormerlySerializedAs("generateFolderPath")] [Tooltip("For example `/GenerateGoogleSheet`")] [SerializeField] private string _generateFolderPath = "/GenerateGoogleSheet"; [FormerlySerializedAs("googleSheetSO")] [Tooltip("You must approach through `GoogleSheetManager.SO()`"), ReadOnly] public ScriptableObject GoogleSheetSo; [SerializeField, ReadOnly] private string _lastUpdated; private string JsonPath => $"{Application.dataPath}{_generateFolderPath}/GoogleSheetJson.json"; private string ClassPath => $"{Application.dataPath}{_generateFolderPath}/GoogleSheetClass.cs"; private string SoPath => $"Assets{_generateFolderPath}/GoogleSheetSO.asset"; private string[] _availSheetArray; private string _json; private bool _refeshTrigger; private static GoogleSheetManager _instance; public static T So() where T : ScriptableObject { if (GetInstance().GoogleSheetSo == null) { Debug.Log($"googleSheetSO is null"); return null; } return GetInstance().GoogleSheetSo as T; } #if UNITY_EDITOR [ContextMenu("FetchGoogleSheet")] private async void FetchGoogleSheet() { //Init _availSheetArray = _availSheets.Split('/'); if (_isAccessGoogleSheet) { Debug.Log($"Loading from google sheet.."); _json = await LoadDataGoogleSheet(_googleSheetUrl); } else { Debug.Log($"Loading from local json.."); _json = LoadDataLocalJson(); } if (_json == null) { Debug.Log("Json is null."); return; } bool isJsonSaved = SaveFileOrSkip(JsonPath, _json); string allClassCode = GenerateCSharpClass(_json); bool isClassSaved = SaveFileOrSkip(ClassPath, allClassCode); if (isJsonSaved || isClassSaved) { _refeshTrigger = true; UnityEditor.AssetDatabase.Refresh(); } else { CreateGoogleSheetSo(); Debug.Log($"Fetch done."); } _lastUpdated = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); } private async Task LoadDataGoogleSheet(string url) { using (HttpClient client = new HttpClient()) { try { byte[] dataBytes = await client.GetByteArrayAsync(url); return Encoding.UTF8.GetString(dataBytes); } catch (HttpRequestException e) { Debug.LogError($"Request error: {e.Message}"); return null; } } } private string LoadDataLocalJson() { if (File.Exists(JsonPath)) { return File.ReadAllText(JsonPath); } Debug.Log($"File not exist.\n{JsonPath}"); return null; } private bool SaveFileOrSkip(string path, string contents) { string directoryPath = Path.GetDirectoryName(path); if (!Directory.Exists(directoryPath)) { Directory.CreateDirectory(directoryPath); } if (File.Exists(path) && File.ReadAllText(path).Equals(contents)) return false; File.WriteAllText(path, contents); return true; } private bool IsExistAvailSheets(string sheetName) { return Array.Exists(_availSheetArray, x => x == sheetName); } private string GenerateCSharpClass(string jsonInput) { JObject jsonObject = JObject.Parse(jsonInput); StringBuilder classCode = new(); // Scriptable Object classCode.AppendLine("using System;\nusing System.Collections.Generic;\nusing UnityEngine;\n"); classCode.AppendLine("/// You must approach through `GoogleSheetManager.SO()`"); classCode.AppendLine("public class GoogleSheetSO : ScriptableObject\n{"); foreach (var sheet in jsonObject) { string className = sheet.Key; if (!IsExistAvailSheets(className)) continue; classCode.AppendLine($"\tpublic List<{className}> {className}List;"); } classCode.AppendLine("}\n"); // Class foreach (var jObject in jsonObject) { string className = jObject.Key; if (!IsExistAvailSheets(className)) continue; var items = (JArray)jObject.Value; var firstItem = (JObject)items[0]; classCode.AppendLine($"[Serializable]\npublic class {className}\n{{"); // int > float > bool > string int itemIndex = 0; int propertyCount = ((JObject)items[0]).Properties().Count(); string[] propertyTypes = new string[propertyCount]; foreach (JToken item in items) { itemIndex = 0; foreach (var property in ((JObject)item).Properties()) { string propertyType = GetCSharpType(property.Value.Type); string oldPropertyType = propertyTypes[itemIndex]; if (oldPropertyType == null) { propertyTypes[itemIndex] = propertyType; } else if (oldPropertyType == "int") { if (propertyType == "int") propertyTypes[itemIndex] = "int"; else if (propertyType == "float") propertyTypes[itemIndex] = "float"; else if (propertyType == "bool") propertyTypes[itemIndex] = "string"; else if (propertyType == "string") propertyTypes[itemIndex] = "string"; } else if (oldPropertyType == "float") { if (propertyType == "int") propertyTypes[itemIndex] = "float"; else if (propertyType == "float") propertyTypes[itemIndex] = "float"; else if (propertyType == "bool") propertyTypes[itemIndex] = "string"; else if (propertyType == "string") propertyTypes[itemIndex] = "string"; } else if (oldPropertyType == "bool") { if (propertyType == "int") propertyTypes[itemIndex] = "string"; else if (propertyType == "float") propertyTypes[itemIndex] = "string"; else if (propertyType == "bool") propertyTypes[itemIndex] = "bool"; else if (propertyType == "string") propertyTypes[itemIndex] = "string"; } itemIndex++; } } itemIndex = 0; foreach (var property in firstItem.Properties()) { string propertyName = property.Name; string propertyType = propertyTypes[itemIndex]; classCode.AppendLine($"\tpublic {propertyType} {propertyName};"); itemIndex++; } classCode.AppendLine("}\n"); } return classCode.ToString(); } private string GetCSharpType(JTokenType jsonType) { switch (jsonType) { case JTokenType.Integer: return "int"; case JTokenType.Float: return "float"; case JTokenType.Boolean: return "bool"; default: return "string"; } } private bool CreateGoogleSheetSo() { if (Type.GetType("GoogleSheetSO") == null) return false; GoogleSheetSo = ScriptableObject.CreateInstance("GoogleSheetSO"); JObject jsonObject = JObject.Parse(_json); try { foreach (var jObject in jsonObject) { string className = jObject.Key; if (!IsExistAvailSheets(className)) continue; Type classType = Type.GetType(className); Type listType = typeof(List<>).MakeGenericType(classType); IList listInst = (IList)Activator.CreateInstance(listType); var items = (JArray)jObject.Value; foreach (var item in items) { object classInst = Activator.CreateInstance(classType); foreach (var property in ((JObject)item).Properties()) { FieldInfo fieldInfo = classType.GetField(property.Name); object value = Convert.ChangeType(property.Value.ToString(), fieldInfo.FieldType); fieldInfo.SetValue(classInst, value); } listInst.Add(classInst); } GoogleSheetSo.GetType().GetField($"{className}List").SetValue(GoogleSheetSo, listInst); } } catch (Exception e) { Debug.LogError($"CreateGoogleSheetSO error: {e.Message}"); } print("CreateGoogleSheetSO"); UnityEditor.AssetDatabase.CreateAsset(GoogleSheetSo, SoPath); UnityEditor.AssetDatabase.SaveAssets(); return true; } private void OnValidate() { if (_refeshTrigger) { bool isCompleted = CreateGoogleSheetSo(); if (isCompleted) { _refeshTrigger = false; Debug.Log($"Fetch done."); } } } #endif private static GoogleSheetManager GetInstance() { if (_instance == null) { _instance = FindFirstObjectByType(); } return _instance; } }