using asap.core; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using UnityEngine; using Unity.SharpZipLib.Zip; using System.Net; using UnityEngine.Networking; using System.Threading; namespace cfg { public sealed partial class Tables { #if UNITY_EDITOR public static async System.Threading.Tasks.Task InitEditor(Action progress) { var projectRoot = Directory.GetParent(Application.dataPath); var dir = Path.Combine(projectRoot.Parent.FullName, "cfgData"); var tables = new cfg.Tables( tableName => SimpleJSON.JSON.Parse(File.ReadAllText(Path.Combine(dir, $"{tableName}.json"))) ); await Task.Yield(); progress.Invoke(1f); await Task.Yield(); return tables; } #endif public static async System.Threading.Tasks.Task Init(Action progress) { EnsureDefaultCfgFileExists(out var dir, out var persistCfgFile); var verFilePath = Path.Combine(dir, "conf_ver.txt"); var ver = File.ReadAllText(verFilePath).Trim(); GEvent.SetGlobalContent("conf_ver", ver); Debug.Log($"cfg init ver {ver}"); GContext.container.Resolve().Set(GConstant.K_AssetVersion, ver); // GlobalUtils.AssetVersion = ver; var zipFilePath = Path.Combine(dir, $"{ver}.zip"); #if UNITY_EDITOR UnityEngine.Assertions.Assert.IsTrue(File.Exists(zipFilePath), $"File {zipFilePath} not found"); #endif progress.Invoke(0f); var map = await Task.Run>(() => GetEntryContent(zipFilePath)); progress.Invoke(0.5f); await Task.Yield(); var tables = new cfg.Tables( tableName => SimpleJSON.JSON.Parse(map[tableName]) ); progress.Invoke(1f); return tables; } private static Dictionary GetEntryContent(string zipPath) { var map = new Dictionary(); var memoryStream = new MemoryStream(); try { using (var zip = new ZipFile(zipPath)) { foreach (ZipEntry entry in zip) { if (entry.Name.EndsWith(".json")) { using (var stream = zip.GetInputStream(entry)) { memoryStream.SetLength(0); memoryStream.Position = 0; stream.CopyTo(memoryStream); var content = Encoding.UTF8.GetString(memoryStream.ToArray()); map.Add(Path.GetFileNameWithoutExtension(entry.Name), content); } } } } } catch (Exception e) { Debug.LogError(e); map.Clear(); var defaultConf = BetterStreamingAssets.ReadAllText("cfgData/conf_ver.txt"); zipPath = Path.Combine(Application.persistentDataPath, $"cfgData/{defaultConf}"); using (var zip = new ZipFile(zipPath)) { foreach (ZipEntry entry in zip) { if (entry.Name.EndsWith(".json")) { using (var stream = zip.GetInputStream(entry)) { memoryStream.SetLength(0); memoryStream.Position = 0; stream.CopyTo(memoryStream); var content = Encoding.UTF8.GetString(memoryStream.ToArray()); map.Add(Path.GetFileNameWithoutExtension(entry.Name), content); } } } } } finally { memoryStream.Close(); memoryStream.Dispose(); } return map; } //public static async System.Threading.Tasks.Task Init(Action progress) //{ //var rawDataMap = new Dictionary(); //var opMap = new Dictionary>(); //var props = typeof(Tables).GetProperties(BindingFlags.Public | BindingFlags.Instance); //var addrs = props.Select(_ => _.Name.ToLower()).ToList(); //foreach (var addr in addrs) //{ //var op = Addressables.LoadAssetAsync(addr); //opMap.Add(addr, op); //} //var tasks = opMap.Values.Select(_ => _.Task); //var total = (float)tasks.Count(); //while (true) //{ //var completed = tasks.Count(_ => _.IsCompleted); //if (completed == total) //{ //progress.Invoke(1f); //break; //} //else //{ //progress.Invoke(completed / total); //} //await Awaiters.NextFrame; //} //var tables = new cfg.Tables( //tableName => SimpleJSON.JSON.Parse(opMap[tableName].Result.text) //); //foreach (var kv in opMap) //{ //Addressables.Release(kv.Value); //} //progress.Invoke(1f); //return tables; //} // private static void EnsureDefaultCfgFileExists(out string dir, out string persistCfgFile) { //Copy build-in cfg to persistentDataPath if not exists or version mismatch dir = Path.Combine(Application.persistentDataPath, "cfgData"); persistCfgFile = Path.Combine(dir, "conf_ver.txt"); var builtinVer = BetterStreamingAssets.ReadAllText("cfgData/conf_ver.txt").Trim(); var needCopy = false; if (!Directory.Exists(dir)) { Debug.Log($"create dir {dir}"); Directory.CreateDirectory(dir); needCopy = true; } else if (File.Exists(persistCfgFile)) { var localVer = File.ReadAllText(persistCfgFile).Trim(); if (localVer != builtinVer) { Debug.Log($"cfg version mismatch: local={localVer}, builtin={builtinVer}"); needCopy = true; } } else { needCopy = true; } if (needCopy) { var files = BetterStreamingAssets.GetFiles("cfgData"); foreach (var file in files) { var savePath = Path.Combine(dir, Path.GetFileName(file)); if (File.Exists(savePath)) File.Delete(savePath); Debug.Log($"copy {file} to {savePath}"); File.WriteAllBytes(savePath, BetterStreamingAssets.ReadAllBytes(file)); } } } private static readonly List<(float vtimeout, float ztimeout)> timeoutValues = new() { (1, 3), (2, 5), (2, 5) }; public static async Task UpdateCfg(IConfig config, bool isSuccessLoadRemoteConfigFile) { EnsureDefaultCfgFileExists(out var dir, out var persistCfgFile); if (!isSuccessLoadRemoteConfigFile) { Debug.LogWarning($"[Tables::UpdateCfg] Skipped because remote config not loaded"); return true; } Debug.Log($"[Tables::UpdateCfg] Start updating cfg, local conf_ver={File.ReadAllText(persistCfgFile).Trim()}"); var maxRetryTimes = timeoutValues.Count; for (int retryTime = 0; retryTime < maxRetryTimes; retryTime++) { (var vtimeout, var ztimeout) = timeoutValues[retryTime]; if (await DoUpdateCfgUWR(config, dir, persistCfgFile, (int)MathF.Ceiling(vtimeout), (int)MathF.Ceiling(ztimeout), retryTime, maxRetryTimes)) return true; } return false; } private static async Task DoUpdateCfgUWR( IConfig config, string dir, string persistCfgFile, int verFileTimeoutInSec, int cfgDataTimeoutInSec, int retryTimes, int maxRetryTimes) { var stopwatch = new System.Diagnostics.Stopwatch(); string localVer = File.ReadAllText(persistCfgFile).Trim(); string remoteVer = localVer; var staticResURL = GConstant.V_Static_Res_URL; if (ChannelManager.IsRustoreStore) staticResURL = GConstant.V_Static_Res_URL_RU; var staticResUrl = config.Get(GConstant.K_Static_Res_URL, staticResURL); var remoteVerUrl = $"{staticResUrl}/cfgData/conf_ver.txt"; Debug.Log($"[Tables::DoUpdateCfgUWR] Fetching remote conf_ver from {remoteVerUrl}, localVer={localVer}"); using (var verReq = UnityWebRequest.Get(remoteVerUrl)) { var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(verFileTimeoutInSec)); var ct = cts.Token; verReq.timeout = verFileTimeoutInSec; verReq.SetRequestHeader("Accept", "application/json"); stopwatch.Start(); var op = verReq.SendWebRequest(); try { while (!op.isDone && !ct.IsCancellationRequested) { await Awaiters.NextFrame; } ct.ThrowIfCancellationRequested(); stopwatch.Stop(); if (verReq.result != UnityWebRequest.Result.Success) { Debug.LogWarning($"Load cfg Ver file failed {verReq.result.ToString()} {verReq.error} {retryTimes + 1}/{maxRetryTimes}"); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfgver"); e.AddContent("duration_ms", stopwatch.ElapsedMilliseconds); e.AddContent("reason", verReq.result.ToString()); e.AddContent("skip", retryTimes >= maxRetryTimes - 1); e.AddContent("error", verReq.error); } return false; } remoteVer = verReq.downloadHandler.text.Trim(); Debug.Log($"[Tables::DoUpdateCfgUWR] Remote conf_ver={remoteVer}, localVer={localVer}, match={remoteVer == localVer}"); } catch (OperationCanceledException) { stopwatch.Stop(); verReq.Abort(); Debug.LogWarning($"Load cfg Ver file cancel {verFileTimeoutInSec}s {retryTimes + 1}/{maxRetryTimes}"); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfgver"); e.AddContent("duration_ms", stopwatch.ElapsedMilliseconds); e.AddContent("reason", "cancel"); e.AddContent("skip", retryTimes >= maxRetryTimes - 1); e.AddContent("error", verReq.error ?? "cancel"); } return false; } finally { cts.Dispose(); } } if (remoteVer == localVer) { Debug.Log($"[Tables::DoUpdateCfgUWR] cfg version unchanged ({localVer}), skip download"); return true; } Debug.Log($"[Tables::DoUpdateCfgUWR] cfg version changed {localVer} -> {remoteVer}, downloading..."); var remoteCfgUrl = $"{staticResUrl}/cfgData/{remoteVer}.zip"; var savePath = Path.Combine(dir, $"{remoteVer}.zip"); using (var cfgReq = UnityWebRequest.Get(remoteCfgUrl)) { var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(cfgDataTimeoutInSec)); var ct = cts.Token; cfgReq.timeout = cfgDataTimeoutInSec; cfgReq.downloadHandler = new DownloadHandlerFile(savePath, false); stopwatch.Reset(); stopwatch.Restart(); var op = cfgReq.SendWebRequest(); try { while (!op.isDone && !ct.IsCancellationRequested) { await Awaiters.NextFrame; } ct.ThrowIfCancellationRequested(); stopwatch.Stop(); if (cfgReq.result != UnityWebRequest.Result.Success) { if (File.Exists(savePath)) File.Delete(savePath); Debug.LogWarning($"Load cfg file failed {cfgReq.result.ToString()} {cfgReq.error} {retryTimes + 1}/{maxRetryTimes}"); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfg"); e.AddContent("duration_ms", stopwatch.ElapsedMilliseconds); e.AddContent("reason", cfgReq.result.ToString()); e.AddContent("skip", retryTimes >= maxRetryTimes - 1); e.AddContent("error", cfgReq.error); } return false; } if (File.Exists(persistCfgFile)) File.Delete(persistCfgFile); File.WriteAllText(persistCfgFile, remoteVer); Debug.Log($"cfg is updated from {localVer} to {remoteVer}"); return true; } catch (OperationCanceledException) { cfgReq.Abort(); stopwatch.Stop(); Debug.LogWarning($"Load cfg file cancel {cfgDataTimeoutInSec}s {retryTimes + 1}/{maxRetryTimes}"); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfg"); e.AddContent("duration_ms", stopwatch.ElapsedMilliseconds); e.AddContent("reason", "cancel"); e.AddContent("skip", retryTimes >= maxRetryTimes - 1); e.AddContent("error", cfgReq.error ?? "cancel"); } return false; } finally { cts.Dispose(); } } } public static async Task IsTableChanged(IConfig config) { var dir = Path.Combine(Application.persistentDataPath, "cfgData"); var persistCfgFile = Path.Combine(dir, "conf_ver.txt"); string localVer = File.ReadAllText(persistCfgFile).Trim(); var staticResURL = GConstant.V_Static_Res_URL; if (ChannelManager.IsRustoreStore) staticResURL = GConstant.V_Static_Res_URL_RU; var staticResUrl = config.Get(GConstant.K_Static_Res_URL, staticResURL); var remoteVerUrl = $"{staticResUrl}/cfgData/conf_ver.txt"; using (var verReq = UnityWebRequest.Get(remoteVerUrl)) { var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(5)); var ct = cts.Token; verReq.timeout = 5; var op = verReq.SendWebRequest(); try { while (!op.isDone && !ct.IsCancellationRequested) { await Awaiters.NextFrame; } ct.ThrowIfCancellationRequested(); if (verReq.result != UnityWebRequest.Result.Success) { return false; } var remoteVer = verReq.downloadHandler.text.Trim(); Debug.Log($"[Tables::IsTableChanged] remoteVer: {remoteVer}, localVer: {localVer}"); return remoteVer != localVer; } catch (OperationCanceledException e) { verReq.Abort(); Debug.LogWarning("Load cfg Ver file cancel 5s "); Debug.LogWarning(e); return false; } catch(Exception e) { Debug.LogWarning(e); return false; } } } /** private static async Task DoUpdateCfg( IConfig config, string dir, string persistCfgFile, int verFileTimeoutInSec, int cfgDataTimeoutInSec, int retryTimes) { var stopwatch = new System.Diagnostics.Stopwatch(); var content = string.Empty; try { string localVer = File.ReadAllText(persistCfgFile).Trim(); var staticResUrl = config.Get(GConstant.K_Static_Res_URL, GConstant.V_Static_Res_URL); if (staticResUrl.Equals(GConstant.V_Static_Res_URL)) { staticResUrl = string.Format(GConstant.V_Static_Res_URL, VersionTool.PackVer); } var remoteVerUrl = $"{staticResUrl}/cfgData/conf_ver.txt"; content = "conf_ver"; var handler = new System.Net.Http.HttpClientHandler() { SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls, ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => { return true; } }; using (var httpClient = new HttpClient(handler)) { httpClient.Timeout = TimeSpan.FromSeconds(verFileTimeoutInSec); stopwatch.Start(); var verFileRespon = await httpClient.GetAsync(remoteVerUrl); stopwatch.Stop(); if (verFileRespon.StatusCode != HttpStatusCode.OK) { var elapsed = stopwatch.ElapsedMilliseconds; Debug.LogWarning($"Load cfg Ver file failed {verFileRespon.StatusCode.ToString()}"); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfg"); e.AddContent("duration_ms", elapsed); e.AddContent("reason", verFileRespon.StatusCode.ToString()); e.AddContent("skip", retryTimes >= 2); } return false; } var remoteVer = (await verFileRespon.Content.ReadAsStringAsync()).Trim(); if (remoteVer != localVer) { var remoteCfgUrl = $"{staticResUrl}/cfgData/{remoteVer}.zip"; content = "cfgzip"; httpClient.Timeout = TimeSpan.FromSeconds(cfgDataTimeoutInSec); stopwatch.Reset(); stopwatch.Start(); var remoteCfgRespon = await httpClient.GetAsync(remoteCfgUrl); stopwatch.Stop(); if (remoteCfgRespon.StatusCode != HttpStatusCode.OK) { var elapsed = stopwatch.ElapsedMilliseconds; Debug.LogWarning($"Load cfg zip file failed {remoteCfgRespon.StatusCode.ToString()}"); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfg"); e.AddContent("duration_ms", elapsed); e.AddContent("reason", remoteCfgRespon.StatusCode.ToString()); e.AddContent("skip", retryTimes >= 2); e.AddContent("content", content); } return false; } var savePath = Path.Combine(dir, $"{remoteVer}.zip"); var remoteCfg = await remoteCfgRespon.Content.ReadAsByteArrayAsync(); if (File.Exists(savePath)) File.Delete(savePath); await File.WriteAllBytesAsync(savePath, remoteCfg); if (File.Exists(persistCfgFile)) File.Delete(persistCfgFile); File.WriteAllText(persistCfgFile, remoteVer); Debug.Log($"cfg is updated from {localVer} to {remoteVer}"); } } return true; } catch (System.Net.Http.HttpRequestException httpEx) { if (stopwatch.IsRunning) stopwatch.Stop(); var elapsed = stopwatch.ElapsedMilliseconds; var errorMessage = $"HTTP request failed: {httpEx.Message}"; if (httpEx.InnerException != null) { errorMessage += $" Inner: {httpEx.InnerException.Message}"; if (httpEx.InnerException.InnerException != null) { errorMessage += $" InnerInner: {httpEx.InnerException.InnerException.Message}"; } } Debug.LogWarning(errorMessage); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfg"); e.AddContent("duration_ms", elapsed); e.AddContent("reason", "httpEx"); e.AddContent("error", httpEx.Message); e.AddContent("skip", retryTimes >= 2); e.AddContent("content", content); } return false; } catch (System.Threading.Tasks.TaskCanceledException timeoutEx) { if (stopwatch.IsRunning) stopwatch.Stop(); var elapsed = stopwatch.ElapsedMilliseconds; Debug.LogWarning($"Request timeout after {verFileTimeoutInSec}s: {timeoutEx.Message}"); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfg"); e.AddContent("duration_ms", elapsed); e.AddContent("reason", "timeout"); e.AddContent("error", timeoutEx.Message); e.AddContent("skip", retryTimes >= 2); e.AddContent("content", content); } return false; } catch (System.IO.IOException ioEx) { if (stopwatch.IsRunning) stopwatch.Stop(); var elapsed = stopwatch.ElapsedMilliseconds; Debug.LogWarning($"IO operation failed: {ioEx.Message}"); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfg"); e.AddContent("duration_ms", elapsed); e.AddContent("reason", "io_exception"); e.AddContent("error", ioEx.Message); e.AddContent("skip", retryTimes >= 2); e.AddContent("content", content); } return false; } catch (Exception exception) { if (stopwatch.IsRunning) stopwatch.Stop(); var elapsed = stopwatch.ElapsedMilliseconds; var errorMessage = $"Unexpected error loading cfg: {exception.Message}"; if (exception.InnerException != null) { errorMessage += $" Inner: {exception.InnerException.Message}"; } Debug.LogError(errorMessage); using (var e = GEvent.TackEvent("retry")) { e.AddContent("type", "cfg"); e.AddContent("duration_ms", elapsed); e.AddContent("reason", "unexpected_error"); e.AddContent("error", exception.Message); e.AddContent("skip", retryTimes >= 2); e.AddContent("content", content); } return false; } } **/ //private async Task ExtractCfg(string ver) //{ //var zipFilePath = Path.Combine(Application.persistentDataPath, $"{ver}.zip"); //var saveDir = Path.Combine(Application.persistentDataPath, "cfgData"); //if (Directory.Exists(saveDir)) //{ //Directory.Delete(saveDir, true); //} //await Task.Run(() => ZipFile.ExtractToDirectory(zipFilePath, Application.persistentDataPath)); //File.Delete(zipFilePath); //} } public static class TablesExtensions { public static cfg.MapData GetMapData(this Tables tables, int id) { return tables.TbMapData[id]; } public static cfg.FishData GetFishData(this Tables tables, int id) { var fishData = tables.TbFishData; if (!fishData.DataMap.ContainsKey(id)) { return fishData.DataList[0]; } return fishData[id]; } public static cfg.RodData GetRodData(this Tables tables, int id) { var fishRodData = tables.TbRodData; if (!fishRodData.DataMap.ContainsKey(id)) { return fishRodData.DataList[0]; } return fishRodData[id]; } public static cfg.AlbumExchange GetAlbumExchange(this Tables tables, int id) { return tables.TbAlbumExchange.GetOrDefault(id); } public static cfg.Item GetItemData(this Tables tables, int id) { return tables.TbItem[id]; } public static string GetItemIconName(this Tables tables, int id) { var item = tables.TbItem; if (!item.DataMap.ContainsKey(id)) { return ""; } return item[id].Icon; } } public sealed partial class Text { public string GetText(string code) { string text = json["text_" + code]; return string.IsNullOrEmpty(text) ? TextEn : text; } } public sealed partial class ShopPackManager { public int MaxPurchaseNew { get { if (MaxPurchase == 0) { return 1; } return MaxPurchase; } } } }