522 lines
16 KiB
C#
522 lines
16 KiB
C#
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
|
|
using Steamworks;
|
|
#endif
|
|
|
|
public class playerMessagePrefab : MonoBehaviour
|
|
{
|
|
[SerializeField] Image playerProfile;
|
|
[SerializeField] Image playerTitleImage;
|
|
[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)
|
|
{
|
|
Bind(steamId, avatarUrl, displayName, string.Empty, title, content, bubbleColor);
|
|
}
|
|
|
|
public void Bind(string steamId, string avatarUrl, string displayName, string titleId, string title, string content, Color bubbleColor)
|
|
{
|
|
_boundSteamId = steamId ?? string.Empty;
|
|
_boundAvatarUrl = avatarUrl ?? string.Empty;
|
|
|
|
BindPlayerTitle(titleId, 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);
|
|
}
|
|
}
|
|
|
|
private void BindPlayerTitle(string titleId, string title)
|
|
{
|
|
EnsureTitleImageReference();
|
|
|
|
PlayerTitleResolver.ResolvedTitle resolvedTitle = PlayerTitleResolver.Resolve(titleId, title);
|
|
bool hasTitleSprite = resolvedTitle.HasSprite;
|
|
bool hasTitleText = resolvedTitle.HasText;
|
|
|
|
if (playerTitleImage != null)
|
|
{
|
|
playerTitleImage.sprite = resolvedTitle.titleSprite;
|
|
playerTitleImage.enabled = hasTitleSprite;
|
|
playerTitleImage.gameObject.SetActive(hasTitleSprite);
|
|
if (!hasTitleSprite)
|
|
{
|
|
playerTitleImage.sprite = null;
|
|
}
|
|
}
|
|
|
|
if (playerTitle != null)
|
|
{
|
|
playerTitle.text = !hasTitleSprite && hasTitleText ? resolvedTitle.titleText : string.Empty;
|
|
playerTitle.gameObject.SetActive(!hasTitleSprite && hasTitleText);
|
|
}
|
|
}
|
|
|
|
private void EnsureTitleImageReference()
|
|
{
|
|
if (playerTitleImage != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Transform[] children = GetComponentsInChildren<Transform>(true);
|
|
for (int i = 0; i < children.Length; i++)
|
|
{
|
|
Transform child = children[i];
|
|
if (child != null && child.name == "玩家称号image")
|
|
{
|
|
playerTitleImage = child.GetComponent<Image>();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
public static bool TryGetCachedAvatarSprite(string steamId, string avatarUrl, out Sprite sprite)
|
|
{
|
|
sprite = null;
|
|
|
|
string normalizedAvatarUrl = GameServer.Client.NetworkManager.NormalizeAvatarUrlForClient(avatarUrl);
|
|
if (!string.IsNullOrWhiteSpace(normalizedAvatarUrl)
|
|
&& AvatarCache.TryGetValue(normalizedAvatarUrl, out Sprite cachedByUrl)
|
|
&& cachedByUrl != null)
|
|
{
|
|
sprite = cachedByUrl;
|
|
return true;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(steamId)
|
|
&& AvatarCache.TryGetValue(steamId, out Sprite cachedBySteamId)
|
|
&& cachedBySteamId != null)
|
|
{
|
|
sprite = cachedBySteamId;
|
|
return true;
|
|
}
|
|
|
|
Sprite diskSprite = TryLoadAvatarSpriteFromDisk(steamId, normalizedAvatarUrl);
|
|
if (diskSprite != null)
|
|
{
|
|
CacheAvatarSpriteInMemory(steamId, normalizedAvatarUrl, diskSprite);
|
|
sprite = diskSprite;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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 !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
|
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");
|
|
}
|
|
}
|