Process.Start is not supported in IL2CPP builds, so now OpenSeeLauncher has an option to do the process launching directly through DLL calls.
This commit is contained in:
parent
633293eb52
commit
e157e9d5c9
11 changed files with 389 additions and 38 deletions
|
|
@ -85,7 +85,6 @@ namespace OpenSee
|
|||
{
|
||||
return AddProcess(Process.GetProcessById(processId).Handle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#region Helper classes
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -11,6 +12,20 @@ using UnityEngine;
|
|||
namespace OpenSee {
|
||||
|
||||
public class OpenSeeLauncher : MonoBehaviour {
|
||||
#region DllImport
|
||||
[DllImport("escapi_x64", CallingConvention = CallingConvention.Cdecl, EntryPoint = "countCaptureDevices")]
|
||||
private static extern int countCaptureDevices_x64();
|
||||
|
||||
[DllImport("escapi_x64", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getCaptureDeviceName")]
|
||||
private static extern void getCaptureDeviceName_x64(int deviceno, [Out] StringBuilder namebuffer, int bufferlength);
|
||||
|
||||
[DllImport("escapi_x86", CallingConvention = CallingConvention.Cdecl, EntryPoint = "countCaptureDevices")]
|
||||
private static extern int countCaptureDevices_x86();
|
||||
|
||||
[DllImport("escapi_x86", CallingConvention = CallingConvention.Cdecl, EntryPoint = "getCaptureDeviceName")]
|
||||
private static extern void getCaptureDeviceName_x86(int deviceno, [Out] StringBuilder namebuffer, int bufferlength);
|
||||
#endregion
|
||||
|
||||
[Header("Settings")]
|
||||
[Tooltip("If this is left empty, it will default to the OpenSee component on the same game object.")]
|
||||
public OpenSee openSeeTarget;
|
||||
|
|
@ -30,6 +45,10 @@ public class OpenSeeLauncher : MonoBehaviour {
|
|||
public string modelPath = "models\\";
|
||||
[Tooltip("Additional options that should be passed to the face tracker.")]
|
||||
public List<string> commandlineOptions = new List<string>(new string[] { "--silent", "1", "--max-threads", "4" });
|
||||
[Tooltip("IL2CPP doesn't support Proocess.Start. When this is enabled, OpenSeeLauncher will create the tracking process by calling the necessary API functions directly through the DLLs. In this case, reading the standard output from the process will not be supported. It will also retrieve the camera list directly through the escapi DLLs, so make sure it is part of your Unity project.")]
|
||||
public bool usePinvoke = false;
|
||||
[Tooltip("When this is enabled, even if usePinvoke is disabled, the camera list will be retrieved through escapi DLLs directly, which can be faster. Make sure the DLLs are in your Unity project.")]
|
||||
public bool usePinvokeListCameras = false;
|
||||
[Header("Runtime information")]
|
||||
[Tooltip("This field shows if the tracker is currently alive.")]
|
||||
public bool trackerAlive = false;
|
||||
|
|
@ -38,6 +57,7 @@ public class OpenSeeLauncher : MonoBehaviour {
|
|||
private Process trackerProcess = null;
|
||||
private StringBuilder trackerSB = null;
|
||||
private bool dontPrintNow = false;
|
||||
private System.IntPtr processHandle = System.IntPtr.Zero;
|
||||
private Job job = null;
|
||||
|
||||
private bool CheckSetup(bool requireTarget) {
|
||||
|
|
@ -85,9 +105,37 @@ public class OpenSeeLauncher : MonoBehaviour {
|
|||
return true;
|
||||
}
|
||||
|
||||
private string[] EscapiListCameras_x64() {
|
||||
List<string> cameras = new List<string>();
|
||||
int count = countCaptureDevices_x64();
|
||||
for (int i = 0; i < count; i++) {
|
||||
StringBuilder namebuffer = new StringBuilder(2048);
|
||||
getCaptureDeviceName_x64(i, namebuffer, 2048);
|
||||
cameras.Add(namebuffer.ToString());
|
||||
}
|
||||
return cameras.ToArray();
|
||||
}
|
||||
|
||||
private string[] EscapiListCameras_x86() {
|
||||
List<string> cameras = new List<string>();
|
||||
int count = countCaptureDevices_x86();
|
||||
for (int i = 0; i < count; i++) {
|
||||
StringBuilder namebuffer = new StringBuilder(2048);
|
||||
getCaptureDeviceName_x86(i, namebuffer, 2048);
|
||||
cameras.Add(namebuffer.ToString());
|
||||
}
|
||||
return cameras.ToArray();
|
||||
}
|
||||
|
||||
public string[] ListCameras() {
|
||||
if (!CheckSetup(false))
|
||||
return null;
|
||||
if (usePinvoke || usePinvokeListCameras) {
|
||||
if (Environment.Is64BitProcess)
|
||||
return EscapiListCameras_x64();
|
||||
else
|
||||
return EscapiListCameras_x86();
|
||||
}
|
||||
|
||||
StringBuilder stringBuilder;
|
||||
ProcessStartInfo processStartInfo;
|
||||
|
|
@ -151,16 +199,6 @@ public class OpenSeeLauncher : MonoBehaviour {
|
|||
if (!CheckSetup(true))
|
||||
return false;
|
||||
|
||||
ProcessStartInfo processStartInfo;
|
||||
|
||||
processStartInfo = new ProcessStartInfo();
|
||||
processStartInfo.CreateNoWindow = true;
|
||||
processStartInfo.RedirectStandardOutput = true;
|
||||
processStartInfo.RedirectStandardInput = true;
|
||||
processStartInfo.RedirectStandardError = true;
|
||||
processStartInfo.UseShellExecute = false;
|
||||
processStartInfo.FileName = exePath;
|
||||
|
||||
List<string> arguments = new List<string>();
|
||||
arguments.Add("--ip");
|
||||
arguments.Add(ip);
|
||||
|
|
@ -177,7 +215,13 @@ public class OpenSeeLauncher : MonoBehaviour {
|
|||
arguments.Add(argument);
|
||||
}
|
||||
string argumentString = EscapeArguments(arguments.ToArray());
|
||||
processStartInfo.Arguments = argumentString;
|
||||
|
||||
if (processHandle != System.IntPtr.Zero) {
|
||||
if (OpenSeeProcessInterface.Alive(processHandle))
|
||||
OpenSeeProcessInterface.TerminateProcess(processHandle, 0);
|
||||
OpenSeeProcessInterface.CloseHandle(processHandle);
|
||||
processHandle = System.IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (trackerProcess != null && !trackerProcess.HasExited) {
|
||||
trackerProcess.CloseMainWindow();
|
||||
|
|
@ -190,32 +234,58 @@ public class OpenSeeLauncher : MonoBehaviour {
|
|||
}
|
||||
}
|
||||
|
||||
trackerSB = new StringBuilder();
|
||||
trackerProcess = new Process();
|
||||
trackerProcess.StartInfo = processStartInfo;
|
||||
dontPrintNow = dontPrint;
|
||||
if (!dontPrintNow) {
|
||||
trackerProcess.EnableRaisingEvents = true;
|
||||
trackerProcess.OutputDataReceived += new DataReceivedEventHandler(delegate(object sender, DataReceivedEventArgs e) {
|
||||
trackerSB.Append(e.Data);
|
||||
trackerSB.Append("\n");
|
||||
});
|
||||
trackerProcess.ErrorDataReceived += new DataReceivedEventHandler(delegate(object sender, DataReceivedEventArgs e) {
|
||||
trackerSB.Append(e.Data);
|
||||
trackerSB.Append("\n");
|
||||
});
|
||||
}
|
||||
trackerProcess.Start();
|
||||
job.AddProcess(trackerProcess.Handle);
|
||||
if (!dontPrintNow) {
|
||||
trackerProcess.BeginOutputReadLine();
|
||||
trackerProcess.BeginErrorReadLine();
|
||||
}
|
||||
if (!usePinvoke) {
|
||||
ProcessStartInfo processStartInfo;
|
||||
processStartInfo = new ProcessStartInfo();
|
||||
processStartInfo.CreateNoWindow = true;
|
||||
processStartInfo.RedirectStandardOutput = true;
|
||||
processStartInfo.RedirectStandardInput = true;
|
||||
processStartInfo.RedirectStandardError = true;
|
||||
processStartInfo.UseShellExecute = false;
|
||||
processStartInfo.FileName = exePath;
|
||||
processStartInfo.Arguments = argumentString;
|
||||
|
||||
trackerSB = new StringBuilder();
|
||||
trackerProcess = new Process();
|
||||
trackerProcess.StartInfo = processStartInfo;
|
||||
dontPrintNow = dontPrint;
|
||||
if (!dontPrintNow) {
|
||||
trackerProcess.EnableRaisingEvents = true;
|
||||
trackerProcess.OutputDataReceived += new DataReceivedEventHandler(delegate(object sender, DataReceivedEventArgs e) {
|
||||
trackerSB.Append(e.Data);
|
||||
trackerSB.Append("\n");
|
||||
});
|
||||
trackerProcess.ErrorDataReceived += new DataReceivedEventHandler(delegate(object sender, DataReceivedEventArgs e) {
|
||||
trackerSB.Append(e.Data);
|
||||
trackerSB.Append("\n");
|
||||
});
|
||||
}
|
||||
trackerProcess.Start();
|
||||
job.AddProcess(trackerProcess.Handle);
|
||||
if (!dontPrintNow) {
|
||||
trackerProcess.BeginOutputReadLine();
|
||||
trackerProcess.BeginErrorReadLine();
|
||||
}
|
||||
|
||||
return !trackerProcess.HasExited;
|
||||
return !trackerProcess.HasExited;
|
||||
} else {
|
||||
string dir = Path.GetDirectoryName(exePath);
|
||||
OpenSeeProcessInterface.Start(exePath, "facetracker " + argumentString, dir, true, out processHandle);
|
||||
if (processHandle != System.IntPtr.Zero) {
|
||||
job.AddProcess(processHandle);
|
||||
return OpenSeeProcessInterface.Alive(processHandle);
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopTracker() {
|
||||
if (processHandle != System.IntPtr.Zero) {
|
||||
if (OpenSeeProcessInterface.Alive(processHandle))
|
||||
OpenSeeProcessInterface.TerminateProcess(processHandle, 0);
|
||||
OpenSeeProcessInterface.CloseHandle(processHandle);
|
||||
processHandle = System.IntPtr.Zero;
|
||||
}
|
||||
if (trackerProcess != null && !trackerProcess.HasExited) {
|
||||
trackerProcess.CloseMainWindow();
|
||||
trackerProcess.Close();
|
||||
|
|
@ -246,11 +316,16 @@ public class OpenSeeLauncher : MonoBehaviour {
|
|||
}
|
||||
|
||||
public void Update() {
|
||||
if (trackerProcess != null)
|
||||
trackerAlive = !trackerProcess.HasExited;
|
||||
else {
|
||||
trackerAlive = false;
|
||||
if (processHandle != System.IntPtr.Zero) {
|
||||
trackerAlive = OpenSeeProcessInterface.Alive(processHandle);
|
||||
return;
|
||||
} else {
|
||||
if (trackerProcess != null)
|
||||
trackerAlive = !trackerProcess.HasExited;
|
||||
else {
|
||||
trackerAlive = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (dontPrintNow || trackerSB == null)
|
||||
return;
|
||||
|
|
|
|||
169
Unity/OpenSeeProcessInterface.cs
Normal file
169
Unity/OpenSeeProcessInterface.cs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace OpenSee {
|
||||
|
||||
// Based on https://github.com/josh4364/IL2cppStartProcess/blob/master/StartExternalProcess.cs
|
||||
|
||||
public class OpenSeeProcessInterface {
|
||||
#region DllImport
|
||||
[DllImport("kernel32.dll", SetLastError=true)]
|
||||
private static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
|
||||
const UInt32 INFINITE = 0xFFFFFFFF;
|
||||
const UInt32 WAIT_ABANDONED = 0x00000080;
|
||||
const UInt32 WAIT_OBJECT_0 = 0x00000000;
|
||||
const UInt32 WAIT_TIMEOUT = 0x00000102;
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreateProcessW(
|
||||
string lpApplicationName,
|
||||
[In] string lpCommandLine,
|
||||
IntPtr procSecAttrs,
|
||||
IntPtr threadSecAttrs,
|
||||
bool bInheritHandles,
|
||||
ProcessCreationFlags dwCreationFlags,
|
||||
IntPtr lpEnvironment,
|
||||
string lpCurrentDirectory,
|
||||
ref STARTUPINFO lpStartupInfo,
|
||||
ref PROCESS_INFORMATION lpProcessInformation
|
||||
);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool TerminateProcess(IntPtr processHandle, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(ProcessAccessRights access, bool inherit, uint processId);
|
||||
|
||||
[Flags]
|
||||
private enum ProcessAccessRights : uint
|
||||
{
|
||||
PROCESS_CREATE_PROCESS = 0x0080, // Required to create a process.
|
||||
PROCESS_CREATE_THREAD = 0x0002, // Required to create a thread.
|
||||
PROCESS_DUP_HANDLE = 0x0040, // Required to duplicate a handle using DuplicateHandle.
|
||||
PROCESS_QUERY_INFORMATION = 0x0400, // Required to retrieve certain information about a process, such as its token, exit code, and priority class (see OpenProcessToken, GetExitCodeProcess, GetPriorityClass, and IsProcessInJob).
|
||||
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000, // Required to retrieve certain information about a process (see QueryFullProcessImageName). A handle that has the PROCESS_QUERY_INFORMATION access right is automatically granted PROCESS_QUERY_LIMITED_INFORMATION. Windows Server 2003 and Windows XP/2000: This access right is not supported.
|
||||
PROCESS_SET_INFORMATION = 0x0200, // Required to set certain information about a process, such as its priority class (see SetPriorityClass).
|
||||
PROCESS_SET_QUOTA = 0x0100, // Required to set memory limits using SetProcessWorkingSetSize.
|
||||
PROCESS_SUSPEND_RESUME = 0x0800, // Required to suspend or resume a process.
|
||||
PROCESS_TERMINATE = 0x0001, // Required to terminate a process using TerminateProcess.
|
||||
PROCESS_VM_OPERATION = 0x0008, // Required to perform an operation on the address space of a process (see VirtualProtectEx and WriteProcessMemory).
|
||||
PROCESS_VM_READ = 0x0010, // Required to read memory in a process using ReadProcessMemory.
|
||||
PROCESS_VM_WRITE = 0x0020, // Required to write to memory in a process using WriteProcessMemory.
|
||||
DELETE = 0x00010000, // Required to delete the object.
|
||||
READ_CONTROL = 0x00020000, // Required to read information in the security descriptor for the object, not including the information in the SACL. To read or write the SACL, you must request the ACCESS_SYSTEM_SECURITY access right. For more information, see SACL Access Right.
|
||||
SYNCHRONIZE = 0x00100000, // The right to use the object for synchronization. This enables a thread to wait until the object is in the signaled state.
|
||||
WRITE_DAC = 0x00040000, // Required to modify the DACL in the security descriptor for the object.
|
||||
WRITE_OWNER = 0x00080000, // Required to change the owner in the security descriptor for the object.
|
||||
STANDARD_RIGHTS_REQUIRED = 0x000f0000,
|
||||
PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFF // All possible access rights for a process object.
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PROCESS_INFORMATION
|
||||
{
|
||||
internal IntPtr hProcess;
|
||||
internal IntPtr hThread;
|
||||
internal uint dwProcessId;
|
||||
internal uint dwThreadId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct STARTUPINFO
|
||||
{
|
||||
internal uint cb;
|
||||
internal IntPtr lpReserved;
|
||||
internal IntPtr lpDesktop;
|
||||
internal IntPtr lpTitle;
|
||||
internal uint dwX;
|
||||
internal uint dwY;
|
||||
internal uint dwXSize;
|
||||
internal uint dwYSize;
|
||||
internal uint dwXCountChars;
|
||||
internal uint dwYCountChars;
|
||||
internal uint dwFillAttribute;
|
||||
internal uint dwFlags;
|
||||
internal ushort wShowWindow;
|
||||
internal ushort cbReserved2;
|
||||
internal IntPtr lpReserved2;
|
||||
internal IntPtr hStdInput;
|
||||
internal IntPtr hStdOutput;
|
||||
internal IntPtr hStdError;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum ProcessCreationFlags : uint
|
||||
{
|
||||
NONE = 0,
|
||||
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
|
||||
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
|
||||
CREATE_NEW_CONSOLE = 0x00000010,
|
||||
CREATE_NEW_PROCESS_GROUP = 0x00000200,
|
||||
CREATE_NO_WINDOW = 0x08000000,
|
||||
CREATE_PROTECTED_PROCESS = 0x00040000,
|
||||
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
|
||||
CREATE_SECURE_PROCESS = 0x00400000,
|
||||
CREATE_SEPARATE_WOW_VDM = 0x00000800,
|
||||
CREATE_SHARED_WOW_VDM = 0x00001000,
|
||||
CREATE_SUSPENDED = 0x00000004,
|
||||
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
|
||||
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
|
||||
DEBUG_PROCESS = 0x00000001,
|
||||
DETACHED_PROCESS = 0x00000008,
|
||||
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
|
||||
INHERIT_PARENT_AFFINITY = 0x00010000
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static uint Start(string exePath, string commandline, string dir, bool hidden, out IntPtr handle)
|
||||
{
|
||||
handle = IntPtr.Zero;
|
||||
ProcessCreationFlags flags = hidden ? ProcessCreationFlags.CREATE_NO_WINDOW : ProcessCreationFlags.NONE;
|
||||
STARTUPINFO startupinfo = new STARTUPINFO
|
||||
{
|
||||
cb = (uint)Marshal.SizeOf<STARTUPINFO>()
|
||||
};
|
||||
PROCESS_INFORMATION processinfo = new PROCESS_INFORMATION();
|
||||
if (!CreateProcessW(exePath, commandline, IntPtr.Zero, IntPtr.Zero, false, flags, IntPtr.Zero, dir, ref startupinfo, ref processinfo))
|
||||
{
|
||||
throw new Win32Exception();
|
||||
}
|
||||
handle = OpenProcess(ProcessAccessRights.PROCESS_ALL_ACCESS, false, processinfo.dwProcessId);
|
||||
|
||||
return processinfo.dwProcessId;
|
||||
}
|
||||
|
||||
public static bool Alive(IntPtr handle) {
|
||||
uint ret = WaitForSingleObject(handle, 0);
|
||||
return ret == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
public static int KillProcess(uint pid)
|
||||
{
|
||||
IntPtr handle = OpenProcess(ProcessAccessRights.PROCESS_ALL_ACCESS, false, pid);
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (!TerminateProcess(handle, 0))
|
||||
{
|
||||
throw new Win32Exception();
|
||||
}
|
||||
if (!CloseHandle(handle))
|
||||
{
|
||||
throw new Win32Exception();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
Unity/OpenSeeProcessInterface.cs.meta
Normal file
11
Unity/OpenSeeProcessInterface.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8130804d2fedd6d47a6cf0a447058d08
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Unity/Plugins/escapi_x64.dll
Normal file
BIN
Unity/Plugins/escapi_x64.dll
Normal file
Binary file not shown.
27
Unity/Plugins/escapi_x64.dll.meta
Normal file
27
Unity/Plugins/escapi_x64.dll.meta
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dd0c04eb3445c3048bfaa964c6bcb16f
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Unity/Plugins/escapi_x86.dll
Normal file
BIN
Unity/Plugins/escapi_x86.dll
Normal file
Binary file not shown.
27
Unity/Plugins/escapi_x86.dll.meta
Normal file
27
Unity/Plugins/escapi_x86.dll.meta
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a504a0f747083b54c99d6e91731ccecc
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Add table
Add a link
Reference in a new issue