mirror of
https://git.aaro.dev/VRCBoard/vrcboard-udon.git
synced 2026-03-17 02:49:46 +00:00
feat: GitHub integration and URL management
All checks were successful
Create Unity Package / package (push) Successful in 8s
All checks were successful
Create Unity Package / package (push) Successful in 8s
This commit is contained in:
@@ -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 Unity’s 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");
|
||||
|
||||
Reference in New Issue
Block a user