first commit
This commit is contained in:
353
Assets/Scripts/Utils/HttpUtil.cs
Normal file
353
Assets/Scripts/Utils/HttpUtil.cs
Normal file
@@ -0,0 +1,353 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
/// <summary>
|
||||
/// UnityWebRequest GET/POST 工具类:
|
||||
/// - GET(query 参数)
|
||||
/// - POST JSON
|
||||
/// - POST 表单(x-www-form-urlencoded / multipart)
|
||||
/// - 上传文件(multipart)
|
||||
/// - 自定义 Header
|
||||
/// - 超时、取消
|
||||
/// - 统一返回文本 / 原始字节
|
||||
/// </summary>
|
||||
public static class HttpUtil
|
||||
{
|
||||
// ========== 公共返回结构 ==========
|
||||
public readonly struct HttpResult
|
||||
{
|
||||
public readonly bool ok;
|
||||
public readonly long statusCode;
|
||||
public readonly string text;
|
||||
public readonly byte[] data;
|
||||
public readonly string error; // 包含网络错误/HTTP错误/取消/超时等信息
|
||||
public readonly Dictionary<string, string> responseHeaders;
|
||||
|
||||
public HttpResult(bool ok, long statusCode, string text, byte[] data, string error,
|
||||
Dictionary<string, string> responseHeaders)
|
||||
{
|
||||
this.ok = ok;
|
||||
this.statusCode = statusCode;
|
||||
this.text = text;
|
||||
this.data = data;
|
||||
this.error = error;
|
||||
this.responseHeaders = responseHeaders;
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"ok={ok}, status={statusCode}, error={error}, textLen={text?.Length ?? 0}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于取消请求(调用 Cancel() 会中止 UnityWebRequest)
|
||||
/// </summary>
|
||||
public sealed class RequestHandle
|
||||
{
|
||||
internal UnityWebRequest req;
|
||||
public bool IsDone => req == null || req.isDone;
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
try
|
||||
{
|
||||
req?.Abort();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 对外 API(协程) ==========
|
||||
|
||||
public static IEnumerator Get(
|
||||
string url,
|
||||
Dictionary<string, string> query = null,
|
||||
Dictionary<string, string> headers = null,
|
||||
int timeoutSeconds = 15,
|
||||
Action<HttpResult> callback = null,
|
||||
RequestHandle handle = null)
|
||||
{
|
||||
var finalUrl = BuildUrlWithQuery(url, query);
|
||||
|
||||
using (var req = UnityWebRequest.Get(finalUrl))
|
||||
{
|
||||
ApplyCommon(req, headers, timeoutSeconds, handle);
|
||||
|
||||
yield return req.SendWebRequest();
|
||||
|
||||
callback?.Invoke(BuildResult(req));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST JSON(Content-Type: application/json)
|
||||
/// </summary>
|
||||
public static IEnumerator PostJson(
|
||||
string url,
|
||||
string jsonBody,
|
||||
Dictionary<string, string> headers = null,
|
||||
int timeoutSeconds = 15,
|
||||
Action<HttpResult> callback = null,
|
||||
RequestHandle handle = null)
|
||||
{
|
||||
var bodyBytes = string.IsNullOrEmpty(jsonBody) ? Array.Empty<byte>() : Encoding.UTF8.GetBytes(jsonBody);
|
||||
|
||||
using (var req = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
|
||||
{
|
||||
req.uploadHandler = new UploadHandlerRaw(bodyBytes);
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
req.SetRequestHeader("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
ApplyCommon(req, headers, timeoutSeconds, handle);
|
||||
|
||||
yield return req.SendWebRequest();
|
||||
|
||||
callback?.Invoke(BuildResult(req));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST 表单(application/x-www-form-urlencoded)
|
||||
/// </summary>
|
||||
public static IEnumerator PostFormUrlEncoded(
|
||||
string url,
|
||||
Dictionary<string, string> form,
|
||||
Dictionary<string, string> headers = null,
|
||||
int timeoutSeconds = 15,
|
||||
Action<HttpResult> callback = null,
|
||||
RequestHandle handle = null)
|
||||
{
|
||||
// Unity 内部会自动按 x-www-form-urlencoded 处理字段
|
||||
var formData = new List<IMultipartFormSection>();
|
||||
if (form != null)
|
||||
{
|
||||
foreach (var kv in form)
|
||||
formData.Add(new MultipartFormDataSection(kv.Key, kv.Value));
|
||||
}
|
||||
|
||||
// 这里用 Post + 手动覆盖 content-type
|
||||
using (var req = UnityWebRequest.Post(url, formData))
|
||||
{
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
req.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
|
||||
|
||||
ApplyCommon(req, headers, timeoutSeconds, handle);
|
||||
|
||||
yield return req.SendWebRequest();
|
||||
|
||||
callback?.Invoke(BuildResult(req));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST multipart(可用于普通表单 + 文件上传)
|
||||
/// files: (fieldName, fileName, bytes, mimeType)
|
||||
/// </summary>
|
||||
public static IEnumerator PostMultipart(
|
||||
string url,
|
||||
Dictionary<string, string> fields,
|
||||
List<(string fieldName, string fileName, byte[] bytes, string mimeType)> files,
|
||||
Dictionary<string, string> headers = null,
|
||||
int timeoutSeconds = 30,
|
||||
Action<HttpResult> callback = null,
|
||||
RequestHandle handle = null)
|
||||
{
|
||||
var form = new List<IMultipartFormSection>();
|
||||
|
||||
if (fields != null)
|
||||
{
|
||||
foreach (var kv in fields)
|
||||
form.Add(new MultipartFormDataSection(kv.Key, kv.Value));
|
||||
}
|
||||
|
||||
if (files != null)
|
||||
{
|
||||
foreach (var f in files)
|
||||
{
|
||||
var mime = string.IsNullOrEmpty(f.mimeType) ? "application/octet-stream" : f.mimeType;
|
||||
form.Add(new MultipartFormFileSection(f.fieldName, f.bytes, f.fileName, mime));
|
||||
}
|
||||
}
|
||||
|
||||
using (var req = UnityWebRequest.Post(url, form))
|
||||
{
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
ApplyCommon(req, headers, timeoutSeconds, handle);
|
||||
|
||||
yield return req.SendWebRequest();
|
||||
|
||||
callback?.Invoke(BuildResult(req));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载二进制(比如图片、ab、配置包等)
|
||||
/// </summary>
|
||||
public static IEnumerator GetBytes(
|
||||
string url,
|
||||
Dictionary<string, string> query = null,
|
||||
Dictionary<string, string> headers = null,
|
||||
int timeoutSeconds = 30,
|
||||
Action<HttpResult> callback = null,
|
||||
RequestHandle handle = null)
|
||||
{
|
||||
var finalUrl = BuildUrlWithQuery(url, query);
|
||||
|
||||
using (var req = UnityWebRequest.Get(finalUrl))
|
||||
{
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
ApplyCommon(req, headers, timeoutSeconds, handle);
|
||||
|
||||
yield return req.SendWebRequest();
|
||||
|
||||
callback?.Invoke(BuildResult(req, preferBytes: true));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 便捷:返回 JSON 反序列化(JsonUtility) ==========
|
||||
// 注意:JsonUtility 不支持 Dictionary / 顶层数组等,你若需要更强建议用 Newtonsoft.Json
|
||||
public static IEnumerator GetJson<T>(
|
||||
string url,
|
||||
Dictionary<string, string> query = null,
|
||||
Dictionary<string, string> headers = null,
|
||||
int timeoutSeconds = 15,
|
||||
Action<bool, T, HttpResult> callback = null,
|
||||
RequestHandle handle = null)
|
||||
{
|
||||
yield return Get(url, query, headers, timeoutSeconds, r =>
|
||||
{
|
||||
if (!r.ok)
|
||||
{
|
||||
callback?.Invoke(false, default, r);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var obj = JsonUtility.FromJson<T>(r.text);
|
||||
callback?.Invoke(true, obj, r);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
callback?.Invoke(false, default, new HttpResult(false, r.statusCode, r.text, r.data,
|
||||
"JSON parse error: " + e.Message, r.responseHeaders));
|
||||
}
|
||||
}, handle);
|
||||
}
|
||||
|
||||
// ========== 内部工具 ==========
|
||||
static void ApplyCommon(UnityWebRequest req, Dictionary<string, string> headers, int timeoutSeconds,
|
||||
RequestHandle handle)
|
||||
{
|
||||
req.timeout = Mathf.Max(1, timeoutSeconds);
|
||||
req.downloadHandler ??= new DownloadHandlerBuffer();
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var kv in headers)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kv.Key))
|
||||
req.SetRequestHeader(kv.Key, kv.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (handle != null) handle.req = req;
|
||||
}
|
||||
|
||||
static HttpResult BuildResult(UnityWebRequest req, bool preferBytes = false)
|
||||
{
|
||||
// Unity 2020+ : result enum
|
||||
bool isNetworkOrHttpError =
|
||||
req.result == UnityWebRequest.Result.ConnectionError ||
|
||||
req.result == UnityWebRequest.Result.ProtocolError ||
|
||||
req.result == UnityWebRequest.Result.DataProcessingError;
|
||||
|
||||
var code = req.responseCode;
|
||||
var headers = req.GetResponseHeaders();
|
||||
|
||||
// 下载内容
|
||||
byte[] data = null;
|
||||
string text = null;
|
||||
|
||||
if (req.downloadHandler != null)
|
||||
{
|
||||
data = req.downloadHandler.data;
|
||||
|
||||
// preferBytes==false 时优先给 text
|
||||
if (!preferBytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
text = req.downloadHandler.text;
|
||||
}
|
||||
catch
|
||||
{
|
||||
text = data != null ? Encoding.UTF8.GetString(data) : null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// preferBytes==true 时 text 也尽量给一份(方便日志)
|
||||
try
|
||||
{
|
||||
text = req.downloadHandler.text;
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 错误信息(包含 HTTP 错误)
|
||||
string err = null;
|
||||
if (isNetworkOrHttpError)
|
||||
{
|
||||
err = req.error;
|
||||
// 有些后端会把错误细节放在 body
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
err = $"{err}\nBody: {TrimForLog(text, 1200)}";
|
||||
}
|
||||
|
||||
bool ok = !isNetworkOrHttpError && code >= 200 && code < 300;
|
||||
|
||||
return new HttpResult(ok, code, text, data, err, headers);
|
||||
}
|
||||
|
||||
static string BuildUrlWithQuery(string url, Dictionary<string, string> query)
|
||||
{
|
||||
if (query == null || query.Count == 0) return url;
|
||||
|
||||
var sb = new StringBuilder(url);
|
||||
sb.Append(url.Contains("?") ? "&" : "?");
|
||||
|
||||
bool first = true;
|
||||
foreach (var kv in query)
|
||||
{
|
||||
if (!first) sb.Append('&');
|
||||
first = false;
|
||||
|
||||
sb.Append(UnityWebRequest.EscapeURL(kv.Key));
|
||||
sb.Append('=');
|
||||
sb.Append(UnityWebRequest.EscapeURL(kv.Value ?? ""));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static string TrimForLog(string s, int maxLen)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return s;
|
||||
if (s.Length <= maxLen) return s;
|
||||
return s.Substring(0, maxLen) + "...(truncated)";
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Utils/HttpUtil.cs.meta
Normal file
3
Assets/Scripts/Utils/HttpUtil.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bb37521163d4f2eb23161f664448ffa
|
||||
timeCreated: 1770561838
|
||||
48
Assets/Scripts/Utils/PlatformInfo.cs
Normal file
48
Assets/Scripts/Utils/PlatformInfo.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
using SystemInfo = UnityEngine.Device.SystemInfo;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public static class PlatformInfo
|
||||
{
|
||||
public static string GetAndroidID()
|
||||
{
|
||||
string androidId = string.Empty;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (string.IsNullOrEmpty(androidId))
|
||||
{
|
||||
androidId = SystemInfo.deviceUniqueIdentifier;
|
||||
}
|
||||
#elif UNITY_ANDROID
|
||||
// 只在 Android 平台上执行此操作
|
||||
if (Application.platform == RuntimePlatform.Android)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
|
||||
{
|
||||
AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
|
||||
AndroidJavaObject contentResolver =
|
||||
currentActivity.Call<AndroidJavaObject>("getContentResolver");
|
||||
AndroidJavaClass secureSettings = new AndroidJavaClass("android.provider.Settings$Secure");
|
||||
androidId = secureSettings.CallStatic<string>("getString", contentResolver, "android_id");
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError("Error while fetching Android ID: " + e.Message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Not running on Android platform.");
|
||||
}
|
||||
#elif UNITY_IOS
|
||||
androidId = SystemInfo.deviceUniqueIdentifier;
|
||||
#endif
|
||||
Debug.LogWarning($"androidId={androidId}");
|
||||
return androidId;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Utils/PlatformInfo.cs.meta
Normal file
3
Assets/Scripts/Utils/PlatformInfo.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7f9f405912d4bb18ccf8686cc44d59e
|
||||
timeCreated: 1770564467
|
||||
91
Assets/Scripts/Utils/XGLoader.cs
Normal file
91
Assets/Scripts/Utils/XGLoader.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using FairyGUI;
|
||||
using NBC;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace NBF
|
||||
{
|
||||
public class XGLoader : GLoader
|
||||
{
|
||||
private static Dictionary<string, Texture2D> _loaderInfo = new Dictionary<string, Texture2D>();
|
||||
|
||||
private Texture _texture;
|
||||
|
||||
protected override void LoadExternal()
|
||||
{
|
||||
if (LoadType() == 1)
|
||||
{
|
||||
LoadExternalSync(url);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info($"LoadExternal={url}");
|
||||
_texture = Resources.Load<Texture>(url);
|
||||
if (_texture != null)
|
||||
onExternalLoadSuccess(new NTexture(_texture));
|
||||
else
|
||||
onExternalLoadFailed();
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable Unity.PerformanceAnalysis
|
||||
protected override void FreeExternal(NTexture tex)
|
||||
{
|
||||
if (LoadType() == 1)
|
||||
{
|
||||
tex.Unload();
|
||||
tex.Dispose();
|
||||
_loaderInfo.Remove(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tex.nativeTexture != null)
|
||||
{
|
||||
// Resources.UnloadAsset(_texture);
|
||||
}
|
||||
}
|
||||
|
||||
private int LoadType()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(url) && url.StartsWith("http"))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void LoadExternalSync(string downloadUrl)
|
||||
{
|
||||
Game.Instance.StartCoroutine(DownloadTexture(downloadUrl));
|
||||
}
|
||||
|
||||
private IEnumerator DownloadTexture(string downloadUrl)
|
||||
{
|
||||
var www = UnityWebRequestTexture.GetTexture(downloadUrl);
|
||||
yield return www.SendWebRequest();
|
||||
if (isDisposed) yield break;
|
||||
try
|
||||
{
|
||||
var texture2D = DownloadHandlerTexture.GetContent(www);
|
||||
if (!_loaderInfo.TryAdd(downloadUrl, texture2D))
|
||||
{
|
||||
_loaderInfo[downloadUrl] = texture2D;
|
||||
}
|
||||
|
||||
if (visible && url.Equals(downloadUrl))
|
||||
{
|
||||
onExternalLoadSuccess(new NTexture(texture2D));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError(e);
|
||||
onExternalLoadFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Utils/XGLoader.cs.meta
Normal file
3
Assets/Scripts/Utils/XGLoader.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e5b0f60806f488d8d0560dc0d6892e2
|
||||
timeCreated: 1770453464
|
||||
Reference in New Issue
Block a user