feat: GitHub integration and URL management
All checks were successful
Create Unity Package / package (push) Successful in 8s

This commit is contained in:
2025-11-25 18:38:00 +02:00
parent ac7c0352e5
commit cf5f71e34f
13 changed files with 1273 additions and 615 deletions

View File

@@ -25,8 +25,14 @@ namespace VRCBoard
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";
@@ -35,26 +41,49 @@ namespace VRCBoard
public int atlasUrlCount = 100;
public VRCBoardBaseComponent[] vrcBoardComponents;
public byte waitIndex = 0;
public VRCUrl[] atlasUrls;
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 VRCUrl[] customModuleUrls;
public bool[] customModuleEnabled;
public VRCUrl atlasInfoUrl;
private DataDictionary _atlasInfo;
public VRCUrl supporterInfoUrl;
private DataDictionary _supporterInfo;
private DataDictionary _supporterInfoVrcLookup;
private DataList _supporterList;
private DataList _perkNodes;
[UdonSynced, FieldChangeCallback(nameof(instanceOwnerOverride))]
private string _instanceOwnerOverride = "";
private string instanceOwnerOverride
@@ -68,31 +97,51 @@ namespace VRCBoard
}
}
[UdonSynced, FieldChangeCallback(nameof(InstanceOwner))] private string _instanceOwner;
[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;
Networking.SetOwner(Networking.LocalPlayer, gameObject);
}
public string InstanceOwner
{
get {
if (!string.IsNullOrWhiteSpace(instanceOwnerOverride)) return instanceOwnerOverride;
return _instanceOwner;
if (automaticInstanceOwner)
{
return _instanceOwner;
}
return String.Empty;
}
private set
{
_instanceOwner = value;
Networking.SetOwner(Networking.LocalPlayer, gameObject);
RequestSerialization();
Debug.Log("Instance owner set to " + 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;
@@ -198,6 +247,7 @@ namespace VRCBoard
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++)
{
@@ -281,7 +331,7 @@ namespace VRCBoard
DataDictionary imageInfo = _GetImageIdInfo(imageId);
if (imageInfo == null)
{
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " not found!");
//Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " not found!");
return false;
}
@@ -304,6 +354,7 @@ namespace VRCBoard
int uploadCount = uploads.Count;
if (uploadCount == 0)
{
Debug.LogWarning("[_GetImageAtlasTexture] " + imageId + " has no uploads.");
return false;
}
// find index of the uploader
@@ -333,16 +384,20 @@ namespace VRCBoard
//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;
}
@@ -351,6 +406,7 @@ namespace VRCBoard
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];
@@ -359,11 +415,19 @@ namespace VRCBoard
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";
@@ -375,6 +439,7 @@ namespace VRCBoard
}
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];
@@ -382,10 +447,16 @@ namespace VRCBoard
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);
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)
@@ -535,7 +606,7 @@ namespace VRCBoard
private void OnAtlasInfoDownload(string data)
{
//Debug.Log("Atlas info download success");
Debug.Log("[VRCBoard] Atlas info download success");
if (VRCJson.TryDeserializeFromJson(data, out DataToken result))
{
if (result.TokenType != TokenType.DataDictionary) return;
@@ -569,7 +640,7 @@ namespace VRCBoard
private void DownloadAtlas(int index)
{
//Debug.Log("Downloading atlas " + index);
Debug.Log("Downloading atlas " + index);
if (index < 0 || index >= atlasUrls.Length) return;
VRCUrl url = GetAtlasUrlFromIndex(index);
VRCImageDownloader imageDownloader = _imageDownloaders[index];
@@ -664,74 +735,100 @@ namespace VRCBoard
textureCacheCurrentSize = newSize;
}
[PublicAPI]
public Texture2D GetTexture2D(string uploader, string imageId, out string imageHash)
public void BilinearScale(Texture2D tex, int newWidth, int newHeight)
{
bool success = _GetImageAtlasTexture(imageId, uploader, out int atlasIndex, out Texture2D texture,
out int position, out int size, out string type, out string hash);
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;
if (!success || atlasIndex == -1)
{
Debug.LogWarning("[VRCBoard] [GetTexture2D] Failed to get texture for " + uploader);
return null;
}
Color[] pixels = texture.GetPixels(startX, startY, blockSize, blockSize);
if (texture == null)
{
Debug.LogWarning("[VRCBoard] [GetTexture2D] Failed to get texture for " + uploader);
return null;
}
// Instead of bilinear sampling, scale using Unitys built-in Resize method.
Texture2D newTexture = new Texture2D(blockSize, blockSize, TextureFormat.RGBA32, false);
newTexture.SetPixels(pixels);
int blockSize = (int)Mathf.Floor((float)(texture.width) / (float)(size));
float startX = position % size;
float startY = size - (int)Mathf.Floor((float)position / (float)size) - 1;
startX = startX / size;
startY = startY / size;
if (bilinearScaling) BilinearScale(newTexture, 512, 512);
//Debug.Log("[VRCBoard] texture startX:" + startX + ", startY:" + startY);
int textureSize = 1024;
Texture2D newTexture = new Texture2D (textureSize, textureSize);
Color[] stretchedPixels = new Color[textureSize * textureSize];
for (int y = 0; y < textureSize; y++)
{
for (int x = 0; x < textureSize; x++)
{
// Get the corresponding pixel from the original texture
float u = startX + ((float)x / textureSize / size);
float v = startY + ((float)y / textureSize / size);
// Sample the original texture at the calculated UV coordinates
Color pixel = texture.GetPixelBilinear(u, v);
stretchedPixels[y * textureSize + x] = pixel;
}
}
//Color[] pixels = texture.GetPixels(startX * blockSize, startY * blockSize, blockSize, blockSize);
newTexture.SetPixels(stretchedPixels);
newTexture.wrapMode = TextureWrapMode.Clamp;
newTexture.wrapMode = TextureWrapMode.Repeat;
newTexture.filterMode = FilterMode.Trilinear;
newTexture.anisoLevel = 1;
newTexture.Apply();
newTexture.Apply(true,true);
AddToTextureCache(hash, newTexture);
return newTexture;
}
[PublicAPI]
public Texture2D GetTexture2D(string uploader, string imageId)
public Texture2D GetTexture2D(string uploader, string imageId, bool bilinearScaling = false)
{
return GetTexture2D(uploader, imageId, out string imageHash);
return GetTexture2D(uploader, imageId, out string imageHash, bilinearScaling);
}
public override void OnImageLoadError(IVRCImageDownload result)
@@ -810,6 +907,7 @@ namespace VRCBoard
public class VrcBoardManagerEditor : Editor
{
bool shouldRefreshUrls = true;
bool showGitSettings = false;
private void OnEnable()
{
shouldRefreshUrls = true;
@@ -844,32 +942,52 @@ namespace VRCBoard
string baseUrl = manager.VrcBoardBaseUrl;
// update the urls
manager.atlasUrls = new VRCUrl[manager.atlasUrlCount];
manager._sourceAtlasUrls = new VRCUrl[manager.atlasUrlCount];
manager._proxyAtlasUrls = new VRCUrl[manager.atlasUrlCount];
for (int i = 0; i < manager.atlasUrlCount; i++)
{
manager.atlasUrls[i] = new VRCUrl(baseUrl + "atlas/" + 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.customModuleUrls = new VRCUrl[3];
manager._sourceCustomModuleUrls = new VRCUrl[3];
manager._proxyCustomModuleUrls = new VRCUrl[3];
manager.customModuleEnabled = new bool[3];
manager.customModuleUrls[0] = new VRCUrl(baseUrl + apiPath + "barmanager");
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.atlasInfoUrl = new VRCUrl(baseUrl + apiPath + "atlas");
manager.supporterInfoUrl = new VRCUrl(baseUrl + apiPath + "supporters");
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);
manager.periodicUpdateInterval = Mathf.Clamp(selectedInterval, 5f, 600f);
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");
@@ -880,6 +998,25 @@ namespace VRCBoard
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");