mirror of
https://git.aaro.dev/VRCBoard/vrcboard-udon.git
synced 2026-03-16 23:29:48 +00:00
All checks were successful
Create Unity Package / package (push) Successful in 8s
1044 lines
40 KiB
C#
1044 lines
40 KiB
C#
using System;
|
||
using JetBrains.Annotations;
|
||
using UdonSharp;
|
||
using UnityEngine;
|
||
using UnityEngine.Serialization;
|
||
using VRC.SDK3.Data;
|
||
using VRC.SDK3.Image;
|
||
using VRC.SDK3.StringLoading;
|
||
using VRC.SDKBase;
|
||
using VRC.Udon.Common.Interfaces;
|
||
using VRCBoard;
|
||
using VRCBoard.Components;
|
||
using Object = UnityEngine.Object;
|
||
|
||
|
||
using UnityEditor;
|
||
|
||
|
||
namespace VRCBoard
|
||
{
|
||
|
||
public enum IDType
|
||
{
|
||
Tier,
|
||
Tag,
|
||
Image
|
||
}
|
||
|
||
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
|
||
public class VRCBoardManager : UdonSharpBehaviour
|
||
{
|
||
public string GIT_USERNAME = "";
|
||
public string GIT_REPO = "";
|
||
public string GIT_BASE_URL => "https://raw.githubusercontent.com/" + GIT_USERNAME + "/" + GIT_REPO + "/refs/heads/main/";
|
||
|
||
public bool debugMode = true;
|
||
|
||
public string vrcBoardDomain = "mydomain.vrcboard.app";
|
||
public float periodicUpdateInterval = 60f;
|
||
public string VrcBoardBaseUrl => "https://" + vrcBoardDomain + "/";
|
||
public int atlasUrlCount = 100;
|
||
|
||
public VRCBoardBaseComponent[] vrcBoardComponents;
|
||
public byte waitIndex = 0;
|
||
|
||
private bool _useProxyUrls = false;
|
||
|
||
#region URLS
|
||
|
||
public VRCUrl[] _sourceAtlasUrls;
|
||
public VRCUrl[] _proxyAtlasUrls;
|
||
|
||
public VRCUrl _sourceAtlasInfoUrl;
|
||
public VRCUrl _proxyAtlasInfoUrl;
|
||
|
||
public VRCUrl[] _sourceCustomModuleUrls;
|
||
public VRCUrl[] _proxyCustomModuleUrls;
|
||
|
||
public VRCUrl _sourceSupporterInfoUrl;
|
||
public VRCUrl _proxySupporterInfoUrl;
|
||
|
||
public VRCUrl[] customModuleUrls => _useProxyUrls ? _proxyCustomModuleUrls : _sourceCustomModuleUrls;
|
||
public VRCUrl supporterInfoUrl => _useProxyUrls ? _proxySupporterInfoUrl : _sourceSupporterInfoUrl;
|
||
public VRCUrl atlasInfoUrl => _useProxyUrls ? _proxyAtlasInfoUrl : _sourceAtlasInfoUrl;
|
||
public VRCUrl[] atlasUrls => _useProxyUrls ? _proxyAtlasUrls : _sourceAtlasUrls;
|
||
|
||
#endregion
|
||
|
||
|
||
private VRCImageDownloader[] _imageDownloaders;
|
||
private Texture2D[] _downloadedAtlasTextures;
|
||
private float[] _atlasDownloadTimes;
|
||
|
||
|
||
public bool[] customModuleEnabled;
|
||
|
||
|
||
private DataDictionary _atlasInfo;
|
||
|
||
|
||
private DataDictionary _supporterInfo;
|
||
private DataDictionary _supporterInfoVrcLookup;
|
||
private DataList _supporterList;
|
||
private DataList _perkNodes;
|
||
|
||
|
||
private string _instanceOwnerOverride = "";
|
||
|
||
private string instanceOwnerOverride
|
||
{
|
||
get => _instanceOwnerOverride;
|
||
set
|
||
{
|
||
_instanceOwnerOverride = value;
|
||
RequestSerialization();
|
||
Debug.Log("Instance owner override set to " + value);
|
||
}
|
||
}
|
||
|
||
[UdonSynced, FieldChangeCallback(nameof(InstanceOwner))]
|
||
private string _instanceOwner;
|
||
|
||
public string[] imageIdDownloadQueue = new string[0];
|
||
public bool automaticInstanceOwner = true;
|
||
|
||
[PublicAPI]
|
||
public void SetInstanceOwner(string owner)
|
||
{
|
||
instanceOwnerOverride = owner;
|
||
}
|
||
|
||
public string InstanceOwner
|
||
{
|
||
get {
|
||
if (!string.IsNullOrWhiteSpace(instanceOwnerOverride)) return instanceOwnerOverride;
|
||
if (automaticInstanceOwner)
|
||
{
|
||
return _instanceOwner;
|
||
}
|
||
return String.Empty;
|
||
}
|
||
private set
|
||
{
|
||
_instanceOwner = value;
|
||
}
|
||
}
|
||
|
||
public string _VRCBoardIdFromName(string name)
|
||
{
|
||
DataDictionary ownerInfo = GetSupporterDataFromPlayer(name);
|
||
if (ownerInfo == null)
|
||
{
|
||
Debug.LogWarning("Failed to get user info, user has likely not linked their account. username: " + name);
|
||
return "";
|
||
}
|
||
bool ownerSuccess = ownerInfo.TryGetValue("id", out DataToken ownerToken);
|
||
if (!ownerSuccess || ownerToken.TokenType != TokenType.String)
|
||
{
|
||
Debug.LogWarning("Failed to get vrcboard ID from displayname");
|
||
return "";
|
||
}
|
||
return ownerToken.String;
|
||
}
|
||
|
||
private void RemoveNullComponents()
|
||
{
|
||
int nullCount = 0;
|
||
for (int i = 0; i < vrcBoardComponents.Length; i++)
|
||
{
|
||
if (vrcBoardComponents[i] == null)
|
||
{
|
||
nullCount++;
|
||
}
|
||
}
|
||
|
||
if (nullCount == 0) return;
|
||
VRCBoardBaseComponent[] newComponents = new VRCBoardBaseComponent[vrcBoardComponents.Length - nullCount];
|
||
int newIndex = 0;
|
||
for (int i = 0; i < vrcBoardComponents.Length; i++)
|
||
{
|
||
if (vrcBoardComponents[i] == null) continue;
|
||
newComponents[newIndex] = vrcBoardComponents[i];
|
||
newIndex++;
|
||
}
|
||
|
||
vrcBoardComponents = newComponents;
|
||
}
|
||
private void Start()
|
||
{
|
||
int urlCount = atlasUrls.Length;
|
||
_imageDownloaders = new VRCImageDownloader[urlCount];
|
||
_downloadedAtlasTextures = new Texture2D[urlCount];
|
||
_atlasDownloadTimes = new float[urlCount];
|
||
for (int i = 0; i < atlasUrls.Length; i++)
|
||
{
|
||
_imageDownloaders[i] = new VRCImageDownloader();
|
||
}
|
||
|
||
RemoveNullComponents();
|
||
|
||
foreach (var component in vrcBoardComponents)
|
||
component._Register(this);
|
||
_PeriodicUpdate();
|
||
_TaskLoop();
|
||
Debug.Log("VRCBoardManager started");
|
||
}
|
||
|
||
public void _PeriodicUpdate()
|
||
{
|
||
//Debug.Log("Periodic update");
|
||
if (Networking.LocalPlayer.isInstanceOwner) { InstanceOwner = Networking.LocalPlayer.displayName; }
|
||
_RequestAtlasInfoUpdate();
|
||
_RequestSupporterInfoUpdate();
|
||
SendCustomEventDelayedSeconds(nameof(_PeriodicUpdate), periodicUpdateInterval);
|
||
}
|
||
public void _TaskLoop()
|
||
{
|
||
//Debug.Log("Task loop");
|
||
_TryUpdateAtlasInfo();
|
||
_TryUpdateSupporterInfo();
|
||
_TryUpdateCustomModules();
|
||
SendCustomEventDelayedSeconds(nameof(_TaskLoop), 1f);
|
||
}
|
||
|
||
|
||
private bool _shouldUpdateAtlasInfo = true;
|
||
private bool _shouldUpdateSupporterInfo = true;
|
||
private bool _shouldUpdateCustomModules = false;
|
||
private void _TryUpdateAtlasInfo()
|
||
{
|
||
if (!_shouldUpdateAtlasInfo) return;
|
||
Debug.Log("Trying to update atlas info !");
|
||
RequestUrlLoad(atlasInfoUrl);
|
||
_shouldUpdateAtlasInfo = false;
|
||
}
|
||
private void _TryUpdateSupporterInfo()
|
||
{
|
||
if (!_shouldUpdateSupporterInfo) return;
|
||
Debug.Log("Trying to update supporter info !");
|
||
RequestUrlLoad(supporterInfoUrl);
|
||
_shouldUpdateSupporterInfo = false;
|
||
}
|
||
|
||
private void _TryUpdateCustomModules()
|
||
{
|
||
if (!_shouldUpdateCustomModules) return;
|
||
Debug.Log("Trying to update custom modules");
|
||
for (int i = 0; i < customModuleUrls.Length; i++)
|
||
{
|
||
if (!customModuleEnabled[i]) continue;
|
||
VRCUrl url = customModuleUrls[i];
|
||
RequestUrlLoad(url);
|
||
}
|
||
_shouldUpdateCustomModules = false;
|
||
}
|
||
|
||
[PublicAPI] public void _RequestAtlasInfoUpdate()
|
||
{
|
||
_shouldUpdateAtlasInfo = true;
|
||
}
|
||
[PublicAPI] public void _RequestSupporterInfoUpdate()
|
||
{
|
||
_shouldUpdateSupporterInfo = true;
|
||
}
|
||
|
||
|
||
private VRCUrl[] _currentlyDownloadingUrls = new VRCUrl[0];
|
||
private void RequestUrlLoad(VRCUrl url)
|
||
{
|
||
Debug.Log("[VRCBOARD] Laoding URL " + url.Get());
|
||
string urlStr = url.Get();
|
||
for (int i = 0; i < _currentlyDownloadingUrls.Length; i++)
|
||
{
|
||
if (_currentlyDownloadingUrls[i].Get() == urlStr)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
VRCStringDownloader.LoadUrl(url, (IUdonEventReceiver)this);
|
||
VRCUrl[] newUrls = new VRCUrl[_currentlyDownloadingUrls.Length + 1];
|
||
for (int i = 0; i < _currentlyDownloadingUrls.Length; i++)
|
||
{
|
||
newUrls[i] = _currentlyDownloadingUrls[i];
|
||
}
|
||
newUrls[_currentlyDownloadingUrls.Length] = url;
|
||
_currentlyDownloadingUrls = newUrls;
|
||
}
|
||
|
||
|
||
public override void OnStringLoadSuccess(IVRCStringDownload download)
|
||
{
|
||
string url = download.Url.Get();
|
||
|
||
VRCUrl[] newUrls = new VRCUrl[_currentlyDownloadingUrls.Length - 1];
|
||
int newIndex = 0;
|
||
for (int i = 0; i < _currentlyDownloadingUrls.Length; i++)
|
||
{
|
||
if (_currentlyDownloadingUrls[i].Get() == url)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
newUrls[newIndex] = _currentlyDownloadingUrls[i];
|
||
newIndex++;
|
||
}
|
||
_currentlyDownloadingUrls = newUrls;
|
||
|
||
//Debug.Log("String load success " + url);
|
||
if (url == atlasInfoUrl.Get())
|
||
{
|
||
OnAtlasInfoDownload(download.Result);
|
||
} else if (url == supporterInfoUrl.Get())
|
||
{
|
||
OnSupporterInfoDownload(download.Result);
|
||
}
|
||
|
||
for (int i = 0; i < customModuleUrls.Length; i++)
|
||
{
|
||
if (customModuleUrls[i].Get() == url)
|
||
{
|
||
OnCustomModuleDownload(i, download.Result);
|
||
}
|
||
}
|
||
}
|
||
public override void OnStringLoadError(IVRCStringDownload result)
|
||
{
|
||
base.OnStringLoadError(result);
|
||
Debug.LogWarning(result.Url.Get() + " : " + result.Error.ToString());
|
||
}
|
||
public DataDictionary _GetImageIdInfo(string imageId)
|
||
{
|
||
if (_atlasInfo == null) return null;
|
||
if (!_atlasInfo.ContainsKey(imageId)) return null;
|
||
DataToken token = _atlasInfo[imageId];
|
||
if (token.TokenType != TokenType.DataDictionary)
|
||
{
|
||
Debug.LogWarning("[_GetImageIdInfo] " + imageId + " does not exist.");
|
||
}
|
||
return token.DataDictionary;
|
||
}
|
||
|
||
public bool _GetImageAtlasTexture(string imageId, string uploader, out int atlasIndex, out Texture2D texture, out int position, out int size, out string type, out string hash)
|
||
{
|
||
texture = null;
|
||
position = -1;
|
||
size = -1;
|
||
atlasIndex = -1;
|
||
type = null;
|
||
hash = null;
|
||
DataDictionary imageInfo = _GetImageIdInfo(imageId);
|
||
if (imageInfo == null)
|
||
{
|
||
//Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " not found!");
|
||
return false;
|
||
}
|
||
|
||
bool imageTypeSuccess = imageInfo.TryGetValue("type", out DataToken typeToken);
|
||
bool uploadsSuccess = imageInfo.TryGetValue("uploads", out DataToken uploadsToken);
|
||
bool atlasSizeSuccess = imageInfo.TryGetValue("size", out DataToken sizeToken);
|
||
if ((!imageTypeSuccess || !uploadsSuccess || !atlasSizeSuccess) ||
|
||
(typeToken.TokenType != TokenType.String || uploadsToken.TokenType != TokenType.DataList || sizeToken.TokenType != TokenType.Double))
|
||
{
|
||
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " is not a valid atlas.");
|
||
return false;
|
||
}
|
||
|
||
type = typeToken.String;
|
||
size = (int)sizeToken.Double;
|
||
|
||
bool isGlobal = type == "global";
|
||
|
||
DataList uploads = uploadsToken.DataList;
|
||
int uploadCount = uploads.Count;
|
||
if (uploadCount == 0)
|
||
{
|
||
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " has no uploads.");
|
||
return false;
|
||
}
|
||
// find index of the uploader
|
||
int uploaderIndex = 0;
|
||
if (!isGlobal)
|
||
{
|
||
uploaderIndex = -1;
|
||
if (uploader == null) return false;
|
||
for (int i = 0; i < uploadCount; i++)
|
||
{
|
||
DataDictionary upload = uploads[i].DataDictionary;
|
||
if (upload == null) continue;
|
||
bool uploaderSuccess = upload.TryGetValue("a", out DataToken uploaderToken);
|
||
//Debug.Log("Uploader success: " + uploaderSuccess);
|
||
if (!uploaderSuccess) continue;
|
||
if (uploaderToken.TokenType != TokenType.String) continue;
|
||
string uploadUploader = uploaderToken.String;
|
||
//Debug.Log("Comparing uploader " + uploadUploader + " to " + uploader);
|
||
if (uploadUploader == uploader)
|
||
{
|
||
//Debug.Log("Match");
|
||
uploaderIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
//Debug.Log("Uploader index: " + uploaderIndex);
|
||
if (uploaderIndex == -1)
|
||
{
|
||
|
||
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " uploader not found. uploaderIndex==-1");
|
||
return false;
|
||
}
|
||
DataDictionary uploadInfo = uploads[uploaderIndex].DataDictionary;
|
||
if (uploadInfo == null)
|
||
{
|
||
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " uploadInfo is null.");
|
||
return false;
|
||
}
|
||
bool atlasIndexSuccess = uploadInfo.TryGetValue("i", out DataToken atlasIndexToken);
|
||
if (!atlasIndexSuccess || atlasIndexToken.TokenType != TokenType.Double)
|
||
{
|
||
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " atlasIndex is not a valid number.");
|
||
return false;
|
||
}
|
||
|
||
atlasIndex = (int)atlasIndexToken.Double;
|
||
|
||
bool positionSuccess = uploadInfo.TryGetValue("p", out DataToken positionToken);
|
||
if (!positionSuccess || positionToken.TokenType != TokenType.Double)
|
||
{
|
||
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " position is not a valid number.");
|
||
return false;
|
||
}
|
||
texture = _downloadedAtlasTextures[atlasIndex];
|
||
position = (int)positionToken.Double;
|
||
|
||
bool hashSuccess = uploadInfo.TryGetValue("h", out DataToken hashToken);
|
||
if (!hashSuccess || hashToken.TokenType != TokenType.String)
|
||
{
|
||
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " hash is not a valid string.");
|
||
return false;
|
||
}
|
||
hash = hashToken.String;
|
||
return true;
|
||
}
|
||
|
||
public void _RequestCustomModuleUpdate(int moduleIndex)
|
||
{
|
||
if (moduleIndex < 0 || moduleIndex >= customModuleUrls.Length) return;
|
||
VRCUrl url = customModuleUrls[moduleIndex];
|
||
RequestUrlLoad(url);
|
||
}
|
||
public void _RequestImageLoad(string imageId, string uploader, VRCBoardBaseComponent component)
|
||
{
|
||
if (uploader == null) uploader = "null";
|
||
//Debug.Log("Requesting image load " + imageId);
|
||
if (string.IsNullOrEmpty(imageId)) return;
|
||
string combinedId = $"{imageId}:{uploader}";
|
||
if (HasImageInQueue(combinedId)) return;
|
||
AddImageToQueue(combinedId);
|
||
}
|
||
private void TryDownloadImageId(string combinedImageId)
|
||
{
|
||
Debug.Log("[VRCBoard] Trying to download imageid " + combinedImageId);
|
||
string[] split = combinedImageId.Split(':');
|
||
if (split.Length != 2) return;
|
||
string imageId = split[0];
|
||
string uploader = split[1];
|
||
if (uploader == "null") uploader = null;
|
||
bool success = _GetImageAtlasTexture(imageId, uploader, out int atlasIndex, out Texture2D texture,
|
||
out int position, out int size, out string type, out string hash);
|
||
Debug.Log("Try download image " + combinedImageId + " success: " + success);
|
||
if (!success) return;
|
||
|
||
/*if ( GetFromTextureCache(hash) != null)
|
||
{
|
||
Debug.Log("[VRCBoard] Image found in texture cache " + combinedImageId);
|
||
return;
|
||
}*/
|
||
|
||
DownloadAtlas(atlasIndex);
|
||
}
|
||
|
||
private void OnSupporterInfoDownload(string data)
|
||
{
|
||
Debug.Log("[VRCBoard] Supporter info download success");
|
||
if (VRCJson.TryDeserializeFromJson(data, out DataToken result))
|
||
{
|
||
if (result.TokenType != TokenType.DataDictionary) return;
|
||
SetSupporterInfo(result.DataDictionary);
|
||
OnSupporterInfoUpdate();
|
||
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError("[VRCBoard] Failed to deserialize supporter info");
|
||
}
|
||
}
|
||
private void SetSupporterInfo(DataDictionary value)
|
||
{
|
||
_supporterInfo = value;
|
||
if (_supporterInfo == null) return;
|
||
bool vrcsuccess = _supporterInfo.TryGetValue("vrclookup", out DataToken vrcToken);
|
||
bool supporterSuccess = _supporterInfo.TryGetValue("supporters", out DataToken supporterToken);
|
||
bool perkSuccess = _supporterInfo.TryGetValue("perkNodes", out DataToken perkToken);
|
||
if (vrcsuccess && vrcToken.TokenType == TokenType.DataDictionary)
|
||
{
|
||
_supporterInfoVrcLookup = vrcToken.DataDictionary;
|
||
}
|
||
if (supporterSuccess && supporterToken.TokenType == TokenType.DataList)
|
||
{
|
||
_supporterList = supporterToken.DataList;
|
||
}
|
||
if (perkSuccess && perkToken.TokenType == TokenType.DataList)
|
||
{
|
||
_perkNodes = perkToken.DataList;
|
||
}
|
||
//Debug.Log("Set supporter info "+vrcsuccess+" "+supporterSuccess+" "+perkSuccess);
|
||
}
|
||
|
||
private void OnCustomModuleDownload(int moduleIndex, string data)
|
||
{
|
||
Debug.Log("[VRCBoard] Custom module download success " + moduleIndex);
|
||
if (VRCJson.TryDeserializeFromJson(data, out DataToken result))
|
||
{
|
||
if (result.TokenType != TokenType.DataDictionary) return;
|
||
OnCustomModuleUpdate(moduleIndex, result.DataDictionary);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError("[VRCBoard] Failed to deserialize custom module " + moduleIndex);
|
||
}
|
||
}
|
||
|
||
private void OnCustomModuleUpdate(int moduleIndex, DataDictionary data)
|
||
{
|
||
Debug.Log("[VRCBoard] Custom module update " + moduleIndex);
|
||
foreach (var component in vrcBoardComponents)
|
||
component._OnCustomModuleUpdate(moduleIndex, data);
|
||
}
|
||
|
||
[PublicAPI]
|
||
public bool PlayerHasTag(VRCPlayerApi player, string tag)
|
||
{
|
||
return PlayerHasTag(player.displayName, tag);
|
||
}
|
||
|
||
[PublicAPI]
|
||
public String[] GetPlayerTags(string displayName)
|
||
{
|
||
DataDictionary supporterData = GetSupporterDataFromPlayer(displayName);
|
||
if (supporterData == null) return new string[0];
|
||
bool perkNodesSuccess = supporterData.TryGetValue("pn", out DataToken perkNodesToken);
|
||
if (!perkNodesSuccess || perkNodesToken.TokenType != TokenType.DataList) return new string[0];
|
||
DataList perkNodes = perkNodesToken.DataList;
|
||
string[] tags = new string[perkNodes.Count];
|
||
for (int i = 0; i < perkNodes.Count; i++)
|
||
{
|
||
DataToken perkNode = perkNodes[i];
|
||
if (perkNode.TokenType != TokenType.String) continue;
|
||
tags[i] = perkNode.String;
|
||
}
|
||
return tags;
|
||
}
|
||
|
||
[PublicAPI]
|
||
public bool PlayerHasTag(string displayName, string tag)
|
||
{
|
||
string[] tags = GetPlayerTags(displayName);
|
||
for (int i = 0; i < tags.Length; i++)
|
||
{
|
||
if (tags[i] == tag) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
[PublicAPI]
|
||
public int SupporterCount => _supporterList.Count;
|
||
[PublicAPI]
|
||
public DataDictionary GetSupporterData(int index)
|
||
{
|
||
DataToken token = _supporterList[index];
|
||
if (token.TokenType != TokenType.DataDictionary) return null;
|
||
DataDictionary supporterData = token.DataDictionary;
|
||
string perkNodesKey = "pn";
|
||
bool perkNodesSuccess = supporterData.TryGetValue(perkNodesKey, out DataToken pnToken);
|
||
if (!perkNodesSuccess || pnToken.TokenType != TokenType.DataList) return supporterData;
|
||
DataList perkNodes = pnToken.DataList;
|
||
for (int i = 0; i < perkNodes.Count; i++)
|
||
{
|
||
DataToken perkNodeToken = perkNodes[i];
|
||
if (perkNodeToken.TokenType != TokenType.Double) continue;
|
||
int perkIndex = (int)perkNodeToken.Double;
|
||
if (perkIndex < 0 || perkIndex >= _perkNodes.Count) continue;
|
||
DataToken perkNode = _perkNodes[perkIndex];
|
||
if (perkNode.TokenType != TokenType.DataDictionary) continue;
|
||
bool perkNameSuccess = perkNode.DataDictionary.TryGetValue("id", out DataToken perkNameToken);
|
||
if (!perkNameSuccess || perkNameToken.TokenType != TokenType.String) continue;
|
||
perkNodes[i] = perkNameToken;
|
||
}
|
||
supporterData[perkNodesKey] = perkNodes;
|
||
return supporterData;
|
||
}
|
||
|
||
[PublicAPI]
|
||
public DataDictionary GetSupporterDataFromPlayer(string vrcName)
|
||
{
|
||
if (_supporterInfoVrcLookup == null) return null;
|
||
if (!_supporterInfoVrcLookup.ContainsKey(vrcName)) return null;
|
||
DataToken token = _supporterInfoVrcLookup[vrcName];
|
||
if (token.TokenType != TokenType.Double) return null;
|
||
int index = (int)token.Double;
|
||
return GetSupporterData(index);
|
||
}
|
||
|
||
[PublicAPI]
|
||
public DataDictionary GetSupporterDataFromPlayer(VRCPlayerApi player)
|
||
{
|
||
return GetSupporterDataFromPlayer(player.displayName);
|
||
}
|
||
|
||
private void OnSupporterInfoUpdate()
|
||
{
|
||
Debug.Log("[VRCBoard] Supporter info updated");
|
||
foreach (var component in vrcBoardComponents)
|
||
component._OnSupporterDataUpdate();
|
||
}
|
||
|
||
private void OnAtlasInfoDownload(string data)
|
||
{
|
||
Debug.Log("[VRCBoard] Atlas info download success");
|
||
if (VRCJson.TryDeserializeFromJson(data, out DataToken result))
|
||
{
|
||
if (result.TokenType != TokenType.DataDictionary) return;
|
||
_atlasInfo = result.DataDictionary;
|
||
OnAtlasInfoUpdate();
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError("[VRCBoard] Failed to deserialize atlas info");
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private void OnAtlasInfoUpdate()
|
||
{
|
||
//Debug.Log("Atlas info updated");
|
||
if (_atlasInfo == null) return;
|
||
while (true)
|
||
{
|
||
string combinedImageId = GetNextImageInQueue();
|
||
if (combinedImageId == null) break;
|
||
TryDownloadImageId(combinedImageId);
|
||
}
|
||
|
||
foreach (var component in vrcBoardComponents)
|
||
component._OnImageInfoUpdate();
|
||
}
|
||
|
||
|
||
|
||
private void DownloadAtlas(int index)
|
||
{
|
||
Debug.Log("Downloading atlas " + index);
|
||
if (index < 0 || index >= atlasUrls.Length) return;
|
||
VRCUrl url = GetAtlasUrlFromIndex(index);
|
||
VRCImageDownloader imageDownloader = _imageDownloaders[index];
|
||
imageDownloader.Dispose();
|
||
|
||
if (url == null) return;
|
||
TextureInfo textureInfo = new TextureInfo();
|
||
textureInfo.FilterMode = FilterMode.Bilinear;
|
||
textureInfo.WrapModeU = TextureWrapMode.Clamp;
|
||
textureInfo.WrapModeV = TextureWrapMode.Clamp;
|
||
textureInfo.WrapModeW = TextureWrapMode.Clamp;
|
||
IVRCImageDownload download =
|
||
imageDownloader.DownloadImage(url, null, (IUdonEventReceiver)this, textureInfo);
|
||
}
|
||
|
||
public override void OnImageLoadSuccess(IVRCImageDownload result)
|
||
{
|
||
string url = result.Url.Get();
|
||
Debug.Log("[VRCBoard] Image load success " + url);
|
||
Texture2D texture = result.Result;
|
||
int index = GetAtlasIndexFromUrl(url);
|
||
if (index == -1) return;
|
||
_downloadedAtlasTextures[index] = texture;
|
||
_atlasDownloadTimes[index] = Time.time;
|
||
Debug.Log("[VRCBoard] Downloaded atlas texture " + index);
|
||
foreach (var component in vrcBoardComponents)
|
||
component._OnAtlasImageLoaded(index);
|
||
}
|
||
|
||
private static int NextPowerOf2(int value)
|
||
{
|
||
if (value <= 0)
|
||
return 1;
|
||
|
||
// Use bitwise manipulation to find the next power of 2
|
||
value--;
|
||
value |= value >> 1;
|
||
value |= value >> 2;
|
||
value |= value >> 4;
|
||
value |= value >> 8;
|
||
value |= value >> 16;
|
||
return value + 1;
|
||
}
|
||
|
||
private string[] textureCacheKeys = new string[0];
|
||
private Texture2D[] textureCacheTextures = new Texture2D[0];
|
||
private int textureCacheCurrentSize = 0;
|
||
|
||
public void AddToTextureCache(string key, Texture2D texture)
|
||
{
|
||
// If the store is full, resize the arrays
|
||
if (textureCacheCurrentSize >= textureCacheKeys.Length)
|
||
{
|
||
ResizeArrays(textureCacheCurrentSize + 1);
|
||
}
|
||
|
||
// Add the new key-value pair
|
||
textureCacheKeys[textureCacheCurrentSize-1] = key;
|
||
textureCacheTextures[textureCacheCurrentSize-1] = texture;
|
||
}
|
||
|
||
// Method to get a texture by its key
|
||
public Texture2D GetFromTextureCache(string key)
|
||
{
|
||
for (int i = 0; i < textureCacheCurrentSize; i++)
|
||
{
|
||
if (textureCacheKeys[i] == key)
|
||
{
|
||
return textureCacheTextures[i];
|
||
}
|
||
}
|
||
//Debug.LogWarning("[VRCBoard] Key not found: " + key);
|
||
return null;
|
||
}
|
||
|
||
private void ResizeArrays(int newSize)
|
||
{
|
||
string[] newKeys = new string[newSize];
|
||
Texture2D[] newTextures = new Texture2D[newSize];
|
||
|
||
// Copy existing data to the new arrays
|
||
for (int i = 0; i < textureCacheCurrentSize; i++)
|
||
{
|
||
newKeys[i] = textureCacheKeys[i];
|
||
newTextures[i] = textureCacheTextures[i];
|
||
}
|
||
|
||
// Replace old arrays with the new resized arrays
|
||
textureCacheKeys = newKeys;
|
||
textureCacheTextures = newTextures;
|
||
|
||
textureCacheCurrentSize = newSize;
|
||
}
|
||
|
||
public void BilinearScale(Texture2D tex, int newWidth, int newHeight)
|
||
{
|
||
Color[] newPixels = new Color[newWidth * newHeight];
|
||
|
||
Color[] originalPixels = tex.GetPixels();
|
||
int origWidth = tex.width;
|
||
int origHeight = tex.height;
|
||
|
||
float ratioX = (float)(origWidth - 1) / newWidth;
|
||
float ratioY = (float)(origHeight - 1) / newHeight;
|
||
|
||
for (int y = 0; y < newHeight; y++)
|
||
{
|
||
float sy = y * ratioY;
|
||
int yFloor = Mathf.FloorToInt(sy);
|
||
int yCeil = Mathf.Min(yFloor + 1, origHeight - 1);
|
||
float fy = sy - yFloor;
|
||
|
||
for (int x = 0; x < newWidth; x++)
|
||
{
|
||
float sx = x * ratioX;
|
||
int xFloor = Mathf.FloorToInt(sx);
|
||
int xCeil = Mathf.Min(xFloor + 1, origWidth - 1);
|
||
float fx = sx - xFloor;
|
||
|
||
// Manual bilinear interpolation
|
||
Color c00 = originalPixels[yFloor * origWidth + xFloor];
|
||
Color c10 = originalPixels[yFloor * origWidth + xCeil];
|
||
Color c01 = originalPixels[yCeil * origWidth + xFloor];
|
||
Color c11 = originalPixels[yCeil * origWidth + xCeil];
|
||
|
||
Color interpolated =
|
||
c00 * (1 - fx) * (1 - fy) +
|
||
c10 * fx * (1 - fy) +
|
||
c01 * (1 - fx) * fy +
|
||
c11 * fx * fy;
|
||
|
||
newPixels[y * newWidth + x] = interpolated;
|
||
}
|
||
}
|
||
|
||
tex.Reinitialize(newWidth, newHeight);
|
||
tex.SetPixels(newPixels);
|
||
tex.Apply();
|
||
}
|
||
|
||
|
||
[PublicAPI]
|
||
public Texture2D GetTexture2D(string uploader, string imageId, out string imageHash, bool bilinearScaling = false)
|
||
{
|
||
imageHash = null;
|
||
|
||
if (!_GetImageAtlasTexture(imageId, uploader, out int atlasIndex, out Texture2D texture,
|
||
out int position, out int size, out string type, out string hash) || atlasIndex == -1 || texture == null)
|
||
{
|
||
Debug.LogWarning("[VRCBoard] [GetTexture2D] Failed to get texture for " + uploader);
|
||
return null;
|
||
}
|
||
|
||
imageHash = hash;
|
||
|
||
Texture2D cachedTexture = GetFromTextureCache(hash);
|
||
if (cachedTexture != null) return cachedTexture;
|
||
|
||
int blockSize = texture.width / size;
|
||
|
||
int xIndex = position % size;
|
||
int yIndex = size - 1 - (position / size);
|
||
|
||
int startX = xIndex * blockSize;
|
||
int startY = yIndex * blockSize;
|
||
|
||
Color[] pixels = texture.GetPixels(startX, startY, blockSize, blockSize);
|
||
|
||
// Instead of bilinear sampling, scale using Unity’s built-in Resize method.
|
||
Texture2D newTexture = new Texture2D(blockSize, blockSize, TextureFormat.RGBA32, false);
|
||
newTexture.SetPixels(pixels);
|
||
|
||
if (bilinearScaling) BilinearScale(newTexture, 512, 512);
|
||
|
||
newTexture.wrapMode = TextureWrapMode.Repeat;
|
||
newTexture.filterMode = FilterMode.Trilinear;
|
||
newTexture.anisoLevel = 1;
|
||
|
||
newTexture.Apply(true,true);
|
||
|
||
AddToTextureCache(hash, newTexture);
|
||
return newTexture;
|
||
}
|
||
|
||
[PublicAPI]
|
||
public Texture2D GetTexture2D(string uploader, string imageId, bool bilinearScaling = false)
|
||
{
|
||
return GetTexture2D(uploader, imageId, out string imageHash, bilinearScaling);
|
||
}
|
||
|
||
public override void OnImageLoadError(IVRCImageDownload result)
|
||
{
|
||
base.OnImageLoadError(result);
|
||
Debug.LogWarning(result.Url.Get() + " : " + result.Error.ToString());
|
||
}
|
||
private int GetAtlasIndexFromUrl(string url)
|
||
{
|
||
for (int i = 0; i < atlasUrls.Length; i++)
|
||
{
|
||
if (atlasUrls[i].Get() == url) return i;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
private VRCUrl GetAtlasUrlFromIndex(int index)
|
||
{
|
||
if (index < 0 || index >= atlasUrls.Length || atlasUrls[index] == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return atlasUrls[index];
|
||
}
|
||
|
||
#region Queue Functions
|
||
|
||
private void AddImageToQueue(string imageId)
|
||
{
|
||
string[] newQueue = new string[imageIdDownloadQueue.Length + 1];
|
||
for (int i = 0; i < imageIdDownloadQueue.Length; i++)
|
||
{
|
||
newQueue[i] = imageIdDownloadQueue[i];
|
||
}
|
||
|
||
newQueue[imageIdDownloadQueue.Length] = imageId;
|
||
imageIdDownloadQueue = newQueue;
|
||
_RequestAtlasInfoUpdate();
|
||
}
|
||
|
||
private bool HasImageInQueue(string imageId)
|
||
{
|
||
for (int i = 0; i < imageIdDownloadQueue.Length; i++)
|
||
{
|
||
if (imageIdDownloadQueue[i] == imageId)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private string GetNextImageInQueue()
|
||
{
|
||
if (imageIdDownloadQueue.Length == 0) return null;
|
||
string imageId = imageIdDownloadQueue[0];
|
||
string[] newQueue = new string[imageIdDownloadQueue.Length - 1];
|
||
for (int i = 1; i < imageIdDownloadQueue.Length; i++)
|
||
{
|
||
newQueue[i - 1] = imageIdDownloadQueue[i];
|
||
}
|
||
|
||
imageIdDownloadQueue = newQueue;
|
||
return imageId;
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|
||
#if UNITY_EDITOR && !UDONSHARP_COMPILER
|
||
namespace VRCBoard
|
||
{
|
||
[CustomEditor(typeof(VRCBoardManager))]
|
||
public class VrcBoardManagerEditor : Editor
|
||
{
|
||
bool shouldRefreshUrls = true;
|
||
bool showGitSettings = false;
|
||
private void OnEnable()
|
||
{
|
||
shouldRefreshUrls = true;
|
||
}
|
||
public override void OnInspectorGUI()
|
||
{
|
||
// is first render
|
||
//base.OnInspectorGUI();
|
||
VRCBoardManager manager = target as VRCBoardManager;
|
||
if (manager == null)
|
||
{
|
||
EditorGUILayout.HelpBox("This script is not attached to a VRCBoardManager object", MessageType.Error);
|
||
return;
|
||
}
|
||
|
||
EditorGUILayout.HelpBox("VRCBoard Manager", MessageType.Info);
|
||
string domainInput = EditorGUILayout.TextField("Your VRCBoard Domain", manager.vrcBoardDomain);
|
||
bool changed = domainInput != manager.vrcBoardDomain;
|
||
|
||
int urlCount = EditorGUILayout.IntField("Atlas Url Count [10-1000]", manager.atlasUrlCount);
|
||
bool urlCountChanged = urlCount != manager.atlasUrlCount;
|
||
manager.atlasUrlCount = Math.Clamp(urlCount, 10, 1000);
|
||
|
||
|
||
if (changed || urlCountChanged || shouldRefreshUrls)
|
||
{
|
||
domainInput = domainInput.Trim();
|
||
domainInput = domainInput.Replace("http://", "");
|
||
domainInput = domainInput.Replace("https://", "");
|
||
domainInput = domainInput.Replace("/", "");
|
||
manager.vrcBoardDomain = domainInput;
|
||
|
||
string baseUrl = manager.VrcBoardBaseUrl;
|
||
// update the urls
|
||
manager._sourceAtlasUrls = new VRCUrl[manager.atlasUrlCount];
|
||
manager._proxyAtlasUrls = new VRCUrl[manager.atlasUrlCount];
|
||
for (int i = 0; i < manager.atlasUrlCount; i++)
|
||
{
|
||
manager._sourceAtlasUrls[i] = new VRCUrl(baseUrl + "atlas/" + i);
|
||
manager._proxyAtlasUrls[i] = new VRCUrl(manager.GIT_BASE_URL + "atlas/image" + i + ".png");
|
||
}
|
||
|
||
const string apiPath = "api/data/v1/";
|
||
|
||
bool[] oldEnabled = manager.customModuleEnabled;
|
||
manager._sourceCustomModuleUrls = new VRCUrl[3];
|
||
manager._proxyCustomModuleUrls = new VRCUrl[3];
|
||
manager.customModuleEnabled = new bool[3];
|
||
manager._sourceCustomModuleUrls[0] = new VRCUrl(baseUrl + apiPath + "module/barmanager");
|
||
manager._proxyCustomModuleUrls[0] = new VRCUrl(manager.GIT_BASE_URL + "custom_modules/barmanager");
|
||
|
||
for (int i = 0; i < oldEnabled.Length; i++)
|
||
{
|
||
if (i < manager.customModuleEnabled.Length) manager.customModuleEnabled[i] = oldEnabled[i];
|
||
}
|
||
|
||
manager._sourceAtlasInfoUrl = new VRCUrl(baseUrl + apiPath + "atlas");
|
||
manager._sourceSupporterInfoUrl = new VRCUrl(baseUrl + apiPath + "supporters");
|
||
|
||
manager._proxyAtlasInfoUrl = new VRCUrl(manager.GIT_BASE_URL + "atlas.json");
|
||
manager._proxySupporterInfoUrl = new VRCUrl(manager.GIT_BASE_URL + "supporters.json");
|
||
|
||
EditorUtility.SetDirty(manager);
|
||
}
|
||
|
||
|
||
float selectedInterval = EditorGUILayout.FloatField("Periodic Update Interval", manager.periodicUpdateInterval);
|
||
if (selectedInterval != manager.periodicUpdateInterval)
|
||
{
|
||
manager.periodicUpdateInterval = Mathf.Clamp(selectedInterval, 5f, 600f);
|
||
EditorUtility.SetDirty(manager);
|
||
}
|
||
|
||
bool instanceOwner = EditorGUILayout.Toggle("Enable Automatic Instance Owner Detection", manager.automaticInstanceOwner);
|
||
if (instanceOwner != manager.automaticInstanceOwner)
|
||
{
|
||
manager.automaticInstanceOwner = instanceOwner;
|
||
EditorUtility.SetDirty(manager);
|
||
}
|
||
|
||
EditorGUILayout.Separator();
|
||
|
||
SerializedProperty customModuleEnabled = serializedObject.FindProperty("customModuleEnabled");
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.LabelField("[Module] Group Manager");
|
||
// dropdown with enabled/disabled
|
||
customModuleEnabled.GetArrayElementAtIndex(0).boolValue = EditorGUILayout.Popup(customModuleEnabled.GetArrayElementAtIndex(0).boolValue ? 1 : 0, new[] {"Disabled", "Enabled"}) == 1;
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.Separator();
|
||
|
||
showGitSettings = EditorGUILayout.Foldout(showGitSettings, "GitHub Settings");
|
||
if (showGitSettings)
|
||
{
|
||
EditorGUILayout.HelpBox("GitHub can be used to host the images on trusted URLs. See more info on the VRCBoard website.", MessageType.Info);
|
||
string gitUser = EditorGUILayout.TextField("GitHub Username", manager.GIT_USERNAME);
|
||
string gitRepo = EditorGUILayout.TextField("GitHub Repo", manager.GIT_REPO);
|
||
//string gitBranch = EditorGUILayout.TextField("GitHub Branch", manager.GIT_BRANCH);
|
||
bool gitChanged = gitUser != manager.GIT_USERNAME || gitRepo != manager.GIT_REPO; //|| gitBranch != manager.GIT_BRANCH;
|
||
if (gitChanged)
|
||
{
|
||
manager.GIT_USERNAME = gitUser;
|
||
manager.GIT_REPO = gitRepo;
|
||
EditorUtility.SetDirty(manager);
|
||
}
|
||
}
|
||
|
||
|
||
EditorGUILayout.Separator();
|
||
|
||
SerializedProperty vrcBoardComponents = serializedObject.FindProperty("vrcBoardComponents");
|
||
EditorGUILayout.PropertyField(vrcBoardComponents);
|
||
|
||
if (GUILayout.Button("Link all VRCBoard components"))
|
||
{
|
||
VRCBoardBaseComponent[] components = FindObjectsOfType<VRCBoardBaseComponent>(true);
|
||
manager.vrcBoardComponents = components;
|
||
// mark the object as dirty
|
||
EditorUtility.SetDirty(manager);
|
||
}
|
||
|
||
|
||
serializedObject.ApplyModifiedProperties();
|
||
|
||
//DrawDefaultInspector();
|
||
shouldRefreshUrls = false;
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
#endif
|