功能批量实现 优化 服务端 新浮动小游戏框架 新界面UI
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.UI;
|
||||
|
||||
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
||||
#define ROOMCHAT_DISABLE_STEAMWORKS
|
||||
#endif
|
||||
|
||||
#if !ROOMCHAT_DISABLE_STEAMWORKS
|
||||
using Steamworks;
|
||||
#endif
|
||||
|
||||
public class playerMessagePrefab : MonoBehaviour
|
||||
{
|
||||
[SerializeField] Image playerProfile;
|
||||
[SerializeField] Text playerTitle;
|
||||
[SerializeField] Text playerName;
|
||||
[SerializeField] Text messageText;
|
||||
[SerializeField] Image messageBtm;
|
||||
|
||||
private static readonly Dictionary<string, Sprite> AvatarCache = new Dictionary<string, Sprite>(StringComparer.Ordinal);
|
||||
private static readonly Dictionary<string, Task<Sprite>> AvatarDownloads = new Dictionary<string, Task<Sprite>>(StringComparer.Ordinal);
|
||||
|
||||
private string _boundSteamId = string.Empty;
|
||||
private string _boundAvatarUrl = string.Empty;
|
||||
|
||||
private static string AvatarCacheFolderPath
|
||||
{
|
||||
get { return Path.Combine(Application.persistentDataPath, "chat_avatar_cache"); }
|
||||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetAvatarRuntimeCaches()
|
||||
{
|
||||
AvatarCache.Clear();
|
||||
AvatarDownloads.Clear();
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(AvatarCacheFolderPath))
|
||||
{
|
||||
Directory.Delete(AvatarCacheFolderPath, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[playerMessagePrefab] Failed to clear avatar cache folder: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind(string steamId, string avatarUrl, string displayName, string title, string content, Color bubbleColor)
|
||||
{
|
||||
_boundSteamId = steamId ?? string.Empty;
|
||||
_boundAvatarUrl = avatarUrl ?? string.Empty;
|
||||
|
||||
if (playerTitle != null)
|
||||
{
|
||||
playerTitle.text = string.IsNullOrWhiteSpace(title) ? string.Empty : title;
|
||||
}
|
||||
|
||||
if (playerName != null)
|
||||
{
|
||||
playerName.text = displayName ?? string.Empty;
|
||||
}
|
||||
|
||||
if (messageText != null)
|
||||
{
|
||||
messageText.text = content ?? string.Empty;
|
||||
}
|
||||
|
||||
if (messageBtm != null)
|
||||
{
|
||||
messageBtm.color = bubbleColor;
|
||||
}
|
||||
|
||||
if (playerProfile != null)
|
||||
{
|
||||
playerProfile.sprite = null;
|
||||
_ = RefreshAvatarAsync(_boundSteamId, _boundAvatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WarmAvatarCacheAsync(string steamId, string avatarUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(steamId) && string.IsNullOrWhiteSpace(avatarUrl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string normalizedAvatarUrl = GameServer.Client.NetworkManager.NormalizeAvatarUrlForClient(avatarUrl);
|
||||
await TryGetAvatarSpriteAsync(steamId, normalizedAvatarUrl);
|
||||
}
|
||||
|
||||
private async Task RefreshAvatarAsync(string steamId, string avatarUrl)
|
||||
{
|
||||
const int maxAttempts = 20;
|
||||
for (int attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
Sprite sprite = await TryGetAvatarSpriteAsync(steamId, avatarUrl);
|
||||
if (playerProfile == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(_boundSteamId, steamId, StringComparison.Ordinal)
|
||||
|| !string.Equals(_boundAvatarUrl, avatarUrl ?? string.Empty, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sprite != null)
|
||||
{
|
||||
playerProfile.sprite = sprite;
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(250);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Sprite> TryGetAvatarSpriteAsync(string steamId, string avatarUrl)
|
||||
{
|
||||
Sprite diskSprite = TryLoadAvatarSpriteFromDisk(steamId, avatarUrl);
|
||||
if (diskSprite != null)
|
||||
{
|
||||
CacheAvatarSpriteInMemory(steamId, avatarUrl, diskSprite);
|
||||
return diskSprite;
|
||||
}
|
||||
|
||||
string avatarSteamId = GameServer.Client.NetworkManager.ParseSteamAvatarUrl(avatarUrl);
|
||||
if (!string.IsNullOrWhiteSpace(avatarSteamId))
|
||||
{
|
||||
return await TryGetSteamAvatarSpriteAsync(avatarSteamId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(avatarUrl))
|
||||
{
|
||||
Sprite remoteSprite = await TryGetRemoteAvatarSprite(avatarUrl);
|
||||
if (remoteSprite != null)
|
||||
{
|
||||
SaveAvatarSpriteToDisk(steamId, avatarUrl, remoteSprite);
|
||||
CacheAvatarSpriteInMemory(steamId, avatarUrl, remoteSprite);
|
||||
return remoteSprite;
|
||||
}
|
||||
}
|
||||
|
||||
return await TryGetSteamAvatarSpriteAsync(steamId);
|
||||
}
|
||||
|
||||
private static async Task<Sprite> TryGetRemoteAvatarSprite(string avatarUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (AvatarCache.TryGetValue(avatarUrl, out Sprite cachedSprite) && cachedSprite != null)
|
||||
{
|
||||
return cachedSprite;
|
||||
}
|
||||
|
||||
Task<Sprite> downloadTask;
|
||||
lock (AvatarDownloads)
|
||||
{
|
||||
if (!AvatarDownloads.TryGetValue(avatarUrl, out downloadTask))
|
||||
{
|
||||
downloadTask = DownloadAvatarSpriteAsync(avatarUrl);
|
||||
AvatarDownloads[avatarUrl] = downloadTask;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Sprite sprite = await downloadTask;
|
||||
if (sprite != null)
|
||||
{
|
||||
CacheAvatarSpriteInMemory(string.Empty, avatarUrl, sprite);
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (AvatarDownloads)
|
||||
{
|
||||
AvatarDownloads.Remove(avatarUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Sprite> DownloadAvatarSpriteAsync(string avatarUrl)
|
||||
{
|
||||
using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(avatarUrl))
|
||||
{
|
||||
request.timeout = 15;
|
||||
await AwaitUnityOperationAsync(request.SendWebRequest());
|
||||
|
||||
if (request.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
Debug.LogWarning($"[playerMessagePrefab] Avatar download failed: url={avatarUrl} error={request.error}");
|
||||
return null;
|
||||
}
|
||||
|
||||
Texture2D texture = DownloadHandlerTexture.GetContent(request);
|
||||
if (texture == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
private static Task AwaitUnityOperationAsync(AsyncOperation operation)
|
||||
{
|
||||
if (operation == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(operation));
|
||||
}
|
||||
|
||||
if (operation.isDone)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
void Complete(AsyncOperation _)
|
||||
{
|
||||
operation.completed -= Complete;
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
operation.completed += Complete;
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static async Task<Sprite> TryGetSteamAvatarSpriteAsync(string steamId)
|
||||
{
|
||||
#if ROOMCHAT_DISABLE_STEAMWORKS
|
||||
return null;
|
||||
#else
|
||||
if (string.IsNullOrWhiteSpace(steamId) || !SteamManager.Initialized)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (AvatarCache.TryGetValue(steamId, out Sprite cached) && cached != null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (!ulong.TryParse(steamId, out ulong parsedSteamId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
CSteamID targetSteamId = new CSteamID(parsedSteamId);
|
||||
SteamFriends.RequestUserInformation(targetSteamId, false);
|
||||
int imageId = -1;
|
||||
const int maxPollFrames = 60;
|
||||
for (int attempt = 0; attempt < maxPollFrames; attempt++)
|
||||
{
|
||||
imageId = SteamFriends.GetLargeFriendAvatar(targetSteamId);
|
||||
if (imageId <= 0)
|
||||
{
|
||||
imageId = SteamFriends.GetMediumFriendAvatar(targetSteamId);
|
||||
}
|
||||
|
||||
if (imageId > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (imageId == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
if (imageId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!SteamUtils.GetImageSize(imageId, out uint width, out uint height) || width == 0 || height == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] imageBuffer = new byte[width * height * 4];
|
||||
if (!SteamUtils.GetImageRGBA(imageId, imageBuffer, imageBuffer.Length))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Texture2D texture = new Texture2D((int)width, (int)height, TextureFormat.RGBA32, false);
|
||||
texture.LoadRawTextureData(imageBuffer);
|
||||
texture.Apply();
|
||||
|
||||
Texture2D flipped = new Texture2D((int)width, (int)height, TextureFormat.RGBA32, false);
|
||||
for (int y = 0; y < (int)height; y++)
|
||||
{
|
||||
Color[] pixels = texture.GetPixels(0, y, (int)width, 1);
|
||||
flipped.SetPixels(0, (int)height - 1 - y, (int)width, 1, pixels);
|
||||
}
|
||||
flipped.Apply();
|
||||
|
||||
Sprite sprite = Sprite.Create(flipped, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f));
|
||||
CacheAvatarSpriteInMemory(steamId, null, sprite);
|
||||
SaveAvatarSpriteToDisk(steamId, null, sprite);
|
||||
return sprite;
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void CacheAvatarSpriteInMemory(string steamId, string avatarUrl, Sprite sprite)
|
||||
{
|
||||
if (sprite == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(avatarUrl))
|
||||
{
|
||||
AvatarCache[avatarUrl] = sprite;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(steamId))
|
||||
{
|
||||
AvatarCache[steamId] = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
private static Sprite TryLoadAvatarSpriteFromDisk(string steamId, string avatarUrl)
|
||||
{
|
||||
string diskPath = GetAvatarDiskCachePath(steamId, avatarUrl);
|
||||
if (string.IsNullOrWhiteSpace(diskPath) || !File.Exists(diskPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
byte[] bytes = File.ReadAllBytes(diskPath);
|
||||
if (bytes == null || bytes.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
||||
if (!texture.LoadImage(bytes, false))
|
||||
{
|
||||
UnityEngine.Object.Destroy(texture);
|
||||
return null;
|
||||
}
|
||||
|
||||
return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[playerMessagePrefab] Failed to load cached avatar: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveAvatarSpriteToDisk(string steamId, string avatarUrl, Sprite sprite)
|
||||
{
|
||||
if (sprite == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string diskPath = GetAvatarDiskCachePath(steamId, avatarUrl);
|
||||
if (string.IsNullOrWhiteSpace(diskPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(AvatarCacheFolderPath);
|
||||
Texture2D readableTexture = ExtractTextureFromSprite(sprite);
|
||||
if (readableTexture == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] bytes = readableTexture.EncodeToPNG();
|
||||
if (bytes == null || bytes.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
File.WriteAllBytes(diskPath, bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[playerMessagePrefab] Failed to save avatar cache: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Texture2D ExtractTextureFromSprite(Sprite sprite)
|
||||
{
|
||||
if (sprite == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Texture2D source = sprite.texture;
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Rect rect = sprite.textureRect;
|
||||
Texture2D texture = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGBA32, false);
|
||||
Color[] pixels = source.GetPixels((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
|
||||
texture.SetPixels(pixels);
|
||||
texture.Apply();
|
||||
return texture;
|
||||
}
|
||||
|
||||
private static string GetAvatarDiskCachePath(string steamId, string avatarUrl)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(steamId))
|
||||
{
|
||||
return Path.Combine(AvatarCacheFolderPath, $"{steamId}.png");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(avatarUrl))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string sanitized = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(avatarUrl))
|
||||
.Replace('/', '_')
|
||||
.Replace('+', '-')
|
||||
.TrimEnd('=');
|
||||
return Path.Combine(AvatarCacheFolderPath, $"{sanitized}.png");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user