Added Unity project files

This commit is contained in:
2026-06-07 16:58:24 +01:00
parent 3cc05d260b
commit 23bbcab156
3942 changed files with 453676 additions and 0 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7a3b976fcbb135046afbf6894a86382a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 7d8ee2f423f373f489148ca12b9a44ef
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
namespace VRC.ExampleCentral.Types.Algolia
{
public class UnityPackage
{
public string AssetId { get; set; }
public string AssetRevision { get; set; }
public string Title { get; set; }
public string Version { get; set; }
public string UnityPackageFile { get; set; }
public string ThumbnailImage { get; set; }
public string Author { get; set; }
public string Description { get; set; }
[Newtonsoft.Json.JsonProperty("_tags")]
public string[] Tags { get; set; }
public string DocsLink { get; set; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe2092d56e9eb434285274ef1583ecaa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 73a1f0f944d9faf40905cd89382d8617
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,104 @@
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.IO;
using JetBrains.Annotations;
namespace VRC.ExampleCentral.Window
{
public static class ExampleCentralSettings
{
public static readonly string customSettingsPath = "VRChat/Example Central";
[SettingsProvider]
public static SettingsProvider CreateExampleCentralSettingsProvider()
{
var provider = new SettingsProvider(customSettingsPath, SettingsScope.Project)
{
label = "Example Central",
// This method will draw the UI for the settings
activateHandler = (searchContext, rootElement) =>
{
var settings = Data.Instance;
var styleSheet = Resources.Load<StyleSheet>("ExampleCentralSettingsStyle");
rootElement.styleSheets.Add(styleSheet);
var container = new VisualElement()
{
name = "container",
};
rootElement.Add(container);
var titleLabel = new Label("Example Central Settings")
{
name = "title-label"
};
container.Add(titleLabel);
var showCEField = new Toggle("Show Creator Economy Examples")
{
value = settings.ShowEconomyPackages,
name = "show-economy-packages-field"
};
showCEField.RegisterValueChangedCallback(evt =>
{
settings.ShowEconomyPackages = evt.newValue;
settings.Save();
var existingExampleCentralWindows = Resources.FindObjectsOfTypeAll<ExampleDownloaderPanel>();
if (existingExampleCentralWindows.Length > 0)
{
var target = existingExampleCentralWindows[0];
target.Refresh();
}
});
container.Add(showCEField);
},
// Populate the search keywords to enable smart search filtering and label highlighting
keywords = new[] { "example", "central", "vrchat", "show creator economy examples" }
};
return provider;
}
public static void OpenSettings()
{
// Open the Project Settings window with these settings selected
SettingsService.OpenProjectSettings(customSettingsPath);
}
// Uses simple embedded Data class which saves and loads from EditorPrefs
public class Data
{
public bool ShowEconomyPackages;
private static Data _data;
public static Data Instance
{
get
{
// Create if needed
if(_data == null)
{
_data = new Data();
// Set properties from EditorPrefs
_data.ShowEconomyPackages = EditorPrefs.GetBool(nameof(ShowEconomyPackages));
}
return _data;
}
}
public void Save()
{
EditorPrefs.SetBool(nameof(ShowEconomyPackages), ShowEconomyPackages);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 76b1e1dae078236489b5d0c483ea0d69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,370 @@
using System;
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Algolia.Search.Clients;
using Algolia.Search.Models.Search;
using UnityEditor.UIElements;
using VRC.Core;
using AlgoliaPackage = VRC.ExampleCentral.Types.Algolia.UnityPackage;
using Button = UnityEngine.UIElements.Button;
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
namespace VRC.ExampleCentral.Window
{
public class ExampleDownloaderPanel : EditorWindow
{
[SerializeField] private VisualTreeAsset VisualTree;
// Visual Elements, cached after creation
private VisualElement goToLoginView;
private VisualElement mainView;
private VisualElement previewThumbnail;
private Label previewTitle;
private Label previewTags;
private Label previewVersion;
private Label previewDescription;
private Button previewDownloadButton;
private Button previewDocsButtons;
private ToolbarSearchField searchField;
private VisualElement examplesScrollviewContainer;
private Button settingsButton;
// Searching
private CancellationTokenSource typingCancellation;
private int searchDelayMs = 500;
private AlgoliaPackage selectedPackage;
private PackageButton selectedButton;
// Algolia Info
public const string AlgoliaSearchKey = "60b185254097bf630e913ceaf103822a";
public const string AlgoliaAppKey = "P787M8AJR8";
public const string AlgoliaIndexName = "unity-packages";
// Analytics rate-limiting
private const int EVENT_LIMIT_EXAMPLE_PREVIEWED = 10;
private const int TIME_LIMIT_EXAMPLE_PREVIEWED = 60;
private int eventCounterExamplePreviewed;
private DateTime lastResetTimeExamplePreviewed = DateTime.Now;
#region Setup and Basics
[MenuItem("VRChat SDK/🏠 Example Central", false, 980)]
public static void ShowWindow()
{
ExampleDownloaderPanel window = GetWindow<ExampleDownloaderPanel>();
window.titleContent = new GUIContent("VRChat Example Central");
window.Show();
AnalyticsSDK.ExampleCentralOpened();
}
private void CreateGUI()
{
// Set up base layout
VisualTree.CloneTree(rootVisualElement);
// Get top-level UI containers
goToLoginView = rootVisualElement.Query<VisualElement>("go-to-login");
mainView = rootVisualElement.Query<VisualElement>("split-main");
// Get Auth View elements
Button goToLoginButton = goToLoginView.Query<Button>("go-to-login-btn");
// If the user is already logged in, enable main view
if (APIUser.IsLoggedIn)
OnUserLoggedIn(null, null);
// Get Listing elements
searchField = rootVisualElement.Query<ToolbarSearchField>("examples-search-field");
searchField.RegisterValueChangedCallback(CheckSearch);
// Get Example elements
examplesScrollviewContainer = rootVisualElement.Query<VisualElement>("examples-scrollview-container");
examplesScrollviewContainer.Clear();
// Get Preview elements
previewThumbnail = rootVisualElement.Query<VisualElement>("preview-thumbnail");
previewTitle = rootVisualElement.Query<Label>("preview-title");
previewTags = rootVisualElement.Query<Label>("preview-tags");
previewVersion = rootVisualElement.Query<Label>("preview-version");
previewDescription = rootVisualElement.Query<Label>("preview-description");
previewDownloadButton = rootVisualElement.Query<Button>("preview-download");
previewDocsButtons = rootVisualElement.Query<Button>("preview-documentation");
previewDownloadButton.clicked += DownloadSelectedPackage;
previewDocsButtons.clicked += OpenSelectedPackageDocs;
goToLoginButton.clicked += OpenControlPanel;
VRCSdkControlPanel.OnPanelLoggedIn += OnUserLoggedIn;
VRCSdkControlPanel.OnPanelLoggedOut += OnUserLoggedOut;
settingsButton = rootVisualElement.Query<Button>("settings-button");
var icon = Resources.Load<Texture2D>("gear");
settingsButton.style.backgroundImage = new StyleBackground(icon);
settingsButton.clicked += ExampleCentralSettings.OpenSettings;
// Set the default preview.
DemoPreview();
// Fetch and display UnityPackages
UpdatePackagesAsync();
}
private void OnDestroy()
{
previewDownloadButton.clicked -= DownloadSelectedPackage;
previewDocsButtons.clicked -= OpenSelectedPackageDocs;
VRCSdkControlPanel.OnPanelLoggedIn -= OnUserLoggedIn;
VRCSdkControlPanel.OnPanelLoggedOut -= OnUserLoggedOut;
}
private static void OpenControlPanel() => VRCSdkControlPanel.ShowControlPanel();
private void OnUserLoggedIn(object _, APIUser __) => ToggleView(true);
private void OnUserLoggedOut(object _, EventArgs __) => ToggleView(false);
private void ToggleView(bool isUserLoggedIn)
{
goToLoginView.style.display = isUserLoggedIn ? DisplayStyle.None : DisplayStyle.Flex;
mainView.style.display = isUserLoggedIn ? DisplayStyle.Flex : DisplayStyle.None;
}
private void OpenSelectedPackageDocs()
{
if (selectedPackage == null) return;
Application.OpenURL($"{selectedPackage.DocsLink}");
}
private void DemoPreview()
{
previewTitle.text = "Select a package on the left to view and import.";
previewVersion.text = "";
previewTags.text = "";
previewDescription.text = "";
previewThumbnail.style.backgroundImage = new StyleBackground();
}
private void AddPackageToList(AlgoliaPackage unityPackage)
{
PackageButton packageButton = new PackageButton();
packageButton.name = unityPackage.Title;
packageButton.PackageLabel.text = unityPackage.Title;
packageButton.clicked += () =>
{
if (selectedButton != null) selectedButton.Select(false);
selectedButton = packageButton;
selectedButton.Select(true);
SelectExample(unityPackage);
};
examplesScrollviewContainer.Add(packageButton);
}
private void SelectExample(AlgoliaPackage unityPackage)
{
if (selectedPackage == unityPackage)
{
return;
}
selectedPackage = unityPackage;
previewTitle.text = unityPackage.Title;
previewVersion.text = $"Version: {unityPackage.Version}";
previewTags.text = $"Tags: {string.Join(',', unityPackage.Tags)}";
previewDescription.text = unityPackage.Description;
string thumbnailPath = DownloadThumbnail(unityPackage.ThumbnailImage);
previewThumbnail.style.backgroundImage = new StyleBackground(LoadTextureFromDisk(thumbnailPath));
// rate-limit event tracking for previewing examples to at most 10 events per minute
DateTime currentTime = DateTime.Now;
TimeSpan elapsedTime = currentTime - lastResetTimeExamplePreviewed;
if (elapsedTime.TotalSeconds > TIME_LIMIT_EXAMPLE_PREVIEWED)
{
eventCounterExamplePreviewed = 0;
lastResetTimeExamplePreviewed = currentTime;
}
if (eventCounterExamplePreviewed < EVENT_LIMIT_EXAMPLE_PREVIEWED)
{
eventCounterExamplePreviewed++;
AnalyticsSDK.ExamplePreviewed(selectedPackage.Title, selectedPackage.Version);
}
}
#endregion
#region Search the Index for Packages
private void UpdatePackagesAsync(string query = "")
{
ShowPackagesForQuery(query);
}
public void Refresh()
{
ShowPackagesForQuery(searchField.value);
}
private void ToggleSearchedButtons(string searchKey)
{
if (string.IsNullOrEmpty(searchKey) || string.IsNullOrWhiteSpace(searchKey))
{
foreach (VisualElement child in examplesScrollviewContainer.Children())
{
child.style.display = DisplayStyle.Flex;
}
}
else
{
foreach (VisualElement child in examplesScrollviewContainer.Children())
{
bool shown = child.name.Contains(searchKey, StringComparison.InvariantCultureIgnoreCase);
child.style.display = shown ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
private void CheckSearch(ChangeEvent<string> evt)
{
ToggleSearchedButtons(evt.newValue);
if (typingCancellation != null)
{
typingCancellation.Cancel();
typingCancellation.Dispose();
}
typingCancellation = new CancellationTokenSource();
DebounceSearch(typingCancellation.Token, evt.newValue);
}
private async Task DebounceSearch(CancellationToken token, string finalText)
{
try
{
await Task.Delay(searchDelayMs, cancellationToken: token);
if (!token.IsCancellationRequested)
{
// User has finished typing
UpdatePackagesAsync(finalText);
}
}
catch (OperationCanceledException)
{
// This block is executed if the delay is cancelled
}
}
private async Task<List<AlgoliaPackage>> FetchPackagesAlgolia(string query = "")
{
SearchClient algolia = new SearchClient(AlgoliaAppKey, AlgoliaSearchKey);
SearchIndex index = algolia.InitIndex(AlgoliaIndexName);
// Construct the embedded list format needed for tag filters
var tagFilters = new List<List<string>>();
if (!ExampleCentralSettings.Data.Instance.ShowEconomyPackages)
{
tagFilters.Add(new List<string>(){"-ce"});
}
// Conduct the search, including tag filters from settings
List<AlgoliaPackage> hits = (await index.SearchAsync<ExampleCentral.Types.Algolia.UnityPackage>(
new Query(query)
{
TagFilters = tagFilters
}
)).Hits;
return hits;
}
private async Task ShowPackagesForQuery(string query = "")
{
List<AlgoliaPackage> packages = await FetchPackagesAlgolia(query);
// Clear the existing list
examplesScrollviewContainer.Clear();
foreach (AlgoliaPackage unityPackage in packages)
{
// Add each package to the list
AddPackageToList(unityPackage);
}
ToggleSearchedButtons(query);
}
#endregion
#region Downloads
private void DownloadSelectedPackage()
{
if (selectedPackage == null) return;
AnalyticsSDK.ExampleDownloaded(selectedPackage.Title, selectedPackage.Version);
DownloadUnityPackage(selectedPackage);
}
private string DownloadThumbnail(string url)
{
// get or create path to store thumbnails, use Local App Data
string thumbnailDir =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VRChat",
"Example Central", "Thumbnails");
Directory.CreateDirectory(thumbnailDir);
string thumbnailPath = Path.Combine(thumbnailDir, Path.GetFileName(url));
// download thumbnail if it doesn't exist
if (!File.Exists(thumbnailPath))
{
WebClient client = new System.Net.WebClient();
client.DownloadFile(url, thumbnailPath);
return thumbnailPath;
}
else
{
return thumbnailPath;
}
}
private void DownloadUnityPackage(AlgoliaPackage unityPackage)
{
string packageDownloadDir =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VRChat",
"Example Central", "Packages");
Directory.CreateDirectory(packageDownloadDir);
string packageDownloadPath = Path.Combine(packageDownloadDir,
$"{unityPackage.Title}-{unityPackage.Version}.unitypackage");
// download file if it doesn't exist
if (!File.Exists(packageDownloadPath))
{
WebClient client = new System.Net.WebClient();
client.DownloadFile(unityPackage.UnityPackageFile, packageDownloadPath);
}
// import the package
AssetDatabase.ImportPackage(packageDownloadPath, true);
}
private Texture2D LoadTextureFromDisk(string path)
{
Texture2D texture = new Texture2D(2, 2);
byte[] fileData = File.ReadAllBytes(path);
texture.LoadImage(fileData);
return texture;
}
#endregion
}
}

View File

@ -0,0 +1,16 @@
fileFormatVersion: 2
guid: 2dd19633aa70e484a94e884e201460a2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- m_ViewDataDictionary: {instanceID: 0}
- VisualTree: {fileID: 9197481963319205126, guid: 850bbb1837d721e48a16d8567e3ecf7b,
type: 3}
- PackageTabButton: {fileID: 9197481963319205126, guid: f4f9892bd4d997b42923c93f4fb8519c,
type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b148a8f35d62fa647b4ba3e93b5a241c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
#container {
margin: 2px 10px;
}
#title-label {
-unity-font-style: bold;
font-size: 18px;
margin-bottom: 10px;
}
#settings-button {
padding: 0;
margin: 0;
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4433d9fd51eebc348aafa2130d7d546b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@ -0,0 +1,40 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<Style src="project://database/Packages/com.vrchat.base/Integrations/VRChat%20Example%20Central/Editor/Example%20Downloader%20Panel/Resources/ExampleDownloaderPanelStyle.uss?fileID=7433441132597879392&amp;guid=2bbc5f61b79c52c4ebcb9fcf6a8a4487&amp;type=3#ExampleDownloaderPanelStyle" />
<ui:VisualElement name="container">
<ui:VisualElement name="go-to-login">
<ui:Button text="Log in to use Example Central" parse-escape-sequences="true" display-tooltip-when-elided="true" name="go-to-login-btn" tooltip="Opens the VRC Control Panel in order to log in and enable Example Central." />
</ui:VisualElement>
<VRC.ExampleCentral.Window.SplitView name="split-main" fixed-pane-index="1" fixed-pane-initial-dimension="500" view-data-key="ExamplePanel-MainView">
<ui:VisualElement name="examples-container">
<ui:VisualElement name="header-examples">
<ui:Label tabindex="-1" text="Search" parse-escape-sequences="true" display-tooltip-when-elided="true" name="examples-label" />
<uie:ToolbarSearchField focusable="true" name="examples-search-field" />
<ui:Button parse-escape-sequences="true" display-tooltip-when-elided="true" name="settings-button" />
</ui:VisualElement>
<ui:ScrollView name="examples-scrollview" horizontal-scroller-visibility="Hidden" vertical-scroller-visibility="Auto">
<ui:VisualElement name="examples-scrollview-container">
<VRC.ExampleCentral.Window.PackageButton style="color: rgb(255, 255, 255);" />
<VRC.ExampleCentral.Window.PackageButton style="color: rgb(255, 255, 255);" />
<VRC.ExampleCentral.Window.PackageButton style="color: rgb(255, 255, 255);" />
<VRC.ExampleCentral.Window.PackageButton style="color: rgb(255, 255, 255);" />
</ui:VisualElement>
</ui:ScrollView>
</ui:VisualElement>
<VRC.ExampleCentral.Window.SplitView name="split-details" orientation="Vertical" fixed-pane-index="0" view-data-key="ExamplePanel-DetailsView" fixed-pane-initial-dimension="200">
<ui:VisualElement name="preview-thumbnail" />
<ui:VisualElement name="preview-settings">
<ui:Label tabindex="-1" text="Examples Thing" parse-escape-sequences="true" display-tooltip-when-elided="true" name="preview-title" />
<ui:ScrollView name="preview-settings-scrollview" horizontal-scroller-visibility="Hidden">
<ui:Label tabindex="-1" text="Version: " parse-escape-sequences="true" display-tooltip-when-elided="true" name="preview-version" />
<ui:Label tabindex="-1" text="Tags: Used, Unused" parse-escape-sequences="true" display-tooltip-when-elided="true" name="preview-tags" />
<ui:Label tabindex="-1" text="Description" parse-escape-sequences="true" display-tooltip-when-elided="true" name="preview-description" style="-unity-font-style: normal; margin-left: 0; margin-right: 0; margin-top: 4px; margin-bottom: 4px; overflow: visible; flex-wrap: nowrap; white-space: normal; text-overflow: ellipsis; color: rgb(255, 255, 255);" />
</ui:ScrollView>
<ui:VisualElement name="preview-options">
<ui:Button text="Import" parse-escape-sequences="true" display-tooltip-when-elided="true" name="preview-download" />
<ui:Button text="Docs" parse-escape-sequences="true" display-tooltip-when-elided="true" name="preview-documentation" />
</ui:VisualElement>
</ui:VisualElement>
</VRC.ExampleCentral.Window.SplitView>
</VRC.ExampleCentral.Window.SplitView>
</ui:VisualElement>
</ui:UXML>

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 850bbb1837d721e48a16d8567e3ecf7b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@ -0,0 +1,212 @@
#container {
flex-direction: row;
flex-grow: 1;
background-color: rgb(31, 31, 31);
}
#go-to-login {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
}
#split-main {
display: none;
margin-left: 16px;
margin-right: 16px;
margin-top: 8px;
margin-bottom: 8px;
}
#split-details {
background-color: rgb(20, 20, 20);
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
margin-left: 16px;
min-width: 155px;
}
#examples-container {
flex-grow: 0;
min-width: 126px;
margin-right: 16px;
}
#header-examples {
flex-direction: row;
margin-bottom: 8px;
height: 24px;
flex-shrink: 0;
}
#examples-label {
-unity-font-style: bold;
font-size: 20px;
margin-right: 16px;
color: rgb(255, 255, 255);
}
#examples-search-field {
flex-grow: 1;
margin-left: 0;
margin-right: 0;
margin-top: 0;
margin-bottom: 0;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
width: 0;
font-size: 14px;
}
#packages-header {
background-color: rgb(41, 41, 41);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
height: 24px;
flex-shrink: 0;
border-left-color: rgb(82, 82, 82);
border-right-color: rgb(82, 82, 82);
border-top-color: rgb(82, 82, 82);
border-bottom-color: rgb(82, 82, 82);
border-bottom-width: 1px;
}
#packages-label {
-unity-font-style: bold;
padding-left: 16px;
padding-right: 4px;
padding-top: 4px;
padding-bottom: 4px;
flex-grow: 1;
-unity-text-align: middle-left;
color: rgb(255, 255, 255);
}
#examples-scrollview {
background-color: rgb(41, 41, 41);
}
#examples-scrollview-container {
background-color: rgb(82, 82, 82);
}
#preview-thumbnail {
height: 144px;
background-color: rgba(89, 89, 89, 0);
flex-shrink: 0;
background-image: none;
-unity-background-scale-mode: scale-to-fit;
min-height: 64px;
}
#preview-settings {
flex-grow: 1;
padding-left: 8px;
padding-right: 8px;
padding-top: 8px;
padding-bottom: 8px;
min-height: 164px;
}
#preview-title {
-unity-font-style: bold;
margin-left: 0;
margin-right: 0;
margin-top: 8px;
margin-bottom: 2px;
font-size: 20px;
white-space: normal;
color: rgb(255, 255, 255);
}
#preview-settings-scrollview {
flex-grow: 1;
}
#preview-version {
-unity-font-style: normal;
flex-grow: 0;
margin-left: 0;
margin-right: 0;
margin-top: 2px;
margin-bottom: 4px;
color: rgb(255, 255, 255);
font-size: 10px;
white-space: normal;
}
#preview-tags {
-unity-font-style: normal;
flex-grow: 0;
margin-left: 0;
margin-right: 0;
margin-top: 2px;
margin-bottom: 4px;
color: rgb(255, 255, 255);
font-size: 10px;
white-space: normal;
}
#preview-options {
flex-direction: row;
height: 32px;
flex-shrink: 0;
min-height: 32px;
margin-top: 8px;
}
#preview-download {
flex-grow: 1;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
margin-left: 0;
margin-right: 0;
margin-top: 0;
margin-bottom: 0;
width: auto;
background-color: rgb(15, 145, 169);
-unity-font-style: bold;
font-size: 14px;
border-left-color: rgb(48, 48, 48);
border-right-color: rgb(48, 48, 48);
border-top-color: rgb(48, 48, 48);
border-bottom-color: rgb(36, 36, 36);
color: rgb(238, 238, 238);
}
#preview-documentation {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
margin-left: 6px;
margin-right: 0;
margin-top: 0;
margin-bottom: 0;
flex-grow: 0;
background-color: rgb(20, 20, 20);
-unity-font-style: bold;
padding-left: 16px;
padding-right: 16px;
font-size: 14px;
color: rgb(255, 255, 255);
border-left-color: rgb(48, 48, 48);
border-right-color: rgb(48, 48, 48);
border-top-color: rgb(48, 48, 48);
border-bottom-color: rgb(36, 36, 36);
}
#settings-button {
background-image: none;
flex-basis: 24px;
width: 24px;
height: 24px;
-unity-background-scale-mode: scale-to-fit;
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2bbc5f61b79c52c4ebcb9fcf6a8a4487
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@ -0,0 +1,7 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<Style src="project://database/Packages/com.vrchat.base/Integrations/VRChat%20Sample%20Central/Editor/Sample%20Downloader%20Panel/Resources/PackageTabButtonStyle.uss?fileID=7433441132597879392&amp;guid=7d25b279c57e51341bb553313ea74fc0&amp;type=3#PackageTabButtonStyle" />
<ui:VisualElement name="container">
<ui:VisualElement name="selection-element" />
<ui:Label tabindex="-1" text="Label" parse-escape-sequences="true" display-tooltip-when-elided="true" />
</ui:VisualElement>
</ui:UXML>

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f4f9892bd4d997b42923c93f4fb8519c
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@ -0,0 +1,28 @@
#container {
flex-grow: 0;
flex-shrink: 0;
height: 40px;
background-color: rgb(41, 41, 41);
margin-bottom: 1px;
}
#selection-element {
flex-grow: 1;
position: absolute;
top: 0;
right: 0;
bottom: 0;
background-color: rgb(56, 56, 56);
display: none;
left: 0;
}
#container Label {
flex-grow: 1;
-unity-text-align: middle-left;
margin-left: 16px;
font-size: 14px;
-unity-font-style: bold;
color: rgb(255, 255, 255);
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d25b279c57e51341bb553313ea74fc0
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

