diff --git a/Convention/[Runtime]/Interaction.cs b/Convention/[Runtime]/Interaction.cs new file mode 100644 index 0000000..e448380 --- /dev/null +++ b/Convention/[Runtime]/Interaction.cs @@ -0,0 +1,679 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; + +namespace Convention +{ + /// + /// 统一的文件交互类,支持本地文件、网络文件和localhost路径的自适应处理 + /// 使用UnityWebRequest实现跨平台兼容,支持WebGL和IL2CPP + /// + /// 是依赖项 + /// + [Serializable] + public sealed class Interaction + { + #region Fields and Properties + + private string originalPath; + private string processedPath; + private PathType pathType; + private object cachedData; + + public string OriginalPath => originalPath; + public string ProcessedPath => processedPath; + public PathType Type => pathType; + + public enum PathType + { + LocalFile, // 本地文件 (file://) + NetworkHTTP, // 网络HTTP (http://) + NetworkHTTPS, // 网络HTTPS (https://) + LocalServer, // 本地服务器 (localhost) + StreamingAssets // StreamingAssets目录 + } + + #endregion + + public Interaction(string path) + { + SetPath(path); + } + + #region Setup + + /// + /// 设置并处理路径 + /// + public Interaction SetPath(string path) + { + originalPath = path; + processedPath = ProcessPath(path); + pathType = DeterminePathType(path); + return this; + } + + /// + /// 处理路径,转换为UnityWebRequest可识别的格式 + /// + private string ProcessPath(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // 网络路径直接返回 + if (path.StartsWith("http://") || path.StartsWith("https://")) + { + return path; + } + + // localhost处理 + if (path.StartsWith("localhost")) + { + return path.StartsWith("localhost/") ? "http://" + path : "http://localhost/" + path; + } + + // StreamingAssets路径处理 + if (path.StartsWith("StreamingAssets/") || path.StartsWith("StreamingAssets\\")) + { + return Path.Combine(Application.streamingAssetsPath, path.Substring(16)).Replace("\\", "/"); + } + + // 本地文件路径处理 + string fullPath = Path.IsPathRooted(path) ? path : Path.GetFullPath(path); + + // WebGL平台特殊处理 + if (Application.platform == RuntimePlatform.WebGLPlayer) + { + // WebGL只能访问StreamingAssets,尝试构建StreamingAssets路径 + return Application.streamingAssetsPath + "/" + Path.GetFileName(fullPath); + } + + // 其他平台使用file://协议 + return "file:///" + fullPath.Replace("\\", "/"); + } + + /// + /// 确定路径类型 + /// + private PathType DeterminePathType(string path) + { + if (string.IsNullOrEmpty(path)) + return PathType.LocalFile; + + if (path.StartsWith("https://")) + return PathType.NetworkHTTPS; + if (path.StartsWith("http://")) + return PathType.NetworkHTTP; + if (path.StartsWith("localhost")) + return PathType.LocalServer; + if (path.StartsWith("StreamingAssets")) + return PathType.StreamingAssets; + + return PathType.LocalFile; + } + + #endregion + + #region Load + + #region LoadAsync + + public IEnumerator LoadAsTextAsync(Action onSuccess, Action onError = null) + { + using UnityWebRequest request = UnityWebRequest.Get(processedPath); + yield return request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.Success) + { + cachedData = request.downloadHandler.text; + onSuccess?.Invoke((string)cachedData); + } + else + { + string errorMsg = $"Failed to load text from {originalPath}: {request.error}"; + onError?.Invoke(errorMsg); + } + } + + public IEnumerator LoadAsBinaryAsync(Action onSuccess, Action onError = null) + { + using UnityWebRequest request = UnityWebRequest.Get(processedPath); + yield return request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.Success) + { + cachedData = request.downloadHandler.data; + onSuccess?.Invoke((byte[])cachedData); + } + else + { + string errorMsg = $"Failed to load binary from {originalPath}: {request.error}"; + onError?.Invoke(errorMsg); + } + } + public IEnumerator LoadAsBinaryAsync(Action progress, Action onSuccess, Action onError = null) + { + using UnityWebRequest request = UnityWebRequest.Get(processedPath); + var result = request.SendWebRequest(); + while (result.isDone == false) + { + progress(result.progress); + yield return null; + } + if (request.result == UnityWebRequest.Result.Success) + { + cachedData = request.downloadHandler.data; + onSuccess?.Invoke((byte[])cachedData); + } + else + { + string errorMsg = $"Failed to load binary from {originalPath}: {request.error}"; + onError?.Invoke(errorMsg); + } + } + + public IEnumerator LoadAsRawJsonAsync(Action onSuccess, Action onError = null) + { + yield return LoadAsTextAsync( + text => + { + try + { + T result = JsonUtility.FromJson(text); + cachedData = result; + onSuccess?.Invoke(result); + } + catch (Exception e) + { + onError?.Invoke($"Failed to parse JSON: {e.Message}"); + } + }, + onError + ); + } + + public IEnumerator LoadAsJsonAsync(Action onSuccess, Action onError = null) + { + yield return LoadAsTextAsync( + text => + { + try + { + using StreamReader reader = new(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(text))); + var jsonReader = new ES3Internal.ES3JSONReader(reader.BaseStream, new(originalPath)); + onSuccess?.Invoke(jsonReader.Read()); + } + catch (Exception e) + { + onError?.Invoke($"Failed to parse JSON: {e.Message}"); + } + }, + onError + ); + } + + public IEnumerator LoadAsImageAsync(Action onSuccess, Action onError = null) + { + using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(processedPath)) + { + yield return request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.Success) + { + Texture2D texture = DownloadHandlerTexture.GetContent(request); + cachedData = texture; + onSuccess?.Invoke(texture); + } + else + { + string errorMsg = $"Failed to load image from {originalPath}: {request.error}"; + onError?.Invoke(errorMsg); + } + } + } + + public IEnumerator LoadAsAudioAsync(Action onSuccess, Action onError = null, AudioType audioType = AudioType.UNKNOWN) + { + if (audioType == AudioType.UNKNOWN) + audioType = GetAudioType(originalPath); + + using (UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(processedPath, audioType)) + { + yield return request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.Success) + { + AudioClip audioClip = DownloadHandlerAudioClip.GetContent(request); + cachedData = audioClip; + onSuccess?.Invoke(audioClip); + } + else + { + string errorMsg = $"Failed to load audio from {originalPath}: {request.error}"; + onError?.Invoke(errorMsg); + } + } + } + + public IEnumerator LoadAsAssetBundle(Action progress, Action onSuccess, Action onError = null) + { + yield return LoadAsBinaryAsync( + progress, + data => + { + try + { + AssetBundle bundle = AssetBundle.LoadFromMemory(data); + if (bundle != null) + { + cachedData = bundle; + onSuccess?.Invoke(bundle); + } + else + { + onError?.Invoke($"Failed to load AssetBundle from data."); + } + } + catch (Exception e) + { + onError?.Invoke($"Failed to load AssetBundle: {e.Message}"); + } + }, + onError + ); + } + + #endregion + + #region Load Sync + + public string LoadAsText() + { + string buffer = null; + bool isEnd = false; + bool IsError = false; + var it = LoadAsTextAsync(x => + { + buffer = x; + isEnd = true; + }, e => + { + IsError = true; + throw new Exception(e); + }); + try + { + while (!isEnd) + { + it.MoveNext(); + } + } + catch + { + if(IsError) + { + throw; + } + } + return buffer; + } + + public byte[] LoadAsBinary() + { + byte[] buffer = null; + bool isEnd = false; + bool IsError = false; + var it = LoadAsBinaryAsync(x => + { + buffer = x; + isEnd = true; + }, e => + { + IsError = true; + throw new Exception(e); + }); + try + { + while (!isEnd) + { + it.MoveNext(); + } + } + catch + { + if (IsError) + { + throw; + } + } + return buffer; + } + + public T LoadAsRawJson() + { + T buffer = default; + bool isEnd = false; + bool IsError = false; + var it = LoadAsRawJsonAsync(x => + { + buffer = x; + isEnd = true; + }, e => + { + IsError = true; + throw new Exception(e); + }); + try + { + while (!isEnd) + { + it.MoveNext(); + } + } + catch + { + if (IsError) + { + throw; + } + } + return buffer; + } + + public T LoadAsJson() + { + T buffer = default; + bool isEnd = false; + bool IsError = false; + var it = LoadAsJsonAsync(x => + { + buffer = x; + isEnd = true; + }, e => + { + IsError = true; + throw new Exception(e); + }); + try + { + while (!isEnd) + { + it.MoveNext(); + } + } + catch + { + if (IsError) + { + throw; + } + } + return buffer; + } + + public Texture2D LoadAsImage() + { + Texture2D buffer = null; + bool isEnd = false; + bool IsError = false; + var it = LoadAsImageAsync(x => + { + buffer = x; + isEnd = true; + }, e => + { + IsError = true; + throw new Exception(e); + }); + try + { + while (!isEnd) + { + it.MoveNext(); + } + } + catch + { + if (IsError) + { + throw; + } + } + return buffer; + } + + public AudioClip LoadAsAudio(AudioType audioType = AudioType.UNKNOWN) + { + AudioClip buffer = null; + bool isEnd = false; + bool IsError = false; + var it = LoadAsAudioAsync(x => + { + buffer = x; + isEnd = true; + }, e => + { + IsError = true; + throw new Exception(e); + }, audioType); + try + { + while (!isEnd) + { + it.MoveNext(); + } + } + catch + { + if (IsError) + { + throw; + } + } + return buffer; + } + + #endregion + + #endregion + + #region Save + + public Interaction SaveAsText(string content) + { + if (Application.platform == RuntimePlatform.WebGLPlayer) + { + throw new NotSupportedException("WebGL平台不支持文件保存。"); + } + + if (pathType != PathType.LocalFile) + { + throw new NotSupportedException("仅支持保存到本地文件路径。"); + } + + new ToolFile(originalPath).SaveAsText(content); + return this; + + } + + public Interaction SaveAsBinary(byte[] data) + { + if (Application.platform == RuntimePlatform.WebGLPlayer) + { + throw new NotSupportedException("WebGL平台不支持文件保存。"); + } + + if (pathType != PathType.LocalFile) + { + throw new NotSupportedException("仅支持保存到本地文件路径。"); + } + + new ToolFile(originalPath).SaveAsBinary(data); + return this; + } + + public Interaction SaveAsRawJson(T obj) + { + new ToolFile(originalPath).SaveAsRawJson(obj); + return this; + } + + public Interaction SaveAsJson(T obj) + { + new ToolFile(originalPath).SaveAsJson(obj); + return this; + } + + #endregion + + #region Tools + + /// + /// 获取本地文件路径 + /// + private string GetLocalPath() + { + if (processedPath.StartsWith("file:///")) + { + return processedPath.Substring(8); + } + return originalPath; + } + + /// + /// 根据文件扩展名获取音频类型 + /// + private AudioType GetAudioType(string path) + { + return BasicAudioSystem.GetAudioType(path); + } + + /// + /// 检查文件是否存在 + /// + public bool Exists() + { + if (pathType == PathType.LocalFile && Application.platform != RuntimePlatform.WebGLPlayer) + { + return File.Exists(GetLocalPath()); + } + else + { + // TODO : 网络文件和WebGL平台需要通过请求检查 + // 当前默认存在 + return true; + } + } + + /// + /// 异步检查文件是否存在 + /// + public IEnumerator ExistsAsync(Action callback) + { + using UnityWebRequest request = UnityWebRequest.Head(processedPath); + yield return request.SendWebRequest(); + callback?.Invoke(request.result == UnityWebRequest.Result.Success); + } + + #endregion + + #region Operator + + public static implicit operator string(Interaction interaction) => interaction?.originalPath; + + public override string ToString() => originalPath; + + /// + /// 路径连接操作符 + /// + public static Interaction operator |(Interaction left, string rightPath) + { + if (left.pathType == PathType.NetworkHTTP || left.pathType == PathType.NetworkHTTPS || left.pathType == PathType.LocalServer) + { + string baseUrl = left.originalPath; + string newUrl = baseUrl.EndsWith("/") ? baseUrl + rightPath : baseUrl + "/" + rightPath; + return new Interaction(newUrl); + } + else + { + string newPath = Path.Combine(left.originalPath, rightPath); + return new Interaction(newPath); + } + } + + #endregion + + #region Tools + + /// + /// 获取文件名 + /// + public string GetFilename() + { + if (pathType == PathType.NetworkHTTP || pathType == PathType.NetworkHTTPS || pathType == PathType.LocalServer) + { + var uri = new Uri(processedPath); + return Path.GetFileName(uri.AbsolutePath); + } + return Path.GetFileName(originalPath); + } + + /// + /// 获取文件扩展名 + /// + public string GetExtension() + { + return Path.GetExtension(GetFilename()); + } + + /// + /// 检查扩展名 + /// + public bool ExtensionIs(params string[] extensions) + { + string ext = GetExtension().ToLower(); + string extWithoutDot = ext.Length > 1 ? ext[1..] : ""; + + foreach (string extension in extensions) + { + string checkExt = extension.ToLower(); + if (ext == checkExt || extWithoutDot == checkExt || ext == "." + checkExt) + return true; + } + return false; + } + + #endregion + + #region Tools + + /// + /// 创建一个新的Interaction实例 + /// + public static Interaction Create(string path) => new(path); + + /// + /// 从StreamingAssets创建 + /// + public static Interaction FromStreamingAssets(string relativePath) + { + return new Interaction("StreamingAssets/" + relativePath); + } + + /// + /// 从本地服务器创建 + /// + public static Interaction FromLocalhost(string path, int port = 80) + { + string url = port == 80 ? $"localhost/{path}" : $"localhost:{port}/{path}"; + return new Interaction(url); + } + + #endregion + } +}