Added Unity project files
This commit is contained in:
@ -0,0 +1,968 @@
|
||||
#if VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UIElements;
|
||||
using VRC.SDK3.ClientSim.Persistence;
|
||||
using VRC.SDK3.Persistence;
|
||||
using VRC.SDKBase;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace VRC.SDK3.ClientSim.Editor
|
||||
{
|
||||
public class ClientSimPlayerDataWindow : EditorWindow
|
||||
{
|
||||
// max number of characters to display PlayerData values with (value strings longer than this are truncated)
|
||||
private const int MAX_LENGTH_SINGLE_LINE = 50;
|
||||
private const int MAX_LENGTH_MULTI_LINE = 10000;
|
||||
|
||||
// number of PlayerData elements to show per page
|
||||
private const int PAGE_SIZE = 12;
|
||||
|
||||
// key used to store selected sort mode in player prefs
|
||||
private const string SORT_MODE_KEY = "PlayerDataSortMode";
|
||||
|
||||
private bool hasLocalPlayerData;
|
||||
private bool isLocalPlayerSelected = true;
|
||||
private string localPlayerName;
|
||||
private int page;
|
||||
private int numPages;
|
||||
|
||||
private Dictionary<string, ClientSimPlayerDataPair> localPlayerData;
|
||||
private IClientSimEventDispatcher eventDispatcher;
|
||||
private VisualElement labelContainer;
|
||||
private VisualElement noDataInfoContainer;
|
||||
private VisualElement pagingContainer;
|
||||
private Label pageLabel;
|
||||
private readonly List<VisualElement> elementsToDisableInPlayMode = new();
|
||||
private DropdownField playerDropdown;
|
||||
private DropdownField sortDropdown;
|
||||
private Button addRemotePlayerTestDataButton;
|
||||
private Button nextPageButton;
|
||||
private Button prevPageButton;
|
||||
private Button clearDataButton;
|
||||
|
||||
private static string LocalPlayerDataRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
string root = Path.GetDirectoryName(Application.dataPath);
|
||||
string path = Path.Combine(root, ClientSimPlayerDataStorage.PlayerDataFolder);
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
private static string LocalPlayerDataPath => LocalPlayerDataRoot + $"/PlayerData_1_{SceneManager.GetActiveScene().name}.json";
|
||||
private static string LocalDataPathPattern => $"PlayerData_*_{SceneManager.GetActiveScene().name}.json";
|
||||
|
||||
[MenuItem("VRChat SDK/ClientSim PlayerData", false, 1500)]
|
||||
public static void Init()
|
||||
{
|
||||
var window = GetWindow<ClientSimPlayerDataWindow>(false, "ClientSim PlayerData");
|
||||
window.minSize = new Vector2(400, 400);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private static void SetData(ClientSimPlayerDataPair data, VRCPlayerApi player)
|
||||
{
|
||||
var playerData = player.GetClientSimPlayer().PlayerDataObject;
|
||||
switch (data.Value.Type)
|
||||
{
|
||||
case ClientSimPlayerDataType.Vector2:
|
||||
playerData.SetVector2(data.Key, data.Value.AsVector2());
|
||||
break;
|
||||
case ClientSimPlayerDataType.Vector3:
|
||||
playerData.SetVector3(data.Key, data.Value.AsVector3());
|
||||
break;
|
||||
case ClientSimPlayerDataType.Vector4:
|
||||
playerData.SetVector4(data.Key, data.Value.AsVector4());
|
||||
break;
|
||||
case ClientSimPlayerDataType.Quaternion:
|
||||
playerData.SetQuaternion(data.Key, data.Value.AsQuaternion());
|
||||
break;
|
||||
case ClientSimPlayerDataType.Color:
|
||||
playerData.SetColor(data.Key, data.Value.AsColor());
|
||||
break;
|
||||
case ClientSimPlayerDataType.Color32:
|
||||
playerData.SetColor32(data.Key, data.Value.AsColor32());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedString:
|
||||
playerData.SetString(data.Key, data.Value.AsWrappedString());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedShort:
|
||||
playerData.SetShort(data.Key, data.Value.AsWrappedShort());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedInt:
|
||||
playerData.SetInt(data.Key, data.Value.AsWrappedInt());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedFloat:
|
||||
playerData.SetFloat(data.Key, data.Value.AsWrappedFloat());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedBool:
|
||||
playerData.SetBool(data.Key, data.Value.AsWrappedBool());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedByte:
|
||||
playerData.SetSByte(data.Key, data.Value.AsWrappedSByte());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedUByte:
|
||||
playerData.SetByte(data.Key, data.Value.AsWrappedUByte());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedBytes:
|
||||
playerData.SetBytes(data.Key, data.Value.AsWrappedBytes());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedUShort:
|
||||
playerData.SetUShort(data.Key, data.Value.AsWrappedUShort());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedUInt:
|
||||
playerData.SetUInt(data.Key, data.Value.AsWrappedUInt());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedULong:
|
||||
playerData.SetULong(data.Key, data.Value.AsWrappedULong());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedDouble:
|
||||
playerData.SetDouble(data.Key, data.Value.AsWrappedDouble());
|
||||
break;
|
||||
case ClientSimPlayerDataType.WrappedLong:
|
||||
playerData.SetLong(data.Key, data.Value.AsWrappedLong());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnEnable()
|
||||
{
|
||||
VisualElement root = rootVisualElement;
|
||||
VisualTreeAsset visualTree = Resources.Load<VisualTreeAsset>(nameof(ClientSimPlayerDataWindow));
|
||||
root.Add(visualTree.CloneTree());
|
||||
|
||||
Button openDataFolderButton = root.Q<Button>("OpenDataFolderButton");
|
||||
Button refreshDataButton = root.Q<Button>("RefreshDataButton");
|
||||
clearDataButton = root.Q<Button>("ClearDataButton");
|
||||
nextPageButton = root.Q<Button>("NextPageButton");
|
||||
prevPageButton = root.Q<Button>("PreviousPageButton");
|
||||
addRemotePlayerTestDataButton = root.Q<Button>("RandomizeButton");
|
||||
playerDropdown = root.Q<DropdownField>("PlayerDropdown");
|
||||
sortDropdown = root.Q<DropdownField>("SortDropdown");
|
||||
noDataInfoContainer = root.Q<VisualElement>("NoDataInfo");
|
||||
pagingContainer = root.Q<VisualElement>("Paging");
|
||||
pageLabel = root.Q<Label>("PageLabel");
|
||||
ScrollView playerDataList = root.Q<ScrollView>("PlayerDataList");
|
||||
|
||||
labelContainer = new VisualElement();
|
||||
playerDataList.Add(labelContainer);
|
||||
|
||||
openDataFolderButton.clicked += OpenLocalDataDirectory;
|
||||
refreshDataButton.clicked += RefreshPlayerData;
|
||||
clearDataButton.clicked += ClearPlayerData;
|
||||
nextPageButton.clicked += NextPage;
|
||||
prevPageButton.clicked += PreviousPage;
|
||||
addRemotePlayerTestDataButton.clicked += AddRemotePlayerTestData;
|
||||
|
||||
// dropdown always includes local player so that users can view PlayerData outside of play mode
|
||||
CheckLocalPlayerName();
|
||||
playerDropdown.choices = new List<string> { localPlayerName };
|
||||
playerDropdown.index = 0;
|
||||
playerDropdown.RegisterValueChangedCallback(OnPlayerSelected);
|
||||
|
||||
// load sort mode from player prefs - it will be applied in LoadPlayerData
|
||||
sortDropdown.SetValueWithoutNotify(PlayerPrefs.GetString(SORT_MODE_KEY, "Alphabetically"));
|
||||
sortDropdown.RegisterValueChangedCallback(OnSortSelected);
|
||||
|
||||
LoadPlayerData();
|
||||
UpdatePage(false);
|
||||
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
if (EditorApplication.isPlaying)
|
||||
{
|
||||
OnPlayModeStateChanged(PlayModeStateChange.EnteredPlayMode);
|
||||
}
|
||||
|
||||
// reload data on scene switch
|
||||
EditorSceneManager.sceneOpened += (_, _) => LoadPlayerData();
|
||||
}
|
||||
|
||||
private void AddRemotePlayerTestData()
|
||||
{
|
||||
object RandomizePlayerDataValue(ClientSimPlayerDataTypeUnion dataPoint)
|
||||
{
|
||||
return dataPoint.Type switch
|
||||
{
|
||||
ClientSimPlayerDataType.Vector2 => new Vector2(Random.Range(0,100), Random.Range(0,100)),
|
||||
ClientSimPlayerDataType.Vector3 => new Vector3(Random.Range(0,100), Random.Range(0,100), Random.Range(0,100)),
|
||||
ClientSimPlayerDataType.Vector4 => new Vector4(Random.Range(0,100), Random.Range(0,100), Random.Range(0,100), Random.Range(0,100)),
|
||||
ClientSimPlayerDataType.Quaternion => new Quaternion(Random.Range(0,100), Random.Range(0,100), Random.Range(0,100), Random.Range(0,100)),
|
||||
ClientSimPlayerDataType.Color => new Color(Random.Range(0,1f), Random.Range(0,1f), Random.Range(0,1f), Random.Range(0,1f)),
|
||||
ClientSimPlayerDataType.Color32 => (Color32)new Color(Random.Range(0,1f), Random.Range(0,1f), Random.Range(0,1f), Random.Range(0,1f)),
|
||||
ClientSimPlayerDataType.WrappedString => MakeRandomString(),
|
||||
ClientSimPlayerDataType.WrappedShort => (short)Random.Range(-10, 10),
|
||||
ClientSimPlayerDataType.WrappedInt => Random.Range(-100, 100),
|
||||
ClientSimPlayerDataType.WrappedFloat => Random.Range(0, 1f),
|
||||
ClientSimPlayerDataType.WrappedBool => Random.Range(0, 2) != 0,
|
||||
ClientSimPlayerDataType.WrappedByte => (sbyte)Random.Range(sbyte.MinValue, sbyte.MaxValue),
|
||||
ClientSimPlayerDataType.WrappedUByte => (byte)Random.Range(byte.MinValue, byte.MaxValue),
|
||||
ClientSimPlayerDataType.WrappedBytes => MakeRandomByteArray(),
|
||||
ClientSimPlayerDataType.WrappedUShort => (ushort)Random.Range(0, 10),
|
||||
ClientSimPlayerDataType.WrappedUInt => (uint)Random.Range(0, 100),
|
||||
ClientSimPlayerDataType.WrappedULong => (ulong)Random.Range(0, 1000),
|
||||
ClientSimPlayerDataType.WrappedDouble => (double)Random.Range(0, 0.1f),
|
||||
ClientSimPlayerDataType.WrappedLong => (long)Random.Range(-1000, 1000),
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
string MakeRandomString()
|
||||
{
|
||||
const string characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
string result = "";
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
result += characters[Random.Range(0, characters.Length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
byte[] MakeRandomByteArray()
|
||||
{
|
||||
byte[] randomBytes = new byte[2];
|
||||
for (int i = 0; i < randomBytes.Length; i++)
|
||||
{
|
||||
randomBytes[i] = (byte)Random.Range(byte.MinValue, byte.MaxValue + 1);
|
||||
}
|
||||
return randomBytes;
|
||||
}
|
||||
|
||||
string playerName = playerDropdown.value;
|
||||
VRCPlayerApi player = VRCPlayerApi.AllPlayers.Find(p => p.displayName.Equals(playerName));
|
||||
if (player.isLocal) return;
|
||||
|
||||
foreach (var kvp in localPlayerData)
|
||||
{
|
||||
var randomizedValue = RandomizePlayerDataValue(kvp.Value.Value);
|
||||
var typeUnion = new ClientSimPlayerDataTypeUnion
|
||||
{
|
||||
Type = kvp.Value.Value.Type,
|
||||
Value = randomizedValue
|
||||
};
|
||||
var dataPair = new ClientSimPlayerDataPair
|
||||
{
|
||||
Key = kvp.Key,
|
||||
Value = typeUnion
|
||||
};
|
||||
SetData(dataPair, player);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerSelected(ChangeEvent<string> playerSelectedEvent)
|
||||
{
|
||||
string selectedPlayerName = playerSelectedEvent.newValue;
|
||||
bool isLocalPlayer = string.Equals(selectedPlayerName, localPlayerName);
|
||||
addRemotePlayerTestDataButton.style.display = isLocalPlayer || !hasLocalPlayerData
|
||||
? DisplayStyle.None
|
||||
: DisplayStyle.Flex;
|
||||
|
||||
isLocalPlayerSelected = isLocalPlayer;
|
||||
if (isLocalPlayer)
|
||||
{
|
||||
LoadPlayerData(Networking.LocalPlayer);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (VRCPlayerApi player in VRCPlayerApi.AllPlayers)
|
||||
{
|
||||
if (player.displayName.Equals(selectedPlayerName))
|
||||
{
|
||||
LoadPlayerData(player);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSortSelected(ChangeEvent<string> evt)
|
||||
{
|
||||
PlayerPrefs.SetString(SORT_MODE_KEY, evt.newValue);
|
||||
LoadPlayerDataForSelectedPlayer();
|
||||
}
|
||||
|
||||
private void LoadPlayerDataForSelectedPlayer()
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
LoadPlayerData();
|
||||
return;
|
||||
}
|
||||
|
||||
string selectedPlayerName = playerDropdown.value;
|
||||
foreach (VRCPlayerApi player in VRCPlayerApi.AllPlayers)
|
||||
{
|
||||
if (player.displayName.Equals(selectedPlayerName))
|
||||
{
|
||||
LoadPlayerData(player);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||||
}
|
||||
|
||||
private void LoadPlayerData(VRCPlayerApi player = null, bool broadcastPlayerDataUpdated = false)
|
||||
{
|
||||
// outside of play mode, we don't have a player game object
|
||||
string path = player == null ? LocalPlayerDataPath : ClientSimPlayerDataStorage.PlayerDataFilePath(player);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
File.WriteAllText(path, "{}");
|
||||
CheckDataEmpty();
|
||||
}
|
||||
else
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
var data = JsonConvert.DeserializeObject<Dictionary<string, ClientSimPlayerDataPair>>(json);
|
||||
UpdatePlayerDataList(data, player);
|
||||
|
||||
// when adding test data for a remote player, let the rest of the system know
|
||||
if (EditorApplication.isPlaying && broadcastPlayerDataUpdated)
|
||||
{
|
||||
var infos = data
|
||||
.Select(kvp => new PlayerData.Info(kvp.Key, PlayerData.State.Added))
|
||||
.ToArray();
|
||||
|
||||
player.GetClientSimPlayer().PlayerDataObject.QueuePlayerDataUpdate(infos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayModeStateChanged(PlayModeStateChange state)
|
||||
{
|
||||
if (!ClientSimMain.TryGetInstance(out var instance)) return;
|
||||
|
||||
if (state == PlayModeStateChange.EnteredPlayMode)
|
||||
{
|
||||
eventDispatcher = instance.GetEventDispatcher();
|
||||
eventDispatcher.Subscribe<ClientSimOnPlayerRestoredEvent>(OnPlayerRestored);
|
||||
eventDispatcher.Subscribe<ClientSimOnPlayerDataUpdatedEvent>(OnPlayerDataUpdated);
|
||||
eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
|
||||
eventDispatcher.Subscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
|
||||
|
||||
CheckLocalPlayerName();
|
||||
playerDropdown.choices[0] = localPlayerName;
|
||||
playerDropdown.value = localPlayerName;
|
||||
|
||||
elementsToDisableInPlayMode.ForEach(e => e.SetEnabled(false));
|
||||
}
|
||||
else if (state == PlayModeStateChange.ExitingPlayMode)
|
||||
{
|
||||
playerDropdown.choices.RemoveRange(1, playerDropdown.choices.Count - 1);
|
||||
playerDropdown.index = 0;
|
||||
|
||||
eventDispatcher?.Unsubscribe<ClientSimOnPlayerRestoredEvent>(OnPlayerRestored);
|
||||
eventDispatcher?.Unsubscribe<ClientSimOnPlayerDataUpdatedEvent>(OnPlayerDataUpdated);
|
||||
eventDispatcher?.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
|
||||
eventDispatcher?.Unsubscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
|
||||
eventDispatcher = null;
|
||||
|
||||
elementsToDisableInPlayMode.ForEach(e => e.SetEnabled(true));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerRestored(ClientSimOnPlayerRestoredEvent payload)
|
||||
{
|
||||
LoadPlayerData(payload.player);
|
||||
}
|
||||
|
||||
private void OnPlayerDataUpdated(ClientSimOnPlayerDataUpdatedEvent payload)
|
||||
{
|
||||
UpdatePlayerDataList(payload.playerData, payload.player);
|
||||
}
|
||||
|
||||
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent payload)
|
||||
{
|
||||
// local player is initialized via ClientSim init flow
|
||||
if (payload.player.isLocal) return;
|
||||
|
||||
if (!playerDropdown.choices.Contains(payload.player.displayName))
|
||||
{
|
||||
playerDropdown.choices.Add(payload.player.displayName);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerLeft(ClientSimOnPlayerLeftEvent payload)
|
||||
{
|
||||
if (payload.player.isLocal) return;
|
||||
if (playerDropdown.choices.Contains(payload.player.displayName))
|
||||
{
|
||||
playerDropdown.choices.Remove(payload.player.displayName);
|
||||
}
|
||||
|
||||
if (playerDropdown.value.Equals(payload.player.displayName))
|
||||
{
|
||||
playerDropdown.index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePlayerDataList(Dictionary<string, ClientSimPlayerDataPair> playerData, VRCPlayerApi player = null, bool redraw = true)
|
||||
{
|
||||
numPages = playerData.Count % PAGE_SIZE == 0
|
||||
? playerData.Count / PAGE_SIZE
|
||||
: playerData.Count / PAGE_SIZE + 1;
|
||||
UpdatePage(false);
|
||||
|
||||
if (player != null)
|
||||
{
|
||||
// show button for adding test data if local player has data and a remote player is selected
|
||||
if (player.isLocal)
|
||||
{
|
||||
localPlayerData = playerData;
|
||||
if (!hasLocalPlayerData && localPlayerData.Count > 0)
|
||||
{
|
||||
hasLocalPlayerData = true;
|
||||
if (!isLocalPlayerSelected)
|
||||
{
|
||||
addRemotePlayerTestDataButton.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only update display if data is updated for currently selected player
|
||||
if (!string.Equals(player.displayName, playerDropdown.value)) return;
|
||||
}
|
||||
|
||||
if (!redraw || CheckDataEmpty(playerData.Count))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sorted = sortDropdown.value switch
|
||||
{
|
||||
// sorts by key
|
||||
"Alphabetically" => playerData.OrderBy(x => x.Key).ToList(),
|
||||
|
||||
// sorts by what keys were most recently set
|
||||
"Last Updated" => playerData
|
||||
.OrderBy(x => x.Value.LastUpdated)
|
||||
.ThenBy(x => x.Key)
|
||||
.Reverse()
|
||||
.ToList(),
|
||||
|
||||
// sorts data as shown in the JSON
|
||||
_ => playerData.ToList()
|
||||
|
||||
};
|
||||
|
||||
// color fields don't have an isDelayed property, so we disable them in play mode
|
||||
elementsToDisableInPlayMode.Clear();
|
||||
|
||||
int pageIndex = 0;
|
||||
int labelIndex = 0;
|
||||
foreach (var kvp in sorted)
|
||||
{
|
||||
// paging
|
||||
if (labelIndex >= PAGE_SIZE)
|
||||
{
|
||||
labelIndex = 0;
|
||||
pageIndex++;
|
||||
}
|
||||
labelIndex++;
|
||||
if (pageIndex != page) continue;
|
||||
|
||||
VisualElement valueField;
|
||||
switch (kvp.Value.Value.Type)
|
||||
{
|
||||
case ClientSimPlayerDataType.Vector2:
|
||||
Vector2Field vector2Field = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsVector2()
|
||||
};
|
||||
SetIsDelayedForCompositeField(vector2Field);
|
||||
vector2Field.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = vector2Field;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.Vector3:
|
||||
Vector3Field vector3Field = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsVector3()
|
||||
};
|
||||
SetIsDelayedForCompositeField(vector3Field);
|
||||
vector3Field.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = vector3Field;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.Vector4:
|
||||
Vector4Field vector4Field = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsVector4()
|
||||
};
|
||||
SetIsDelayedForCompositeField(vector4Field);
|
||||
vector4Field.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = vector4Field;
|
||||
break;
|
||||
|
||||
// show quaternions as euler angles, so they're more intuitive to edit
|
||||
case ClientSimPlayerDataType.Quaternion:
|
||||
Vector3Field quaternionField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsQuaternion().eulerAngles
|
||||
};
|
||||
SetIsDelayedForCompositeField(quaternionField);
|
||||
quaternionField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = quaternionField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.Color:
|
||||
ColorField colorField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsColor()
|
||||
};
|
||||
colorField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
colorField.SetEnabled(!EditorApplication.isPlaying);
|
||||
elementsToDisableInPlayMode.Add(colorField);
|
||||
valueField = colorField;
|
||||
break;
|
||||
|
||||
// color32 is shown as a regular color field
|
||||
case ClientSimPlayerDataType.Color32:
|
||||
ColorField color32Field = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsColor32()
|
||||
};
|
||||
color32Field.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
color32Field.SetEnabled(!EditorApplication.isPlaying);
|
||||
elementsToDisableInPlayMode.Add(color32Field);
|
||||
valueField = color32Field;
|
||||
break;
|
||||
|
||||
// string fields are multiline
|
||||
case ClientSimPlayerDataType.WrappedString:
|
||||
TextField textField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = Truncate(true, kvp.Value.Value.AsWrappedString()),
|
||||
isDelayed = true,
|
||||
multiline = true,
|
||||
maxLength = MAX_LENGTH_MULTI_LINE
|
||||
};
|
||||
textField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = textField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedShort:
|
||||
IntegerField shortField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedShort(),
|
||||
isDelayed = true
|
||||
};
|
||||
shortField.RegisterValueChangedCallback(e =>
|
||||
{
|
||||
if (e.newValue is >= short.MinValue and <= short.MaxValue)
|
||||
{
|
||||
ValueFieldCallback(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
shortField.SetValueWithoutNotify(e.previousValue);
|
||||
}
|
||||
});
|
||||
valueField = shortField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedInt:
|
||||
IntegerField intField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedInt(),
|
||||
isDelayed = true
|
||||
};
|
||||
intField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = intField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedFloat:
|
||||
FloatField floatField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedFloat(),
|
||||
isDelayed = true
|
||||
};
|
||||
floatField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = floatField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedBool:
|
||||
Toggle toggle = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedBool()
|
||||
};
|
||||
toggle.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = toggle;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedByte:
|
||||
IntegerField sbyteField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedSByte(),
|
||||
isDelayed = true
|
||||
};
|
||||
sbyteField.RegisterValueChangedCallback(e =>
|
||||
{
|
||||
if (e.newValue is >= sbyte.MinValue and <= sbyte.MaxValue)
|
||||
{
|
||||
ValueFieldCallback(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
sbyteField.SetValueWithoutNotify(e.previousValue);
|
||||
}
|
||||
});
|
||||
valueField = sbyteField;
|
||||
break;
|
||||
|
||||
// byte arrays are readonly to avoid confusion about encoding and format
|
||||
// users can still manually set byte array values in the JSON if needed
|
||||
case ClientSimPlayerDataType.WrappedBytes:
|
||||
var byteArray = kvp.Value.Value.AsWrappedBytes();
|
||||
string readableValue = byteArray.Aggregate("[ ", (current, b) => current + $"{(int)b} ") + "]";
|
||||
TextField bytesField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = Truncate(false, readableValue),
|
||||
isDelayed = true,
|
||||
maxLength = MAX_LENGTH_SINGLE_LINE
|
||||
};
|
||||
bytesField.SetEnabled(false);
|
||||
valueField = bytesField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedUShort:
|
||||
UnsignedIntegerField ushortField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedUShort(),
|
||||
isDelayed = true,
|
||||
};
|
||||
ushortField.RegisterValueChangedCallback(e =>
|
||||
{
|
||||
if (e.newValue <= ushort.MaxValue)
|
||||
{
|
||||
ValueFieldCallback(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
ushortField.SetValueWithoutNotify(e.previousValue);
|
||||
}
|
||||
});
|
||||
valueField = ushortField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedUByte:
|
||||
UnsignedIntegerField byteField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedUByte(),
|
||||
isDelayed = true,
|
||||
};
|
||||
byteField.RegisterValueChangedCallback(e =>
|
||||
{
|
||||
if (e.newValue <= byte.MaxValue)
|
||||
{
|
||||
ValueFieldCallback(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
byteField.SetValueWithoutNotify(e.previousValue);
|
||||
}
|
||||
});
|
||||
valueField = byteField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedUInt:
|
||||
UnsignedIntegerField uintField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedUInt(),
|
||||
isDelayed = true,
|
||||
};
|
||||
uintField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = uintField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedULong:
|
||||
UnsignedLongField ulongField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedULong(),
|
||||
isDelayed = true,
|
||||
};
|
||||
ulongField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = ulongField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedDouble:
|
||||
DoubleField doubleField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedDouble(),
|
||||
isDelayed = true,
|
||||
};
|
||||
doubleField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = doubleField;
|
||||
break;
|
||||
|
||||
case ClientSimPlayerDataType.WrappedLong:
|
||||
LongField longField = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = kvp.Value.Value.AsWrappedLong(),
|
||||
isDelayed = true,
|
||||
};
|
||||
longField.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = longField;
|
||||
break;
|
||||
|
||||
default:
|
||||
TextField field = new()
|
||||
{
|
||||
label = kvp.Key,
|
||||
value = Truncate(false, kvp.Value.Value.Value.ToString()),
|
||||
isDelayed = true,
|
||||
maxLength = MAX_LENGTH_SINGLE_LINE
|
||||
};
|
||||
field.RegisterValueChangedCallback(ValueFieldCallback);
|
||||
valueField = field;
|
||||
break;
|
||||
}
|
||||
|
||||
labelContainer.Add(valueField);
|
||||
continue;
|
||||
|
||||
// truncate a value
|
||||
string Truncate(bool isMultiline, string value)
|
||||
{
|
||||
const string end = "... (truncated)";
|
||||
int maxLength = isMultiline ? MAX_LENGTH_MULTI_LINE : MAX_LENGTH_SINGLE_LINE;
|
||||
return value.Length > maxLength
|
||||
? $"{value.Truncate(maxLength - end.Length)}{end}"
|
||||
: value;
|
||||
}
|
||||
|
||||
// in play mode, set value through PlayerData interface
|
||||
// in edit mode, update the JSON directly
|
||||
void ValueFieldCallback<T>(ChangeEvent<T> evt)
|
||||
{
|
||||
ClientSimPlayerDataTypeUnion typeUnion = new ClientSimPlayerDataTypeUnion
|
||||
{
|
||||
Type = kvp.Value.Value.Type,
|
||||
Value = evt.newValue
|
||||
};
|
||||
|
||||
// quaternions are displayed as Vector3 eulers
|
||||
if (kvp.Value.Value.Type == ClientSimPlayerDataType.Quaternion && evt.newValue is Vector3 eulers)
|
||||
{
|
||||
typeUnion.Value = Quaternion.Euler(eulers);
|
||||
}
|
||||
|
||||
// color32s are displayed as colors
|
||||
if (kvp.Value.Value.Type == ClientSimPlayerDataType.Color32 && evt.newValue is Color color)
|
||||
{
|
||||
typeUnion.Value = (Color32)color;
|
||||
}
|
||||
|
||||
// bytes are displayed ints
|
||||
if (kvp.Value.Value.Type == ClientSimPlayerDataType.WrappedUByte)
|
||||
{
|
||||
typeUnion.Value = Convert.ToByte(evt.newValue);
|
||||
}
|
||||
if (kvp.Value.Value.Type == ClientSimPlayerDataType.WrappedByte)
|
||||
{
|
||||
typeUnion.Value = Convert.ToSByte(evt.newValue);
|
||||
}
|
||||
|
||||
ClientSimPlayerDataPair dataPair = new() { Key = kvp.Key, Value = typeUnion };
|
||||
|
||||
if (EditorApplication.isPlaying)
|
||||
{
|
||||
SetData(dataPair, player);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
string json = File.ReadAllText(LocalPlayerDataPath);
|
||||
var data = JsonConvert.DeserializeObject<Dictionary<string, ClientSimPlayerDataPair>>(json);
|
||||
data[kvp.Key] = dataPair;
|
||||
json = JsonConvert.SerializeObject(data, Formatting.Indented);
|
||||
File.WriteAllText(LocalPlayerDataPath, json);
|
||||
|
||||
// After serializing the new value, reload it into ClientSim PlayerData and refresh the UI.
|
||||
|
||||
// Note: Color fields lack an isDelayed property and serialize every frame during editing.
|
||||
// Rebuilding the UI while a User edits a color will break the field's update loop.
|
||||
// To avoid this, we update ClientSim PlayerData directly and skip the UI rebuild.
|
||||
// If the user has sorted fields by "Last Updated", the color field will not sort to the top from this.
|
||||
if (kvp.Value.Value.Type is ClientSimPlayerDataType.Color or ClientSimPlayerDataType.Color32)
|
||||
{
|
||||
UpdatePlayerDataList(data, player, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdatePlayerDataList(data, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// is there a better way to find all FloatField children and their immediate parent?
|
||||
void SetIsDelayedForCompositeField(VisualElement compositeField)
|
||||
{
|
||||
VisualElement container = compositeField.contentContainer.Children().ToList()[1];
|
||||
container.style.flexShrink = 1;
|
||||
foreach (var child in container.Children())
|
||||
{
|
||||
if (child is FloatField f)
|
||||
{
|
||||
f.isDelayed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshPlayerData()
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
LoadPlayerData();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var player in VRCPlayerApi.AllPlayers)
|
||||
{
|
||||
player.GetClientSimPlayer().PlayerDataObject.Decode(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OpenLocalDataDirectory()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Process.Start("explorer.exe", LocalPlayerDataRoot.Replace("/", "\\"));
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
Process.Start("open", LocalPlayerDataRoot);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
Process.Start("xdg-open", LocalPlayerDataRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearPlayerData()
|
||||
{
|
||||
StringBuilder clearedFiles = new StringBuilder("Cleared PlayerData files:");
|
||||
string[] playerDataFiles = Directory.GetFiles(LocalPlayerDataRoot, LocalDataPathPattern);
|
||||
foreach (var file in playerDataFiles)
|
||||
{
|
||||
File.Delete(file);
|
||||
clearedFiles.Append($"\n\t{file}");
|
||||
|
||||
// in play mode, let other systems know PlayerData was cleared
|
||||
if (EditorApplication.isPlaying)
|
||||
{
|
||||
string playerId = file.Split("_")[1].Split(".")[0];
|
||||
int id = int.Parse(playerId);
|
||||
var player = VRCPlayerApi.GetPlayerById(id);
|
||||
{
|
||||
eventDispatcher.SendEvent(new ClientSimOnPlayerDataClearedEvent { player = player });
|
||||
}
|
||||
}
|
||||
|
||||
hasLocalPlayerData = false;
|
||||
if (!isLocalPlayerSelected)
|
||||
{
|
||||
addRemotePlayerTestDataButton.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
CheckDataEmpty();
|
||||
}
|
||||
|
||||
page = 0;
|
||||
numPages = 0;
|
||||
UpdatePage();
|
||||
|
||||
if (playerDataFiles.Length > 0)
|
||||
{
|
||||
this.Log(clearedFiles.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void NextPage()
|
||||
{
|
||||
page++;
|
||||
UpdatePage();
|
||||
}
|
||||
|
||||
private void PreviousPage()
|
||||
{
|
||||
page--;
|
||||
UpdatePage();
|
||||
}
|
||||
|
||||
private void UpdatePage(bool reload = true)
|
||||
{
|
||||
bool isFirstPage = page == 0;
|
||||
bool isLastPage = page == numPages - 1;
|
||||
|
||||
prevPageButton.SetEnabled(!isFirstPage);
|
||||
nextPageButton.SetEnabled(!isLastPage);
|
||||
|
||||
pageLabel.text = $"{page+1} / {numPages}";
|
||||
if (reload)
|
||||
{
|
||||
LoadPlayerDataForSelectedPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckDataEmpty(int numPlayerData = 0)
|
||||
{
|
||||
bool isEmpty = numPlayerData == 0;
|
||||
clearDataButton.SetEnabled(!isEmpty);
|
||||
noDataInfoContainer.style.display = isEmpty ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
sortDropdown.style.display = isEmpty ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
pagingContainer.style.display = numPlayerData > PAGE_SIZE ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
labelContainer.Clear();
|
||||
return isEmpty;
|
||||
}
|
||||
|
||||
private void CheckLocalPlayerName()
|
||||
{
|
||||
localPlayerName = string.IsNullOrEmpty(ClientSimSettings.Instance.customLocalPlayerName)
|
||||
? "[1] Local Player"
|
||||
: ClientSimSettings.Instance.customLocalPlayerName;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b21b5684b10448b9b0a445cfc16fcfd
|
||||
timeCreated: 1709067526
|
||||
@ -0,0 +1,296 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using VRC.SDK3.ClientSim.Editor.VisualElements;
|
||||
using VRC.SDK3.ClientSim.Interfaces;
|
||||
using VRC.SDKBase;
|
||||
|
||||
namespace VRC.SDK3.ClientSim.Editor
|
||||
{
|
||||
#if VRC_ENABLE_PLAYER_PERSISTENCE
|
||||
public class ClientSimPlayerObjectWindow : EditorWindow
|
||||
{
|
||||
|
||||
private Dictionary<int, List<PlayerObjectData>> playerObjects = new Dictionary<int, List<PlayerObjectData>>();
|
||||
private List<ClientSimNetworkHolderInstanceElement> playerObjectVisualElements = new List<ClientSimNetworkHolderInstanceElement>();
|
||||
private List<VRCPlayerApi> players = new List<VRCPlayerApi>();
|
||||
private IClientSimEventDispatcher eventDispatcher;
|
||||
|
||||
private DropdownField playerDropdown;
|
||||
private DropdownField Sort;
|
||||
private Label pageLabel;
|
||||
private Button nextPage;
|
||||
private Button prevPage;
|
||||
private TextField searchField;
|
||||
private ScrollView playerDataList;
|
||||
private VisualElement HelpBox;
|
||||
private Label HelpBoxLabel;
|
||||
|
||||
private int CurrentIndex = 0;
|
||||
private int CurrentSelectedPlayerID = -1;
|
||||
private static int CountPerPage = 10;
|
||||
private static string HelpTextEnterPlayMode = "Enter Play Mode to view PlayerObjects";
|
||||
|
||||
[MenuItem("VRChat SDK/ClientSim PlayerObjects", false, 1500)]
|
||||
public static void Init()
|
||||
{
|
||||
var window = GetWindow<ClientSimPlayerObjectWindow>(false, "ClientSim PlayerObjects");
|
||||
window.minSize = new Vector2(400, 400);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
public void OnEnable()
|
||||
{
|
||||
VisualElement root = rootVisualElement;
|
||||
VisualTreeAsset visualTree = Resources.Load<VisualTreeAsset>(nameof(ClientSimPlayerObjectWindow));
|
||||
visualTree.CloneTree(root);
|
||||
|
||||
playerDropdown = root.Q<DropdownField>("PlayerDropdown");
|
||||
playerDropdown.choices = new List<string>();
|
||||
playerDropdown.RegisterValueChangedCallback((evt) =>
|
||||
{
|
||||
CurrentSelectedPlayerID = players[playerDropdown.index].playerId;
|
||||
CurrentIndex = 0;
|
||||
pageLabel.text = "Page 0";
|
||||
UpdateCurrentPage(null);
|
||||
});
|
||||
|
||||
Sort = root.Q<DropdownField>("Sort");
|
||||
Sort.choices = new List<string>()
|
||||
{
|
||||
"Alphabetical",
|
||||
"Last modified"
|
||||
};
|
||||
Sort.SetValueWithoutNotify("Alphabetical");
|
||||
|
||||
searchField = root.Q<TextField>("Search");
|
||||
searchField.RegisterValueChangedCallback((evt) =>
|
||||
{
|
||||
UpdateCurrentPage(null);
|
||||
});
|
||||
|
||||
playerDataList = root.Q<ScrollView>("PlayerObjectList");
|
||||
|
||||
nextPage = root.Q<Button>("Right");
|
||||
nextPage.clicked += NextPage;
|
||||
nextPage.style.display = DisplayStyle.None;
|
||||
|
||||
prevPage = root.Q<Button>("Left");
|
||||
prevPage.clicked += PreviousPage;
|
||||
prevPage.style.display = DisplayStyle.None;
|
||||
|
||||
pageLabel = root.Q<Label>("PagingLabel");
|
||||
pageLabel.text = "Page 1";
|
||||
|
||||
HelpBox = root.Q<VisualElement>("HelpTextBox");
|
||||
HelpBoxLabel = HelpBox.Q<Label>("HelpTextBoxLabel");
|
||||
HelpBoxLabel.text = HelpTextEnterPlayMode;
|
||||
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
if (EditorApplication.isPlaying)
|
||||
{
|
||||
OnPlayModeStateChanged(PlayModeStateChange.EnteredPlayMode);
|
||||
}
|
||||
|
||||
VRCPlayerApi[] newplayers = new VRCPlayerApi[VRCPlayerApi.GetPlayerCount()];
|
||||
newplayers = VRCPlayerApi.GetPlayers(newplayers);
|
||||
|
||||
foreach (VRCPlayerApi player in newplayers)
|
||||
{
|
||||
ClientSimOnPlayerJoinedEvent playerJoinedEvent = new ClientSimOnPlayerJoinedEvent();
|
||||
playerJoinedEvent.player = player;
|
||||
OnPlayerJoined(playerJoinedEvent);
|
||||
}
|
||||
|
||||
for(int i = 0; i < CountPerPage; i++)
|
||||
{
|
||||
ClientSimNetworkHolderInstanceElement element = new ClientSimNetworkHolderInstanceElement();
|
||||
playerObjectVisualElements.Add(element);
|
||||
playerDataList.Add(element);
|
||||
element.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void OnPlayModeStateChanged(PlayModeStateChange state)
|
||||
{
|
||||
if (!ClientSimMain.TryGetInstance(out var instance)) return;
|
||||
|
||||
if (state == PlayModeStateChange.EnteredPlayMode)
|
||||
{
|
||||
eventDispatcher = instance.GetEventDispatcher();
|
||||
eventDispatcher.Subscribe<ClientSimOnPlayerObjectUpdatedEvent>(OnPlayerObjectUpdated);
|
||||
eventDispatcher.Subscribe<ClientSimOnPlayerObjectUpdateEndedEvent>(UpdateCurrentPage);
|
||||
eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
|
||||
eventDispatcher.Subscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
|
||||
|
||||
nextPage.style.display = DisplayStyle.Flex;
|
||||
HelpBox.style.display = DisplayStyle.None;
|
||||
}
|
||||
else if (state == PlayModeStateChange.ExitingPlayMode)
|
||||
{
|
||||
playerDropdown.choices.RemoveRange(1, playerDropdown.choices.Count - 1);
|
||||
playerDropdown.index = 0;
|
||||
|
||||
playerDataList.Clear();
|
||||
CurrentIndex = 0;
|
||||
CurrentSelectedPlayerID = -1;
|
||||
|
||||
nextPage.style.display = DisplayStyle.None;
|
||||
prevPage.style.display = DisplayStyle.None;
|
||||
|
||||
HelpBox.style.display = DisplayStyle.Flex;
|
||||
|
||||
eventDispatcher?.Unsubscribe<ClientSimOnPlayerObjectUpdatedEvent>(OnPlayerObjectUpdated);
|
||||
eventDispatcher?.Unsubscribe<ClientSimOnPlayerObjectUpdateEndedEvent>(UpdateCurrentPage);
|
||||
eventDispatcher?.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
|
||||
eventDispatcher?.Unsubscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
|
||||
eventDispatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void NextPage()
|
||||
{
|
||||
CurrentIndex += CountPerPage;
|
||||
pageLabel.text = "Page " + (CurrentIndex / CountPerPage + 1);
|
||||
if(CurrentIndex+CountPerPage > playerObjects[CurrentSelectedPlayerID].Count)
|
||||
nextPage.style.display = DisplayStyle.None;
|
||||
else
|
||||
{
|
||||
nextPage.style.display = DisplayStyle.Flex;
|
||||
prevPage.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviousPage()
|
||||
{
|
||||
CurrentIndex -= CountPerPage;
|
||||
pageLabel.text = "Page " + (CurrentIndex / CountPerPage + 1);
|
||||
|
||||
if(CurrentIndex <= 0)
|
||||
prevPage.style.display = DisplayStyle.None;
|
||||
else
|
||||
{
|
||||
prevPage.style.display = DisplayStyle.Flex;
|
||||
nextPage.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerObjectUpdated(ClientSimOnPlayerObjectUpdatedEvent payload)
|
||||
{
|
||||
int playerId = payload.Data.GetPlayerId();
|
||||
if(playerObjects.TryGetValue(playerId, out var instanceElements))
|
||||
{
|
||||
int index = instanceElements.FindIndex((x) => x.NetworkView == payload.Data);
|
||||
if (index == -1)
|
||||
{
|
||||
AddElementToList(payload);
|
||||
}
|
||||
|
||||
} else {
|
||||
playerObjects[playerId] = new List<PlayerObjectData>();
|
||||
AddElementToList(payload);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddElementToList(ClientSimOnPlayerObjectUpdatedEvent payload)
|
||||
{
|
||||
int componentCount = payload.Data.GetNetworkComponentCount();
|
||||
int playerId = payload.Data.GetPlayerId();
|
||||
for (int i = 0; i < componentCount; i++)
|
||||
{
|
||||
MonoBehaviour component = payload.Data.GetNetworkComponents()[i];
|
||||
|
||||
PlayerObjectData element =
|
||||
new PlayerObjectData
|
||||
{
|
||||
NetworkView = payload.Data,
|
||||
lastModified = DateTime.Now,
|
||||
Name = $"{component.GetType().Name} ({component.gameObject.name})",
|
||||
};
|
||||
|
||||
playerObjects[playerId].Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCurrentPage(ClientSimOnPlayerObjectUpdateEndedEvent e)
|
||||
{
|
||||
if(playerObjects.Count == 0 || CurrentSelectedPlayerID == -1)
|
||||
return;
|
||||
|
||||
int index = CurrentIndex;
|
||||
int count = playerObjects[CurrentSelectedPlayerID].Count;
|
||||
int localIndex = 0;
|
||||
for (; index < count;)
|
||||
{
|
||||
PlayerObjectData element = playerObjects[CurrentSelectedPlayerID][index];
|
||||
if(searchField.value != "" && !element.Name.Contains(searchField.value))
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
playerObjectVisualElements[localIndex].UpdateData(element);
|
||||
|
||||
localIndex++;
|
||||
if(localIndex >= CountPerPage)
|
||||
break;
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
for (int i = localIndex; i < playerObjectVisualElements.Count; i++)
|
||||
{
|
||||
playerObjectVisualElements[i].style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
prevPage.style.display = CurrentIndex - CountPerPage < 0 ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
nextPage.style.display = CurrentIndex + CountPerPage >= count ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
|
||||
pageLabel.style.display = prevPage.style.display == DisplayStyle.None && nextPage.style.display == DisplayStyle.None ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent payload)
|
||||
{
|
||||
if (players.Contains(payload.player)) return;
|
||||
|
||||
if(playerObjects.ContainsKey(payload.player.playerId) == false)
|
||||
playerObjects.Add(payload.player.playerId, new List<PlayerObjectData>());
|
||||
playerDropdown.choices.Add(payload.player.displayName);
|
||||
players.Add(payload.player);
|
||||
if (CurrentSelectedPlayerID == -1)
|
||||
{
|
||||
playerDropdown.index = 0;
|
||||
CurrentSelectedPlayerID = payload.player.playerId;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerLeft(ClientSimOnPlayerLeftEvent payload)
|
||||
{
|
||||
if (!playerObjects.ContainsKey(payload.player.playerId)) return;
|
||||
|
||||
playerObjects.Remove(payload.player.playerId);
|
||||
players.Remove(payload.player);
|
||||
playerDropdown.choices.Remove(playerDropdown.choices.Find(x => x == payload.player.displayName));
|
||||
if (CurrentSelectedPlayerID == payload.player.playerId)
|
||||
{
|
||||
playerDropdown.index = 0;
|
||||
CurrentSelectedPlayerID = players[0].playerId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct PlayerObjectData
|
||||
{
|
||||
public IClientSimNetworkSerializer NetworkView;
|
||||
public DateTime lastModified;
|
||||
public string Name;
|
||||
public int index;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 780b242d9ac449059aaf28150bd5a6ef
|
||||
timeCreated: 1715609826
|
||||
@ -0,0 +1,511 @@
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using VRC.SDKBase;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace VRC.SDK3.ClientSim.Editor
|
||||
{
|
||||
public class ClientSimSettingsWindow : EditorWindow
|
||||
{
|
||||
// General content
|
||||
private readonly GUIContent _generalFoldoutGuiContent = new GUIContent("General Settings", "");
|
||||
private readonly GUIContent _enableToggleGuiContent = new GUIContent("Enable ClientSim", "If enabled, all triggers will function similarly to VRChat. Note that behavior may be different than the actual game!");
|
||||
private readonly GUIContent _displayLogsToggleGuiContent = new GUIContent("Enable Console Logging", "Enabling logging will print messages to the console when certain events happen. Examples include trigger execution, pickup grabbed, station entered, etc.");
|
||||
private readonly GUIContent _deleteEditorOnlyToggleGuiContent = new GUIContent("Remove \"EditorOnly\"", "Enabling this setting will ensure that all objects with the tag \"EditorOnly\" are deleted when in playmode. This can be helpful in finding objects that will not be uploaded with your world. Enable console logging to see which objects are deleted.");
|
||||
private readonly GUIContent _startupDelayGuiContent = new GUIContent("Startup Delay", "The duration that the Client Sim will wait to simulate the VRChat client loading before spawning the player and initializing Udon. This is useful to test when Unity components behave differently at startup compared to VRChat.");
|
||||
private readonly GUIContent _stopOnScriptChangesToggleGuiContent = new GUIContent("Stop On Script Changes", "If enabled, the editor will stop if script changes are detected while in play mode. This will override the Unity Editor setting 'Preferences > General > Script Changes While Playing'.");
|
||||
private readonly GUIContent _hideMenuOnLaunchToggleGuiContent = new GUIContent("Hide Menu On Launch", "Enabling this setting will prevent the ClientSim menu from being displayed when initially entering play mode.");
|
||||
private readonly GUIContent _setTargetFrameRateGuiContent = new GUIContent("Set Target FrameRate", "Should ClientSim set the target framerate on startup? This will automatically set the physics delta time to match expected framerate. Disabling this setting is useful when profiling.");
|
||||
private readonly GUIContent _targetFrameRateGuiContent = new GUIContent("Target FrameRate", "The target framerate unity should aim for. Default is 90 fps.");
|
||||
|
||||
// Player Controller content
|
||||
private readonly GUIContent _playerControllerFoldoutGuiContent = new GUIContent("Player Controller Settings", "");
|
||||
private readonly GUIContent _playerControllerToggleGuiContent = new GUIContent("Spawn Player Controller", "If enabled, a player controller will spawn and allow you to move around your world as if in desktop mode. Supports interacts and pickups.");
|
||||
|
||||
private readonly GUIContent _showDesktopReticleGuiContent = new GUIContent("Show Desktop Reticle", "Show or hide the center Desktop reticle image.");
|
||||
private readonly GUIContent _showTooltipsGuiContent = new GUIContent("Show Tooltips", "If enabled, hovering over an interactable object or pickup will display a tooltip above the object.");
|
||||
private readonly GUIContent _invertMouseLookGuiContent = new GUIContent("Invert Mouse Look", "If enabled, moving the mouse up or down will invert the direction the player will look up and down.");
|
||||
private int selectedLanguageIndex;
|
||||
|
||||
// Player settings
|
||||
private readonly GUIContent _playerButtonsFoldoutGuiContent = new GUIContent("Player Settings", "");
|
||||
private readonly GUIContent _localPlayerCustomNameGuiContent = new GUIContent("Local Player Name", "Set a custom name for the local player. Useful for testing udon script name detection");
|
||||
private readonly GUIContent _playerHeightGuiContent = new GUIContent("Initial Player Height", "How tall should the player be in meters at app start. Default height is 1.9. Note that the player's collision capsule is 1.6 and never changes.");
|
||||
private readonly GUIContent _currentLanguageGuiContent = new GUIContent("Current Language", "The language the player is currently using.");
|
||||
private readonly GUIContent _isMasterGuiContent = new GUIContent("Local Player Is Master", "Set whether the local player starts off as the master of the instance. Setting this to false and starting Client Sim will spawn a remote player before the local player.");
|
||||
private readonly GUIContent _isInstanceOwnerGuiContent = new GUIContent("Is Instance Owner", "Set whether the local player is considered the instance owner");
|
||||
private readonly GUIContent _isVRCPlusGuiContent = new GUIContent("Is VRC+", "Set whether the local player has an active VRC+ subscription");
|
||||
private readonly GUIContent _remotePlayerCustomNameGuiContent = new GUIContent("Remote Player Name", "Set a custom name for the next spawned remote player. Useful for testing udon script name detection");
|
||||
|
||||
private const int WARNING_ICON_SIZE = 60;
|
||||
|
||||
private static Texture2D _warningIcon;
|
||||
private static ClientSimSettings _settings;
|
||||
private static IClientSimPlayerHeightManager _heightManager;
|
||||
|
||||
private Vector2 _scrollPosition;
|
||||
private GUIStyle _boxStyle;
|
||||
private GUIStyle _multilineLabel;
|
||||
private bool _showGeneralSettings = true;
|
||||
private bool _showPlayerControllerSettings = true;
|
||||
private bool _showPlayerButtons = true;
|
||||
|
||||
private string _version;
|
||||
private string _remotePlayerCustomName = "";
|
||||
|
||||
private bool _needsInputSetup = false;
|
||||
private bool _needsInputManagerSetup = false;
|
||||
private bool _needsAudioSetup = false;
|
||||
private bool _needsLayerSetup = false;
|
||||
|
||||
[MenuItem("VRChat SDK/ClientSim", false, 1500)]
|
||||
public static void Init()
|
||||
{
|
||||
ClientSimSettingsWindow window = GetWindow<ClientSimSettingsWindow>(false, "ClientSim Settings");
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_settings == null)
|
||||
{
|
||||
_settings = ClientSimSettings.Instance;
|
||||
}
|
||||
|
||||
if (_warningIcon == null)
|
||||
{
|
||||
// Reuse VRChat's warning icon.
|
||||
_warningIcon = Resources.Load<Texture2D>("2FAIcons/SDK_Warning_Triangle_icon");
|
||||
}
|
||||
|
||||
_version = ClientSimResourceLoader.GetVersion();
|
||||
}
|
||||
|
||||
private void OnFocus()
|
||||
{
|
||||
// Verify settings to know if we need to display "Do It" buttons
|
||||
_needsInputSetup = !ClientSimProjectSettingsSetup.IsUsingCorrectInputAxesSettings();
|
||||
_needsInputManagerSetup = !ClientSimProjectSettingsSetup.IsUsingCorrectInputTypeSettings();
|
||||
_needsAudioSetup = !ClientSimProjectSettingsSetup.IsUsingCorrectAudioSettings();
|
||||
|
||||
// VRChat layer setup
|
||||
_needsLayerSetup = !UpdateLayers.AreLayersSetup() || !UpdateLayers.IsCollisionLayerMatrixSetup();
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
float tempLabelWidth = EditorGUIUtility.labelWidth;
|
||||
EditorGUIUtility.labelWidth = 175;
|
||||
|
||||
_boxStyle = new GUIStyle(EditorStyles.helpBox);
|
||||
_multilineLabel = new GUIStyle(EditorStyles.label);
|
||||
_multilineLabel.wordWrap = true;
|
||||
|
||||
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
||||
|
||||
DrawHeader();
|
||||
|
||||
DrawWindow();
|
||||
|
||||
DrawFooter();
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
EditorGUIUtility.labelWidth = tempLabelWidth;
|
||||
}
|
||||
|
||||
private void DrawWindow()
|
||||
{
|
||||
if (_needsAudioSetup || _needsInputSetup || _needsInputManagerSetup || _needsLayerSetup)
|
||||
{
|
||||
DrawDoItButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
// Disables UI if ClientSim is disabled
|
||||
DrawGeneralSettings();
|
||||
|
||||
DrawPlayerControllerSettings();
|
||||
|
||||
DrawPlayerButtons();
|
||||
|
||||
// Disable group from General settings
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
ClientSimSettings.SaveSettings(_settings);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHeader()
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
DrawVersion();
|
||||
}
|
||||
|
||||
private void DrawVersion()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Version: " + _version);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DrawDoItButtons()
|
||||
{
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
|
||||
// Display a warning icon informing them of project setting issues.
|
||||
GUIStyle labelStyle = new GUIStyle(EditorStyles.label)
|
||||
{ alignment = TextAnchor.MiddleCenter, wordWrap = true };
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
string content = "You must address the following issues before you can test this content using ClientSim!";
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
content += "\nPlease exit playmode before applying these settings.";
|
||||
}
|
||||
|
||||
GUILayout.Label(new GUIContent(_warningIcon), GUILayout.Width(WARNING_ICON_SIZE), GUILayout.Height(WARNING_ICON_SIZE));
|
||||
EditorGUILayout.LabelField(content, labelStyle, GUILayout.Height(WARNING_ICON_SIZE));
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
GUILayout.Space(5);
|
||||
|
||||
|
||||
EditorGUI.BeginDisabledGroup(Application.isPlaying);
|
||||
|
||||
DrawAudioSettingsDoIt();
|
||||
|
||||
DrawInputAxesSettingsDoIt();
|
||||
|
||||
DrawInputManagerSettingsDoIt();
|
||||
|
||||
DrawLayerSettingsSection();
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
|
||||
private void DrawAudioSettingsDoIt()
|
||||
{
|
||||
if (!_needsAudioSetup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BeginWarningArea();
|
||||
|
||||
GUILayout.Label("Audio Spatializer Settings", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
GUILayout.Label("VRChat uses an audio spatializer that is different from the default Unity spatializer. Clicking this button will modify the project's audio settings to use this audio spatializer.", _multilineLabel);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Set Audio Spatializer"))
|
||||
{
|
||||
bool doIt = EditorUtility.DisplayDialog("Set Audio Spatializer for ClientSim",
|
||||
"This will modify the project's audio settings to use the audio spatializer. Are you sure you want to continue?",
|
||||
"Do it!", "Don't do it");
|
||||
if (doIt)
|
||||
{
|
||||
ClientSimProjectSettingsSetup.SetAudioSettings();
|
||||
_needsAudioSetup = false;
|
||||
}
|
||||
}
|
||||
|
||||
EndWarningArea();
|
||||
}
|
||||
|
||||
private void DrawInputAxesSettingsDoIt()
|
||||
{
|
||||
if (!_needsInputSetup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BeginWarningArea();
|
||||
|
||||
GUILayout.Label("Input Axes Settings", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
GUILayout.Label("VRChat uses a custom list of Input Axes. This will allow you to test these Input Axes in Udon. Clicking this button will replace this project's input axes with VRChat's and remove any custom axes added by the user.", _multilineLabel);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Apply VRChat Input Axes"))
|
||||
{
|
||||
bool doIt = EditorUtility.DisplayDialog("Set Input Axes for ClientSim",
|
||||
"This will replace this project's input axes with VRChat's. Any custom input axes will be removed. Are you sure you want to continue?",
|
||||
"Do it!", "Don't do it");
|
||||
if (doIt)
|
||||
{
|
||||
ClientSimProjectSettingsSetup.ApplyClientSimInputAxes();
|
||||
_needsInputSetup = false;
|
||||
}
|
||||
}
|
||||
|
||||
EndWarningArea();
|
||||
}
|
||||
|
||||
private void DrawInputManagerSettingsDoIt()
|
||||
{
|
||||
if (!_needsInputManagerSetup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BeginWarningArea();
|
||||
|
||||
GUILayout.Label("Input Manager Settings", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
GUILayout.Label("VRChat and ClientSim use both the legacy Input Manager and the new Input System package. Without this setting, input will not work in playmode for ClientSim or Udon. Clicking this button will update the project settings to use both input systems and then *RESTART* Unity to apply the changes.", _multilineLabel);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Set Input Manager"))
|
||||
{
|
||||
bool doIt = EditorUtility.DisplayDialog("Set Input Manager for ClientSim",
|
||||
"This will update the project settings to use both the legacy Input Manager and the new Input System package. Clicking \"Do it!\" will also *RESTART* Unity to apply the changes. Are you sure you want to continue?",
|
||||
"Do it!", "Don't do it");
|
||||
if (doIt)
|
||||
{
|
||||
ClientSimProjectSettingsSetup.SetInputTypeSettings();
|
||||
_needsInputManagerSetup = false;
|
||||
|
||||
// After importing the new input system, a dialog is displayed to enable it and disable the old.
|
||||
// This method is then called after to restart unity and recompile code.
|
||||
// Since the class is internal, Reflection is needed to call the method.
|
||||
var inputAssembly = typeof(InputSystem).Assembly;
|
||||
var editorHelpersType = inputAssembly.GetType("UnityEngine.InputSystem.Editor.EditorHelpers");
|
||||
var restartEditorMethod = editorHelpersType.GetMethod("RestartEditorAndRecompileScripts",
|
||||
BindingFlags.Public | BindingFlags.Static);
|
||||
restartEditorMethod.Invoke(null, new object[] { false });
|
||||
}
|
||||
}
|
||||
|
||||
EndWarningArea();
|
||||
}
|
||||
|
||||
private void DrawLayerSettingsSection()
|
||||
{
|
||||
if (!_needsLayerSetup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BeginWarningArea();
|
||||
|
||||
GUILayout.Label("Layer Settings", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
GUILayout.Label("VRChat scenes must have the same Unity layer configuration as VRChat. Please see the VRChat Build Control Panel to setup the project's layers and collision matrix.", _multilineLabel);
|
||||
|
||||
// TODO create button to open build control panel.
|
||||
|
||||
EndWarningArea();
|
||||
}
|
||||
|
||||
private void DrawGeneralSettings()
|
||||
{
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
_showGeneralSettings = EditorGUILayout.Foldout(_showGeneralSettings, _generalFoldoutGuiContent, true);
|
||||
|
||||
if (_showGeneralSettings)
|
||||
{
|
||||
AddIndent();
|
||||
|
||||
if (_settings.enableClientSim && FindFirstObjectByType<VRC_SceneDescriptor>() == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("No VRC_SceneDescriptor in scene. Please add one to enable ClientSim.", MessageType.Warning);
|
||||
}
|
||||
if (_settings.enableClientSim && Application.isPlaying && !ClientSimMain.HasInstance())
|
||||
{
|
||||
EditorGUILayout.HelpBox("Please exit and re-enter playmode to enable ClientSim!", MessageType.Warning);
|
||||
}
|
||||
|
||||
_settings.enableClientSim = EditorGUILayout.Toggle(_enableToggleGuiContent, _settings.enableClientSim);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!_settings.enableClientSim);
|
||||
|
||||
_settings.displayLogs = EditorGUILayout.Toggle(_displayLogsToggleGuiContent, _settings.displayLogs);
|
||||
|
||||
_settings.stopOnScriptChanges = EditorGUILayout.Toggle(_stopOnScriptChangesToggleGuiContent, _settings.stopOnScriptChanges);
|
||||
|
||||
|
||||
// Settings that cannot be changed at runtime
|
||||
EditorGUI.BeginDisabledGroup(Application.isPlaying);
|
||||
|
||||
_settings.deleteEditorOnly = EditorGUILayout.Toggle(_deleteEditorOnlyToggleGuiContent, _settings.deleteEditorOnly);
|
||||
_settings.hideMenuOnLaunch = EditorGUILayout.Toggle(_hideMenuOnLaunchToggleGuiContent, _settings.hideMenuOnLaunch);
|
||||
_settings.setTargetFrameRate = EditorGUILayout.Toggle(_setTargetFrameRateGuiContent, _settings.setTargetFrameRate);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!_settings.setTargetFrameRate);
|
||||
_settings.targetFrameRate = EditorGUILayout.IntField(_targetFrameRateGuiContent, _settings.targetFrameRate);
|
||||
_settings.targetFrameRate = Mathf.Max(1, _settings.targetFrameRate);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
_settings.initializationDelay = EditorGUILayout.FloatField(_startupDelayGuiContent, _settings.initializationDelay);
|
||||
_settings.initializationDelay = Mathf.Max(0, _settings.initializationDelay);
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
RemoveIndent();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
private void DrawPlayerControllerSettings()
|
||||
{
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
|
||||
_showPlayerControllerSettings = EditorGUILayout.Foldout(_showPlayerControllerSettings, _playerControllerFoldoutGuiContent, true);
|
||||
if (_showPlayerControllerSettings)
|
||||
{
|
||||
AddIndent();
|
||||
|
||||
_settings.spawnPlayer = EditorGUILayout.Toggle(_playerControllerToggleGuiContent, _settings.spawnPlayer);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!_settings.spawnPlayer);
|
||||
|
||||
_settings.showDesktopReticle = EditorGUILayout.Toggle(_showDesktopReticleGuiContent, _settings.showDesktopReticle);
|
||||
_settings.showTooltips = EditorGUILayout.Toggle(_showTooltipsGuiContent, _settings.showTooltips);
|
||||
_settings.invertMouseLook = EditorGUILayout.Toggle(_invertMouseLookGuiContent, _settings.invertMouseLook);
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
RemoveIndent();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
private void DrawPlayerButtons()
|
||||
{
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
_showPlayerButtons = EditorGUILayout.Foldout(_showPlayerButtons, _playerButtonsFoldoutGuiContent, true);
|
||||
if (_showPlayerButtons)
|
||||
{
|
||||
AddIndent();
|
||||
|
||||
bool hasInstance = ClientSimMain.HasInstance();
|
||||
|
||||
// Values cannot change once ClientSim has started.
|
||||
EditorGUI.BeginDisabledGroup(hasInstance || Application.isPlaying);
|
||||
|
||||
_settings.customLocalPlayerName = EditorGUILayout.TextField(_localPlayerCustomNameGuiContent, _settings.customLocalPlayerName);
|
||||
_settings.playerStartHeight = EditorGUILayout.FloatField(_playerHeightGuiContent, _settings.playerStartHeight);
|
||||
selectedLanguageIndex = EditorGUILayout.Popup(_currentLanguageGuiContent, selectedLanguageIndex, _settings.GetAvailableDisplayLanguages());
|
||||
_settings.currentLanguage = _settings.GetLanguage(selectedLanguageIndex);
|
||||
_settings.localPlayerIsMaster = EditorGUILayout.Toggle(_isMasterGuiContent, _settings.localPlayerIsMaster);
|
||||
_settings.isInstanceOwner = EditorGUILayout.Toggle(_isInstanceOwnerGuiContent, _settings.isInstanceOwner);
|
||||
_settings.isVRCPlus = EditorGUILayout.Toggle(_isVRCPlusGuiContent, _settings.isVRCPlus);
|
||||
|
||||
// TODO display desktop/vr option here
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!hasInstance || !Application.isPlaying);
|
||||
|
||||
_remotePlayerCustomName = EditorGUILayout.TextField(_remotePlayerCustomNameGuiContent, _remotePlayerCustomName);
|
||||
|
||||
if (GUILayout.Button("Spawn Remote Player"))
|
||||
{
|
||||
ClientSimMain.SpawnRemotePlayer(_remotePlayerCustomName);
|
||||
}
|
||||
|
||||
List<VRCPlayerApi> playersToRemove = new List<VRCPlayerApi>();
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
foreach (var player in VRCPlayerApi.AllPlayers)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
GUILayout.Label(player.displayName);
|
||||
GUILayout.Space(5);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(VRCPlayerApi.AllPlayers.Count == 1 || player.isLocal);
|
||||
|
||||
if (GUILayout.Button("Remove Player"))
|
||||
{
|
||||
playersToRemove.Add(player);
|
||||
}
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
EditorGUI.BeginDisabledGroup(VRCPlayerApi.AllPlayers.Count == 1);
|
||||
|
||||
if (GUILayout.Button("Simulate VRC+ Gift"))
|
||||
{
|
||||
player.GetClientSimPlayer().SimulateVRCPlusGift();
|
||||
}
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
for (int i = playersToRemove.Count - 1; i >= 0; --i)
|
||||
{
|
||||
ClientSimMain.RemovePlayer(playersToRemove[i]);
|
||||
}
|
||||
playersToRemove.Clear();
|
||||
}
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
RemoveIndent();
|
||||
}
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
private void AddIndent()
|
||||
{
|
||||
++EditorGUI.indentLevel;
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(EditorGUI.indentLevel * 7 + 4);
|
||||
EditorGUILayout.BeginVertical();
|
||||
}
|
||||
|
||||
private void RemoveIndent()
|
||||
{
|
||||
--EditorGUI.indentLevel;
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void BeginWarningArea()
|
||||
{
|
||||
EditorGUILayout.BeginVertical(_boxStyle);
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.BeginVertical();
|
||||
}
|
||||
|
||||
private void EndWarningArea()
|
||||
{
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
GUILayout.Space(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9447dfb6ba20ab1479485c89c34cf4a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user