View File

@ -0,0 +1,140 @@
fileFormatVersion: 2
guid: 6d947179418dad34d9759416f16501d0
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,55 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace VRC.ExampleCentral.Window
{
public class SplitView : TwoPaneSplitView
{
public new class UxmlFactory : UxmlFactory<SplitView, UxmlTraits> { }
}
public class PackageButton : VisualElement
{
public new class UxmlFactory : UxmlFactory<PackageButton, UxmlTraits> { }
public Label PackageLabel;
private VisualElement selectionElement;
private Clickable clickable;
/// <summary>
/// Called when Package Button is clicked.
/// </summary>
public event Action clicked
{
add { if (clickable != null) clickable.clicked += value; }
remove { if (clickable != null) clickable.clicked -= value; }
}
public PackageButton()
{
VisualTreeAsset uxml = Resources.Load<VisualTreeAsset>("PackageTabButton");
uxml.CloneTree(this);
PackageLabel = this.Query<Label>();
selectionElement = this.Query<VisualElement>("selection-element");
clickable = new Clickable(OnClicked);
this.AddManipulator(clickable);
}
private void OnClicked(EventBase obj)
{
Select(true);
}
public void Select(bool value)
{
selectionElement.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 67c2bdfc395e77b4abdc8b6e5c51653d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1a183835343d1e7448143d7227e65054
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace VRC.ExampleCentral.Editor
{
/// <summary>
/// Stores the GUID and Hash for each Asset in an Example
/// </summary>
[Serializable]
public class AssetInfo
{
public string GUID { get; private set; }
public string Hash { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="AssetInfo"/> class with the specified GUID and hash.
/// Used for deserialization, after the hash is computed.
/// </summary>
/// <param name="guid">The GUID of the asset.</param>
/// <param name="hash">The hash of the asset.</param>
public AssetInfo(string guid, string hash)
{
GUID = guid;
Hash = hash;
}
/// <summary>
/// Initializes a new instance of the <see cref="AssetInfo"/> class with the specified GUID.
/// The hash is computed based on the asset's content.
/// </summary>
/// <param name="guid">The GUID of the asset.</param>
public AssetInfo(string guid)
{
GUID = guid;
Hash = ComputeAssetHash(guid);
}
/// <summary>
/// Computes the hash of the asset based on its content.
/// </summary>
/// <param name="guid">The GUID of the asset.</param>
/// <returns>The computed hash of the asset.</returns>
private string ComputeAssetHash(string guid)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
string fullPath = Path.Combine(Application.dataPath, assetPath.Substring("Assets/".Length));
if (!File.Exists(fullPath))
return "File Not Found";
try
{
using FileStream stream = File.OpenRead(fullPath);
using SHA256 sha = SHA256.Create();
byte[] hashBytes = sha.ComputeHash(stream);
StringBuilder sb = new StringBuilder();
foreach (byte b in hashBytes)
sb.Append(b.ToString("X2"));
return sb.ToString();
}
catch (Exception ex)
{
Debug.LogError($"Error computing hash for {assetPath}: {ex.Message}");
return "Error";
}
}
/// <summary>
/// Serializes the asset to a string, using a pipe character as a separator.
/// </summary>
/// <returns>A string representation of the asset's guid and hash.</returns>
public string Serialize()
{
return $"{GUID}|{Hash}";
}
/// <summary>
/// Deserializes the asset information from a string.
/// </summary>
/// <param name="serializedData">The serialized data string.</param>
/// <returns>An instance of <see cref="AssetInfo"/>.</returns>
/// <exception cref="ArgumentException">Thrown when the serialized data format is invalid.</exception>
public static AssetInfo Deserialize(string serializedData)
{
var parts = serializedData.Split('|');
if (parts.Length != 2)
throw new ArgumentException("Invalid serialized data format");
return new AssetInfo(parts[0], parts[1]);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d95aa121fc5f5848b1a4a6b1eae7299
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,27 @@
using System;
namespace VRC.ExampleCentral.Editor
{
/// <summary>
/// Stores a description of changes made to an Example
/// </summary>
[Serializable]
public class Changes
{
/// <summary>
/// SemVer-compatible version number (SemVer not currently enforced anywhere)
/// </summary>
public string Version;
/// <summary>
/// Plain text description of changes
/// </summary>
public string Value;
public Changes(string version, string value)
{
Version = version;
Value = value;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 305cd65774c510c409011962a5752a51
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace VRC.ExampleCentral.Editor
{
public enum ExampleStatus
{
Local,
Draft,
Published
}
[Serializable]
public class ExampleData : ScriptableObject, ISerializationCallbackReceiver
{
[SerializeField]
public string author;
[SerializeField]
public string title;
[SerializeField]
public Texture2D thumbnail;
[SerializeField]
public string description;
[SerializeField]
public string version;
[SerializeField]
public string documentationLink;
[SerializeField]
public string vrcWorldId;
[SerializeField]
public List<string> tags = new();
[SerializeField]
public SceneAsset exampleScene;
[SerializeField]
private List<string> serializedAssets;
public List<AssetInfo> assets = new();
[SerializeField]
public string sanityId = $"drafts.{Guid.NewGuid().ToString()}";
[SerializeField]
public ExampleStatus ExampleStatus = ExampleStatus.Local;
[SerializeField]
public List<Changes> changes = new();
// Serialize the AssetInfo list to a list of strings
public void OnBeforeSerialize()
{
serializedAssets = assets.ConvertAll(asset => asset.Serialize());
}
// Deserialize the List<string> back to the AssetInfo list
public void OnAfterDeserialize()
{
assets = serializedAssets.ConvertAll(AssetInfo.Deserialize);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 02be9066d74f7884ab56988f40f56297
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
{
"name": "VRC.ExampleCentral.Editor",
"rootNamespace": "",
"references": [
"UniTask",
"VRC.SDKBase.Editor"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b0ff9a3db60cc184381fafd0c1e6b375
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: