Did a few things

This commit is contained in:
2021-07-23 09:45:12 +02:00
parent 3da75e4cc6
commit db537bd7b7
122 changed files with 15101 additions and 619 deletions

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 771cb9cc384ae3f40909b9958419a209
folderAsset: yes
timeCreated: 1535571973
licenseType: Free
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: c1aa92cd8198f654b933334ac725dc4f
timeCreated: 1538785894
licenseType: Free
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
Unity User Reporting SDK Version 1.0
Included all source in the SDK instead of relying on DLLs.
Upgraded from WWW to UnityWebRequest.
For 2018.3 and above, added a new IUserReportingPlatform for asynchronous screen shots and report generation (DirectX only). To enable this feature, switch the UserReportingPlatform to Async on the UserReporting game object.
Fixed an issue where successful user report submissions were reporting as an error.
Fixed a possible memory leak when taking screenshots.
Fixed various small bugs.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 502667b69f01dbc449740d3f231a9678
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f0d5c0c9b35b46242935c4d341d53f0b
folderAsset: yes
timeCreated: 1535571942
licenseType: Free
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Provides extensions for working with attachments.
/// </summary>
public static class AttachmentExtensions
{
#region Static Methods
/// <summary>
/// Adds a JSON attachment.
/// </summary>
/// <param name="instance">The extended instance.</param>
/// <param name="name">The name of the attachment.</param>
/// <param name="fileName">The file name.</param>
/// <param name="contents">The contents.</param>
public static void AddJson(this List<UserReportAttachment> instance, string name, string fileName, string contents)
{
if (instance != null)
{
instance.Add(new UserReportAttachment(name, fileName, "application/json", System.Text.Encoding.UTF8.GetBytes(contents)));
}
}
/// <summary>
/// Adds a text attachment.
/// </summary>
/// <param name="instance">The extended instance.</param>
/// <param name="name">The name of the attachment.</param>
/// <param name="fileName">The file name.</param>
/// <param name="contents">The contents.</param>
public static void AddText(this List<UserReportAttachment> instance, string name, string fileName, string contents)
{
if (instance != null)
{
instance.Add(new UserReportAttachment(name, fileName, "text/plain", System.Text.Encoding.UTF8.GetBytes(contents)));
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,346 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Unity.Cloud
{
/// <summary>
/// Represents a cyclical list.
/// </summary>
/// <typeparam name="T">The type of the items in the list.</typeparam>
public class CyclicalList<T> : IList<T>
{
#region Nested Types
/// <summary>
/// Represents an enumerator for a cyclical list.
/// </summary>
private struct Enumerator : IEnumerator<T>
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="Enumerator"/> class.
/// </summary>
/// <param name="list">The list.</param>
public Enumerator(CyclicalList<T> list)
{
this.list = list;
this.currentIndex = -1;
}
#endregion
#region Fields
private int currentIndex;
private CyclicalList<T> list;
#endregion
#region Properties
/// <summary>
/// Gets the current item.
/// </summary>
public T Current
{
get
{
if (this.currentIndex < 0 || this.currentIndex >= this.list.Count)
{
return default(T);
}
return this.list[this.currentIndex];
}
}
/// <summary>
/// Gets the current item.
/// </summary>
object IEnumerator.Current
{
get { return this.Current; }
}
#endregion
#region Methods
/// <summary>
/// Disposes of the enumerator.
/// </summary>
public void Dispose()
{
// Empty
}
/// <summary>
/// Moves to the next item.
/// </summary>
/// <returns>A value indicating whether the move was successful.</returns>
public bool MoveNext()
{
this.currentIndex++;
return this.currentIndex < this.list.count;
}
/// <summary>
/// Resets the enumerator.
/// </summary>
public void Reset()
{
this.currentIndex = 0;
}
#endregion
}
#endregion
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="CyclicalList{T}"/> class.
/// </summary>
/// <param name="capacity">The capacity.</param>
public CyclicalList(int capacity)
{
this.items = new T[capacity];
}
#endregion
#region Fields
private int count;
private T[] items;
private int nextPointer;
#endregion
#region Properties
/// <summary>
/// Gets the capacity.
/// </summary>
public int Capacity
{
get { return this.items.Length; }
}
/// <summary>
/// Gets the count.
/// </summary>
public int Count
{
get { return this.count; }
}
/// <summary>
/// Gets a value indicating whether the cyclical list is read only.
/// </summary>
public bool IsReadOnly
{
get { return false; }
}
/// <summary>
/// Indexes into the cyclical list.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item at the specified index.</returns>
public T this[int index]
{
get
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
return this.items[this.GetPointer(index)];
}
set
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
this.items[this.GetPointer(index)] = value;
}
}
#endregion
#region Methods
/// <summary>
/// Adds an item to the cyclical list.
/// </summary>
/// <param name="item">The item.</param>
public void Add(T item)
{
this.items[this.nextPointer] = item;
this.count++;
if (this.count > this.items.Length)
{
this.count = this.items.Length;
}
this.nextPointer++;
if (this.nextPointer >= this.items.Length)
{
this.nextPointer = 0;
}
}
/// <summary>
/// Clears the cyclical list.
/// </summary>
public void Clear()
{
this.count = 0;
this.nextPointer = 0;
}
/// <summary>
/// Determines whether the cyclical list contains the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A value indicating whether the cyclical list contains the specified item.</returns>
public bool Contains(T item)
{
foreach (T listItem in this)
{
if (listItem.Equals(item))
{
return true;
}
}
return false;
}
/// <summary>
/// Copies the cyclical list to an array.
/// </summary>
/// <param name="array">The array.</param>
/// <param name="arrayIndex">The array index.</param>
public void CopyTo(T[] array, int arrayIndex)
{
int i = 0;
foreach (T listItem in this)
{
int currentArrayIndex = arrayIndex + i;
if (currentArrayIndex >= array.Length)
{
break;
}
array[currentArrayIndex] = listItem;
i++;
}
}
/// <summary>
/// Gets the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// Gets the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
/// <summary>
/// Gets the next eviction.
/// </summary>
/// <returns>The next eviction.</returns>
public T GetNextEviction()
{
return this.items[this.nextPointer];
}
/// <summary>
/// Gets a pointer.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The pointer.</returns>
private int GetPointer(int index)
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
if (this.count < this.items.Length)
{
return index;
}
return (this.nextPointer + index) % this.count;
}
/// <summary>
/// Gets the index of the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The index of the specified item.</returns>
public int IndexOf(T item)
{
int i = 0;
foreach (T listItem in this)
{
if (listItem.Equals(item))
{
return i;
}
i++;
}
return -1;
}
/// <summary>
/// Inserts an item into the cyclical list. This is a no-op on a cyclical list.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="item">The item.</param>
public void Insert(int index, T item)
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
}
/// <summary>
/// Removes an item from the cyclical list.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A value indicating whether the removal was successful. This is a no-op on a cyclical list.</returns>
public bool Remove(T item)
{
return false;
}
/// <summary>
/// Removes an item from the cyclical list at the specified index. This is a no-op on a cyclical list.
/// </summary>
/// <param name="index">The index.</param>
public void RemoveAt(int index)
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents a user reporting platform.
/// </summary>
public interface IUserReportingPlatform
{
#region Methods
/// <summary>
/// Deserialized the specified JSON.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="json">The JSON.</param>
/// <returns>The deserialized object instance.</returns>
T DeserializeJson<T>(string json);
/// <summary>
/// Gets device metadata.
/// </summary>
/// <returns>Device metadata.</returns>
IDictionary<string, string> GetDeviceMetadata();
/// <summary>
/// Modifies a user report.
/// </summary>
/// <param name="userReport">The user report.</param>
void ModifyUserReport(UserReport userReport);
/// <summary>
/// Called at the end of a frame.
/// </summary>
/// <param name="client">The client.</param>
void OnEndOfFrame(UserReportingClient client);
/// <summary>
/// Posts to an endpoint.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="contentType">The content type.</param>
/// <param name="content">The content.</param>
/// <param name="progressCallback">The progress callback. Provides the upload and download progress.</param>
/// <param name="callback">The callback. Provides a value indicating whether the post was successful and provides the resulting byte array.</param>
void Post(string endpoint, string contentType, byte[] content, Action<float, float> progressCallback, Action<bool, byte[]> callback);
/// <summary>
/// Runs a task asynchronously.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="callback">The callback.</param>
void RunTask(Func<object> task, Action<object> callback);
/// <summary>
/// Sends an analytics event.
/// </summary>
/// <param name="eventName">The event name.</param>
/// <param name="eventData">The event data.</param>
void SendAnalyticsEvent(string eventName, Dictionary<string, object> eventData);
/// <summary>
/// Serializes the specified object instance.
/// </summary>
/// <param name="instance">The object instance.</param>
/// <returns>The JSON.</returns>
string SerializeJson(object instance);
/// <summary>
/// Takes a screenshot.
/// </summary>
/// <param name="frameNumber">The frame number.</param>
/// <param name="maximumWidth">The maximum width.</param>
/// <param name="maximumHeight">The maximum height.</param>
/// <param name="source">The source. Passing null will capture the screen. Passing a camera will capture the camera's view. Passing a render texture will capture the render texture.</param>
/// <param name="callback">The callback. Provides the screenshot.</param>
void TakeScreenshot(int frameNumber, int maximumWidth, int maximumHeight, object source, Action<int, byte[]> callback);
/// <summary>
/// Called on update.
/// </summary>
/// <param name="client">The client.</param>
void Update(UserReportingClient client);
#endregion
}
}

View File

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

View File

@@ -0,0 +1,13 @@
namespace Unity.Cloud.Authorization
{
public enum LicenseLevel
{
None = 0,
Personal = 10,
Plus = 20,
Pro = 30,
}
}

View File

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

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents a metrics gathering mode.
/// </summary>
public enum MetricsGatheringMode
{
/// <summary>
/// Automatic. Some metrics are gathered automatically.
/// </summary>
Automatic = 0,
/// <summary>
/// Manual. No metrics are gathered automatically.
/// </summary>
Manual = 1,
/// <summary>
/// Disabled. No metrics are gathered. Sampling a metric is a no-op.
/// </summary>
Disabled = 2
}
}

View File

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

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents a null user reporting platform.
/// </summary>
public class NullUserReportingPlatform : IUserReportingPlatform
{
#region Methods
/// <inheritdoc cref="IUserReportingPlatform"/>
public T DeserializeJson<T>(string json)
{
return default(T);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public IDictionary<string, string> GetDeviceMetadata()
{
return new Dictionary<string, string>();
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void ModifyUserReport(UserReport userReport)
{
// Empty
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void OnEndOfFrame(UserReportingClient client)
{
// Empty
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void Post(string endpoint, string contentType, byte[] content, Action<float, float> progressCallback, Action<bool, byte[]> callback)
{
progressCallback(1, 1);
callback(true, content);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void RunTask(Func<object> task, Action<object> callback)
{
callback(task());
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void SendAnalyticsEvent(string eventName, Dictionary<string, object> eventData)
{
// Empty
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public string SerializeJson(object instance)
{
return string.Empty;
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void TakeScreenshot(int frameNumber, int maximumWidth, int maximumHeight, object source, Action<int, byte[]> callback)
{
callback(frameNumber, new byte[0]);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void Update(UserReportingClient client)
{
// Empty
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,80 @@
using System;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Provides static methods for helping with PNG images.
/// </summary>
public static class PngHelper
{
#region Static Methods
/// <summary>
/// Gets a PNG image's height from base 64 encoded data.
/// </summary>
/// <param name="data">The data.</param>
/// <returns>The height.</returns>
public static int GetPngHeightFromBase64Data(string data)
{
// Preconditions
if (data == null || data.Length < 32)
{
return 0;
}
// Implementation
byte[] bytes = Convert.FromBase64String(data.Substring(0, 32));
byte[] heightBytes = PngHelper.Slice(bytes, 20, 24);
Array.Reverse(heightBytes);
int height = BitConverter.ToInt32(heightBytes, 0);
return height;
}
/// <summary>
/// Gets a PNG image's width from base 64 encoded data.
/// </summary>
/// <param name="data">The data.</param>
/// <returns>The width.</returns>
public static int GetPngWidthFromBase64Data(string data)
{
// Preconditions
if (data == null || data.Length < 32)
{
return 0;
}
// Implementation
byte[] bytes = Convert.FromBase64String(data.Substring(0, 32));
byte[] widthBytes = PngHelper.Slice(bytes, 16, 20);
Array.Reverse(widthBytes);
int width = BitConverter.ToInt32(widthBytes, 0);
return width;
}
/// <summary>
/// Slices a byte array.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
/// <returns>The slices byte array.</returns>
private static byte[] Slice(byte[] source, int start, int end)
{
if (end < 0)
{
end = source.Length + end;
}
int len = end - start;
byte[] res = new byte[len];
for (int i = 0; i < len; i++)
{
res[i] = source[i + start];
}
return res;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,47 @@
using System;
namespace Unity.Cloud
{
/// <summary>
/// Provides static methods for helping with preconditions.
/// </summary>
public static class Preconditions
{
#region Static Methods
/// <summary>
/// Ensures that an argument is less than or equal to the specified length.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="length">The length.</param>
/// <param name="argumentName">The argument name.</param>
public static void ArgumentIsLessThanOrEqualToLength(object value, int length, string argumentName)
{
string stringValue = value as string;
if (stringValue != null && stringValue.Length > length)
{
throw new ArgumentException(argumentName);
}
}
/// <summary>
/// Ensures that an argument is not null or whitespace.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="argumentName">The argument name.</param>
public static void ArgumentNotNullOrWhitespace(object value, string argumentName)
{
if (value == null)
{
throw new ArgumentNullException(argumentName);
}
string stringValue = value as string;
if (stringValue != null && stringValue.Trim() == string.Empty)
{
throw new ArgumentNullException(argumentName);
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Unity.Cloud
{
/// <summary>
/// Represents a serializable exception.
/// </summary>
public class SerializableException
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="SerializableException"/> class.
/// </summary>
public SerializableException()
{
// Empty
}
/// <summary>
/// Creates a new instance of the <see cref="SerializableException"/> class.
/// </summary>
/// <param name="exception">The exception.</param>
public SerializableException(Exception exception)
{
// Message
this.Message = exception.Message;
// Full Text
this.FullText = exception.ToString();
// Type
Type exceptionType = exception.GetType();
this.Type = exceptionType.FullName;
// Stack Trace
this.StackTrace = new List<SerializableStackFrame>();
StackTrace stackTrace = new StackTrace(exception, true);
foreach (StackFrame stackFrame in stackTrace.GetFrames())
{
this.StackTrace.Add(new SerializableStackFrame(stackFrame));
}
// Problem Identifier
if (this.StackTrace.Count > 0)
{
SerializableStackFrame stackFrame = this.StackTrace[0];
this.ProblemIdentifier = string.Format("{0} at {1}.{2}", this.Type, stackFrame.DeclaringType, stackFrame.MethodName);
}
else
{
this.ProblemIdentifier = this.Type;
}
// Detailed Problem Identifier
if (this.StackTrace.Count > 1)
{
SerializableStackFrame stackFrame1 = this.StackTrace[0];
SerializableStackFrame stackFrame2 = this.StackTrace[1];
this.DetailedProblemIdentifier = string.Format("{0} at {1}.{2} from {3}.{4}", this.Type, stackFrame1.DeclaringType, stackFrame1.MethodName, stackFrame2.DeclaringType, stackFrame2.MethodName);
}
// Inner Exception
if (exception.InnerException != null)
{
this.InnerException = new SerializableException(exception.InnerException);
}
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the detailed problem identifier.
/// </summary>
public string DetailedProblemIdentifier { get; set; }
/// <summary>
/// Gets or sets the full text.
/// </summary>
public string FullText { get; set; }
/// <summary>
/// Gets or sets the inner exception.
/// </summary>
public SerializableException InnerException { get; set; }
/// <summary>
/// Gets or sets the message.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets the problem identifier.
/// </summary>
public string ProblemIdentifier { get; set; }
/// <summary>
/// Gets or sets the stack trace.
/// </summary>
public List<SerializableStackFrame> StackTrace { get; set; }
/// <summary>
/// Gets or sets the type.
/// </summary>
public string Type { get; set; }
#endregion
}
}

View File

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

View File

@@ -0,0 +1,74 @@
using System;
using System.Diagnostics;
using System.Reflection;
namespace Unity.Cloud
{
/// <summary>
/// Represents a serializable stack frame.
/// </summary>
public class SerializableStackFrame
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="SerializableStackFrame"/> class.
/// </summary>
public SerializableStackFrame()
{
// Empty
}
/// <summary>
/// Creates a new instance of the <see cref="SerializableStackFrame"/> class.
/// </summary>
/// <param name="stackFrame">The stack frame.</param>
public SerializableStackFrame(StackFrame stackFrame)
{
MethodBase method = stackFrame.GetMethod();
Type declaringType = method.DeclaringType;
this.DeclaringType = declaringType != null ? declaringType.FullName : null;
this.Method = method.ToString();
this.MethodName = method.Name;
this.FileName = stackFrame.GetFileName();
this.FileLine = stackFrame.GetFileLineNumber();
this.FileColumn = stackFrame.GetFileColumnNumber();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the declaring type.
/// </summary>
public string DeclaringType { get; set; }
/// <summary>
/// Gets or sets the file column.
/// </summary>
public int FileColumn { get; set; }
/// <summary>
/// Gets or sets the file line.
/// </summary>
public int FileLine { get; set; }
/// <summary>
/// Gets or sets the file name.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets the method.
/// </summary>
public string Method { get; set; }
/// <summary>
/// Gets or sets the method name.
/// </summary>
public string MethodName { get; set; }
#endregion
}
}

View File

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

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report.
/// </summary>
public class UserReport : UserReportPreview
{
#region Nested Types
/// <summary>
/// Provides sorting for metrics.
/// </summary>
private class UserReportMetricSorter : IComparer<UserReportMetric>
{
#region Methods
/// <inheritdoc />
public int Compare(UserReportMetric x, UserReportMetric y)
{
return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
}
#endregion
}
#endregion
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReport"/> class.
/// </summary>
public UserReport()
{
this.AggregateMetrics = new List<UserReportMetric>();
this.Attachments = new List<UserReportAttachment>();
this.ClientMetrics = new List<UserReportMetric>();
this.DeviceMetadata = new List<UserReportNamedValue>();
this.Events = new List<UserReportEvent>();
this.Fields = new List<UserReportNamedValue>();
this.Measures = new List<UserReportMeasure>();
this.Screenshots = new List<UserReportScreenshot>();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the attachments.
/// </summary>
public List<UserReportAttachment> Attachments { get; set; }
/// <summary>
/// Gets or sets the client metrics.
/// </summary>
public List<UserReportMetric> ClientMetrics { get; set; }
/// <summary>
/// Gets or sets the device metadata.
/// </summary>
public List<UserReportNamedValue> DeviceMetadata { get; set; }
/// <summary>
/// Gets or sets the events.
/// </summary>
public List<UserReportEvent> Events { get; set; }
/// <summary>
/// Gets or sets the fields.
/// </summary>
public List<UserReportNamedValue> Fields { get; set; }
/// <summary>
/// Gets or sets the measures.
/// </summary>
public List<UserReportMeasure> Measures { get; set; }
/// <summary>
/// Gets or sets the screenshots.
/// </summary>
public List<UserReportScreenshot> Screenshots { get; set; }
#endregion
#region Methods
/// <summary>
/// Clones the user report.
/// </summary>
/// <returns>The cloned user report.</returns>
public UserReport Clone()
{
UserReport userReport = new UserReport();
userReport.AggregateMetrics = this.AggregateMetrics != null ? this.AggregateMetrics.ToList() : null;
userReport.Attachments = this.Attachments != null ? this.Attachments.ToList() : null;
userReport.ClientMetrics = this.ClientMetrics != null ? this.ClientMetrics.ToList() : null;
userReport.ContentLength = this.ContentLength;
userReport.DeviceMetadata = this.DeviceMetadata != null ? this.DeviceMetadata.ToList() : null;
userReport.Dimensions = this.Dimensions.ToList();
userReport.Events = this.Events != null ? this.Events.ToList() : null;
userReport.ExpiresOn = this.ExpiresOn;
userReport.Fields = this.Fields != null ? this.Fields.ToList() : null;
userReport.Identifier = this.Identifier;
userReport.IPAddress = this.IPAddress;
userReport.Measures = this.Measures != null ? this.Measures.ToList() : null;
userReport.ProjectIdentifier = this.ProjectIdentifier;
userReport.ReceivedOn = this.ReceivedOn;
userReport.Screenshots = this.Screenshots != null ? this.Screenshots.ToList() : null;
userReport.Summary = this.Summary;
userReport.Thumbnail = this.Thumbnail;
return userReport;
}
/// <summary>
/// Completes the user report. This is called by the client and only needs to be called when constructing a user report manually.
/// </summary>
public void Complete()
{
// Thumbnail
if (this.Screenshots.Count > 0)
{
this.Thumbnail = this.Screenshots[this.Screenshots.Count - 1];
}
// Aggregate Metrics
Dictionary<string, UserReportMetric> aggregateMetrics = new Dictionary<string, UserReportMetric>();
foreach (UserReportMeasure measure in this.Measures)
{
foreach (UserReportMetric metric in measure.Metrics)
{
if (!aggregateMetrics.ContainsKey(metric.Name))
{
UserReportMetric userReportMetric = new UserReportMetric();
userReportMetric.Name = metric.Name;
aggregateMetrics.Add(metric.Name, userReportMetric);
}
UserReportMetric aggregateMetric = aggregateMetrics[metric.Name];
aggregateMetric.Sample(metric.Average);
aggregateMetrics[metric.Name] = aggregateMetric;
}
}
if (this.AggregateMetrics == null)
{
this.AggregateMetrics = new List<UserReportMetric>();
}
foreach (KeyValuePair<string, UserReportMetric> kvp in aggregateMetrics)
{
this.AggregateMetrics.Add(kvp.Value);
}
this.AggregateMetrics.Sort(new UserReportMetricSorter());
}
/// <summary>
/// Fixes the user report by replace null lists with empty lists.
/// </summary>
public void Fix()
{
this.AggregateMetrics = this.AggregateMetrics ?? new List<UserReportMetric>();
this.Attachments = this.Attachments ?? new List<UserReportAttachment>();
this.ClientMetrics = this.ClientMetrics ?? new List<UserReportMetric>();
this.DeviceMetadata = this.DeviceMetadata ?? new List<UserReportNamedValue>();
this.Dimensions = this.Dimensions ?? new List<UserReportNamedValue>();
this.Events = this.Events ?? new List<UserReportEvent>();
this.Fields = this.Fields ?? new List<UserReportNamedValue>();
this.Measures = this.Measures ?? new List<UserReportMeasure>();
this.Screenshots = this.Screenshots ?? new List<UserReportScreenshot>();
}
/// <summary>
/// Gets the dimension string for the dimensions associated with this user report.
/// </summary>
/// <returns></returns>
public string GetDimensionsString()
{
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < this.Dimensions.Count; i++)
{
UserReportNamedValue dimension = this.Dimensions[i];
stringBuilder.Append(dimension.Name);
stringBuilder.Append(": ");
stringBuilder.Append(dimension.Value);
if (i != this.Dimensions.Count - 1)
{
stringBuilder.Append(", ");
}
}
return stringBuilder.ToString();
}
/// <summary>
/// Removes screenshots above a certain size from the user report.
/// </summary>
/// <param name="maximumWidth">The maximum width.</param>
/// <param name="maximumHeight">The maximum height.</param>
/// <param name="totalBytes">The total bytes allowed by screenshots.</param>
/// <param name="ignoreCount">The number of screenshots to ignoreCount.</param>
public void RemoveScreenshots(int maximumWidth, int maximumHeight, int totalBytes, int ignoreCount)
{
int byteCount = 0;
for (int i = this.Screenshots.Count; i > 0; i--)
{
if (i < ignoreCount)
{
continue;
}
UserReportScreenshot screenshot = this.Screenshots[i];
byteCount += screenshot.DataBase64.Length;
if (byteCount > totalBytes)
{
break;
}
if (screenshot.Width > maximumWidth || screenshot.Height > maximumHeight)
{
this.Screenshots.RemoveAt(i);
}
}
}
/// <summary>
/// Casts the user report to a user report preview.
/// </summary>
/// <returns>The user report preview.</returns>
public UserReportPreview ToPreview()
{
UserReportPreview userReportPreview = new UserReportPreview();
userReportPreview.AggregateMetrics = this.AggregateMetrics != null ? this.AggregateMetrics.ToList() : null;
userReportPreview.ContentLength = this.ContentLength;
userReportPreview.Dimensions = this.Dimensions != null ? this.Dimensions.ToList() : null;
userReportPreview.ExpiresOn = this.ExpiresOn;
userReportPreview.Identifier = this.Identifier;
userReportPreview.IPAddress = this.IPAddress;
userReportPreview.ProjectIdentifier = this.ProjectIdentifier;
userReportPreview.ReceivedOn = this.ReceivedOn;
userReportPreview.Summary = this.Summary;
userReportPreview.Thumbnail = this.Thumbnail;
return userReportPreview;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,28 @@
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report appearance hint.
/// </summary>
public enum UserReportAppearanceHint
{
/// <summary>
/// Display normally.
/// </summary>
Normal = 0,
/// <summary>
/// Display landscape.
/// </summary>
Landscape = 1,
/// <summary>
/// Display portrait.
/// </summary>
Portrait = 2,
/// <summary>
/// Display large.
/// </summary>
Large = 3
}
}

View File

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

View File

@@ -0,0 +1,59 @@
using System;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report attachment.
/// </summary>
public struct UserReportAttachment
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportAttachment"/> struct.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="fileName">The file name.</param>
/// <param name="contentType">The content type.</param>
/// <param name="data">The data.</param>
public UserReportAttachment(string name, string fileName, string contentType, byte[] data)
{
this.Name = name;
this.FileName = fileName;
this.ContentType = contentType;
this.DataBase64 = Convert.ToBase64String(data);
this.DataIdentifier = null;
}
#endregion
#region Properties
/// <summary>
/// Get or sets the content type.
/// </summary>
public string ContentType { get; set; }
/// <summary>
/// Gets or sets the data (base 64 encoded).
/// </summary>
public string DataBase64 { get; set; }
/// <summary>
/// Gets or sets the data identifier. This property will be overwritten by the server if provided.
/// </summary>
public string DataIdentifier { get; set; }
/// <summary>
/// Gets or sets the file name.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
#endregion
}
}

View File

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

View File

@@ -0,0 +1,52 @@
using System;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report event.
/// </summary>
public struct UserReportEvent
{
#region Properties
/// <summary>
/// Gets or sets the exception.
/// </summary>
public SerializableException Exception { get; set; }
/// <summary>
/// Gets or sets the frame number.
/// </summary>
public int FrameNumber { get; set; }
/// <summary>
/// Gets or sets the full message.
/// </summary>
public string FullMessage
{
get { return string.Format("{0}{1}{2}", this.Message, Environment.NewLine, this.StackTrace); }
}
/// <summary>
/// Gets or sets the level.
/// </summary>
public UserReportEventLevel Level { get; set; }
/// <summary>
/// Gets or sets the message.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets the stack trace.
/// </summary>
public string StackTrace { get; set; }
/// <summary>
/// Gets or sets the timestamp.
/// </summary>
public DateTime Timestamp { get; set; }
#endregion
}
}

View File

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

View File

@@ -0,0 +1,28 @@
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report event level.
/// </summary>
public enum UserReportEventLevel
{
/// <summary>
/// Info.
/// </summary>
Info = 0,
/// <summary>
/// Success.
/// </summary>
Success = 1,
/// <summary>
/// Warning.
/// </summary>
Warning = 2,
/// <summary>
/// Error.
/// </summary>
Error = 3
}
}

View File

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

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report list.
/// </summary>
public class UserReportList
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportList"/> class.
/// </summary>
public UserReportList()
{
this.UserReportPreviews = new List<UserReportPreview>();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the continuation token.
/// </summary>
public string ContinuationToken { get; set; }
/// <summary>
/// Gets or sets the error.
/// </summary>
public string Error { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the list has more items.
/// </summary>
public bool HasMore { get; set; }
/// <summary>
/// Gets or sets the user report previews.
/// </summary>
public List<UserReportPreview> UserReportPreviews { get; set; }
#endregion
#region Methods
/// <summary>
/// Completes the list. This only need to be called by the creator of the list.
/// </summary>
/// <param name="originalLimit">The original limit.</param>
/// <param name="continuationToken">The continuation token.</param>
public void Complete(int originalLimit, string continuationToken)
{
if (this.UserReportPreviews.Count > 0)
{
if (this.UserReportPreviews.Count > originalLimit)
{
while (this.UserReportPreviews.Count > originalLimit)
{
this.UserReportPreviews.RemoveAt(this.UserReportPreviews.Count - 1);
}
this.ContinuationToken = continuationToken;
this.HasMore = true;
}
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report measure.
/// </summary>
public struct UserReportMeasure
{
#region Properties
/// <summary>
/// Gets or sets the end frame number.
/// </summary>
public int EndFrameNumber { get; set; }
/// <summary>
/// Gets or sets the metadata.
/// </summary>
public List<UserReportNamedValue> Metadata { get; set; }
/// <summary>
/// Gets or sets the metrics.
/// </summary>
public List<UserReportMetric> Metrics { get; set; }
/// <summary>
/// Gets or sets the start frame number.
/// </summary>
public int StartFrameNumber { get; set; }
#endregion
}
}

View File

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

View File

@@ -0,0 +1,69 @@
using System;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report metrics.
/// </summary>
public struct UserReportMetric
{
#region Properties
/// <summary>
/// Gets the average.
/// </summary>
public double Average
{
get { return this.Sum / this.Count; }
}
/// <summary>
/// Gets the count.
/// </summary>
public int Count { get; set; }
/// <summary>
/// Gets the maximum.
/// </summary>
public double Maximum { get; set; }
/// <summary>
/// Gets the minimum.
/// </summary>
public double Minimum { get; set; }
/// <summary>
/// Gets the name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets the sum.
/// </summary>
public double Sum { get; set; }
#endregion
#region Methods
/// <summary>
/// Samples a value.
/// </summary>
/// <param name="value">The value.</param>
public void Sample(double value)
{
if (this.Count == 0)
{
this.Minimum = double.MaxValue;
this.Maximum = double.MinValue;
}
this.Count++;
this.Sum += value;
this.Minimum = Math.Min(this.Minimum, value);
this.Maximum = Math.Max(this.Maximum, value);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,37 @@
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report named value.
/// </summary>
public struct UserReportNamedValue
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportNamedValue"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public UserReportNamedValue(string name, string value)
{
this.Name = name;
this.Value = value;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
public string Value { get; set; }
#endregion
}
}

View File

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

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using Unity.Cloud.Authorization;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report preview or the fly weight version of a user report.
/// </summary>
public class UserReportPreview
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportPreview"/> class.
/// </summary>
public UserReportPreview()
{
this.Dimensions = new List<UserReportNamedValue>();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the aggregate metrics.
/// </summary>
public List<UserReportMetric> AggregateMetrics { get; set; }
/// <summary>
/// Gets or sets the appearance hint.
/// </summary>
public UserReportAppearanceHint AppearanceHint { get; set; }
/// <summary>
/// Gets or sets the content length. This property will be overwritten by the server if provided.
/// </summary>
public long ContentLength { get; set; }
/// <summary>
/// Gets or sets the dimensions.
/// </summary>
public List<UserReportNamedValue> Dimensions { get; set; }
/// <summary>
/// Gets or sets the time at which the user report expires. This property will be overwritten by the server if provided.
/// </summary>
public DateTime ExpiresOn { get; set; }
/// <summary>
/// Gets or sets the geo country.
/// </summary>
public string GeoCountry { get; set; }
/// <summary>
/// Gets or sets the identifier. This property will be overwritten by the server if provided.
/// </summary>
public string Identifier { get; set; }
/// <summary>
/// Gets or sets the IP address. This property will be overwritten by the server if provided.
/// </summary>
public string IPAddress { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user report is hidden in the UI if a dimension filter is not specified. This is recommended for automated or high volume reports.
/// </summary>
public bool IsHiddenWithoutDimension { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user report is silent. Silent user reports do not send events to integrations. This is recommended for automated or high volume reports.
/// </summary>
public bool IsSilent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user report is temporary. Temporary user reports are short lived and not queryable.
/// </summary>
public bool IsTemporary { get; set; }
/// <summary>
/// Gets or sets the license level. This property will be overwritten by the server if provided.
/// </summary>
public LicenseLevel LicenseLevel { get; set; }
/// <summary>
/// Gets or sets the project identifier.
/// </summary>
public string ProjectIdentifier { get; set; }
/// <summary>
/// Gets or sets the time at which the user report was received. This property will be overwritten by the server if provided.
/// </summary>
public DateTime ReceivedOn { get; set; }
/// <summary>
/// Gets or sets the summary.
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Gets or sets the thumbnail. This screenshot will be resized by the server if too large. Keep the last screenshot small in order to reduce report size and increase submission speed.
/// </summary>
public UserReportScreenshot Thumbnail { get; set; }
#endregion
}
}

View File

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

View File

@@ -0,0 +1,43 @@
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report screenshot.
/// </summary>
public struct UserReportScreenshot
{
#region Properties
/// <summary>
/// Gets or sets the data (base 64 encoded). Screenshots must be in PNG format.
/// </summary>
public string DataBase64 { get; set; }
/// <summary>
/// Gets or sets the data identifier. This property will be overwritten by the server if provided.
/// </summary>
public string DataIdentifier { get; set; }
/// <summary>
/// Gets or sets the frame number.
/// </summary>
public int FrameNumber { get; set; }
/// <summary>
/// Gets the height.
/// </summary>
public int Height
{
get { return PngHelper.GetPngHeightFromBase64Data(this.DataBase64); }
}
/// <summary>
/// Gets the width.
/// </summary>
public int Width
{
get { return PngHelper.GetPngWidthFromBase64Data(this.DataBase64); }
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,665 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents a user reporting client.
/// </summary>
public class UserReportingClient
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportingClient"/> class.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="projectIdentifier">The project identifier.</param>
/// <param name="platform">The platform.</param>
/// <param name="configuration">The configuration.</param>
public UserReportingClient(string endpoint, string projectIdentifier, IUserReportingPlatform platform, UserReportingClientConfiguration configuration)
{
// Arguments
this.Endpoint = endpoint;
this.ProjectIdentifier = projectIdentifier;
this.Platform = platform;
this.Configuration = configuration;
// Configuration Clean Up
this.Configuration.FramesPerMeasure = this.Configuration.FramesPerMeasure > 0 ? this.Configuration.FramesPerMeasure : 1;
this.Configuration.MaximumEventCount = this.Configuration.MaximumEventCount > 0 ? this.Configuration.MaximumEventCount : 1;
this.Configuration.MaximumMeasureCount = this.Configuration.MaximumMeasureCount > 0 ? this.Configuration.MaximumMeasureCount : 1;
this.Configuration.MaximumScreenshotCount = this.Configuration.MaximumScreenshotCount > 0 ? this.Configuration.MaximumScreenshotCount : 1;
// Lists
this.clientMetrics = new Dictionary<string, UserReportMetric>();
this.currentMeasureMetadata = new Dictionary<string, string>();
this.currentMetrics = new Dictionary<string, UserReportMetric>();
this.events = new CyclicalList<UserReportEvent>(configuration.MaximumEventCount);
this.measures = new CyclicalList<UserReportMeasure>(configuration.MaximumMeasureCount);
this.screenshots = new CyclicalList<UserReportScreenshot>(configuration.MaximumScreenshotCount);
// Device Metadata
this.deviceMetadata = new List<UserReportNamedValue>();
foreach (KeyValuePair<string, string> kvp in this.Platform.GetDeviceMetadata())
{
this.AddDeviceMetadata(kvp.Key, kvp.Value);
}
// Client Version
this.AddDeviceMetadata("UserReportingClientVersion", "2.0");
// Synchronized Action
this.synchronizedActions = new List<Action>();
this.currentSynchronizedActions = new List<Action>();
// Update Stopwatch
this.updateStopwatch = new Stopwatch();
// Is Connected to Logger
this.IsConnectedToLogger = true;
}
#endregion
#region Fields
private Dictionary<string, UserReportMetric> clientMetrics;
private Dictionary<string, string> currentMeasureMetadata;
private Dictionary<string, UserReportMetric> currentMetrics;
private List<Action> currentSynchronizedActions;
private List<UserReportNamedValue> deviceMetadata;
private CyclicalList<UserReportEvent> events;
private int frameNumber;
private bool isMeasureBoundary;
private int measureFrames;
private CyclicalList<UserReportMeasure> measures;
private CyclicalList<UserReportScreenshot> screenshots;
private int screenshotsSaved;
private int screenshotsTaken;
private List<Action> synchronizedActions;
private Stopwatch updateStopwatch;
#endregion
#region Properties
/// <summary>
/// Gets the configuration.
/// </summary>
public UserReportingClientConfiguration Configuration { get; private set; }
/// <summary>
/// Gets the endpoint.
/// </summary>
public string Endpoint { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the client is connected to the logger. If true, log messages will be included in user reports.
/// </summary>
public bool IsConnectedToLogger { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the client is self reporting. If true, event and metrics about the client will be included in user reports.
/// </summary>
public bool IsSelfReporting { get; set; }
/// <summary>
/// Gets the platform.
/// </summary>
public IUserReportingPlatform Platform { get; private set; }
/// <summary>
/// Gets the project identifier.
/// </summary>
public string ProjectIdentifier { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether user reporting events should be sent to analytics.
/// </summary>
public bool SendEventsToAnalytics { get; set; }
#endregion
#region Methods
/// <summary>
/// Adds device metadata.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void AddDeviceMetadata(string name, string value)
{
lock (this.deviceMetadata)
{
UserReportNamedValue userReportNamedValue = new UserReportNamedValue();
userReportNamedValue.Name = name;
userReportNamedValue.Value = value;
this.deviceMetadata.Add(userReportNamedValue);
}
}
/// <summary>
/// Adds measure metadata. Measure metadata is associated with a period of time.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void AddMeasureMetadata(string name, string value)
{
if (this.currentMeasureMetadata.ContainsKey(name))
{
this.currentMeasureMetadata[name] = value;
}
else
{
this.currentMeasureMetadata.Add(name, value);
}
}
/// <summary>
/// Adds a synchronized action.
/// </summary>
/// <param name="action">The action.</param>
private void AddSynchronizedAction(Action action)
{
if (action == null)
{
throw new ArgumentNullException("action");
}
lock (this.synchronizedActions)
{
this.synchronizedActions.Add(action);
}
}
/// <summary>
/// Clears the screenshots.
/// </summary>
public void ClearScreenshots()
{
lock (this.screenshots)
{
this.screenshots.Clear();
}
}
/// <summary>
/// Creates a user report.
/// </summary>
/// <param name="callback">The callback. Provides the user report that was created.</param>
public void CreateUserReport(Action<UserReport> callback)
{
this.LogEvent(UserReportEventLevel.Info, "Creating user report.");
this.WaitForPerforation(this.screenshotsTaken, () =>
{
this.Platform.RunTask(() =>
{
// Start Stopwatch
Stopwatch stopwatch = Stopwatch.StartNew();
// Copy Data
UserReport userReport = new UserReport();
userReport.ProjectIdentifier = this.ProjectIdentifier;
// Device Metadata
lock (this.deviceMetadata)
{
userReport.DeviceMetadata = this.deviceMetadata.ToList();
}
// Events
lock (this.events)
{
userReport.Events = this.events.ToList();
}
// Measures
lock (this.measures)
{
userReport.Measures = this.measures.ToList();
}
// Screenshots
lock (this.screenshots)
{
userReport.Screenshots = this.screenshots.ToList();
}
// Complete
userReport.Complete();
// Modify
this.Platform.ModifyUserReport(userReport);
// Stop Stopwatch
stopwatch.Stop();
// Sample Client Metric
this.SampleClientMetric("UserReportingClient.CreateUserReport.Task", stopwatch.ElapsedMilliseconds);
// Copy Client Metrics
foreach (KeyValuePair<string, UserReportMetric> kvp in this.clientMetrics)
{
userReport.ClientMetrics.Add(kvp.Value);
}
// Return
return userReport;
}, (result) => { callback(result as UserReport); });
});
}
/// <summary>
/// Gets the endpoint.
/// </summary>
/// <returns>The endpoint.</returns>
public string GetEndpoint()
{
if (this.Endpoint == null)
{
return "https://localhost";
}
return this.Endpoint.Trim();
}
/// <summary>
/// Logs an event.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="message">The message.</param>
public void LogEvent(UserReportEventLevel level, string message)
{
this.LogEvent(level, message, null, null);
}
/// <summary>
/// Logs an event.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="message">The message.</param>
/// <param name="stackTrace">The stack trace.</param>
public void LogEvent(UserReportEventLevel level, string message, string stackTrace)
{
this.LogEvent(level, message, stackTrace, null);
}
/// <summary>
/// Logs an event with a stack trace and exception.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="message">The message.</param>
/// <param name="stackTrace">The stack trace.</param>
/// <param name="exception">The exception.</param>
private void LogEvent(UserReportEventLevel level, string message, string stackTrace, Exception exception)
{
lock (this.events)
{
UserReportEvent userReportEvent = new UserReportEvent();
userReportEvent.Level = level;
userReportEvent.Message = message;
userReportEvent.FrameNumber = this.frameNumber;
userReportEvent.StackTrace = stackTrace;
userReportEvent.Timestamp = DateTime.UtcNow;
if (exception != null)
{
userReportEvent.Exception = new SerializableException(exception);
}
this.events.Add(userReportEvent);
}
}
/// <summary>
/// Logs an exception.
/// </summary>
/// <param name="exception">The exception.</param>
public void LogException(Exception exception)
{
this.LogEvent(UserReportEventLevel.Error, null, null, exception);
}
/// <summary>
/// Samples a client metric. These metrics are only sample when self reporting is enabled.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void SampleClientMetric(string name, double value)
{
if (double.IsInfinity(value) || double.IsNaN(value))
{
return;
}
if (!this.clientMetrics.ContainsKey(name))
{
UserReportMetric newUserReportMetric = new UserReportMetric();
newUserReportMetric.Name = name;
this.clientMetrics.Add(name, newUserReportMetric);
}
UserReportMetric userReportMetric = this.clientMetrics[name];
userReportMetric.Sample(value);
this.clientMetrics[name] = userReportMetric;
// Self Reporting
if (this.IsSelfReporting)
{
this.SampleMetric(name, value);
}
}
/// <summary>
/// Samples a metric. Metrics can be sampled frequently and have low overhead.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void SampleMetric(string name, double value)
{
if (this.Configuration.MetricsGatheringMode == MetricsGatheringMode.Disabled)
{
return;
}
if (double.IsInfinity(value) || double.IsNaN(value))
{
return;
}
if (!this.currentMetrics.ContainsKey(name))
{
UserReportMetric newUserReportMetric = new UserReportMetric();
newUserReportMetric.Name = name;
this.currentMetrics.Add(name, newUserReportMetric);
}
UserReportMetric userReportMetric = this.currentMetrics[name];
userReportMetric.Sample(value);
this.currentMetrics[name] = userReportMetric;
}
/// <summary>
/// Saves a user report to disk.
/// </summary>
/// <param name="userReport">The user report.</param>
public void SaveUserReportToDisk(UserReport userReport)
{
this.LogEvent(UserReportEventLevel.Info, "Saving user report to disk.");
string json = this.Platform.SerializeJson(userReport);
File.WriteAllText("UserReport.json", json);
}
/// <summary>
/// Sends a user report to the server.
/// </summary>
/// <param name="userReport">The user report.</param>
/// <param name="callback">The callback. Provides a value indicating whether sending the user report was successful and provides the user report after it is modified by the server.</param>
public void SendUserReport(UserReport userReport, Action<bool, UserReport> callback)
{
this.SendUserReport(userReport, null, callback);
}
/// <summary>
/// Sends a user report to the server.
/// </summary>
/// <param name="userReport">The user report.</param>
/// <param name="progressCallback">The progress callback. Provides the upload and download progress.</param>
/// <param name="callback">The callback. Provides a value indicating whether sending the user report was successful and provides the user report after it is modified by the server.</param>
public void SendUserReport(UserReport userReport, Action<float, float> progressCallback, Action<bool, UserReport> callback)
{
try
{
if (userReport == null)
{
return;
}
if (userReport.Identifier != null)
{
this.LogEvent(UserReportEventLevel.Warning, "Identifier cannot be set on the client side. The value provided was discarded.");
return;
}
if (userReport.ContentLength != 0)
{
this.LogEvent(UserReportEventLevel.Warning, "ContentLength cannot be set on the client side. The value provided was discarded.");
return;
}
if (userReport.ReceivedOn != default(DateTime))
{
this.LogEvent(UserReportEventLevel.Warning, "ReceivedOn cannot be set on the client side. The value provided was discarded.");
return;
}
if (userReport.ExpiresOn != default(DateTime))
{
this.LogEvent(UserReportEventLevel.Warning, "ExpiresOn cannot be set on the client side. The value provided was discarded.");
return;
}
this.LogEvent(UserReportEventLevel.Info, "Sending user report.");
string json = this.Platform.SerializeJson(userReport);
byte[] jsonData = Encoding.UTF8.GetBytes(json);
string endpoint = this.GetEndpoint();
string url = string.Format(string.Format("{0}/api/userreporting", endpoint));
this.Platform.Post(url, "application/json", jsonData, (uploadProgress, downloadProgress) =>
{
if (progressCallback != null)
{
progressCallback(uploadProgress, downloadProgress);
}
}, (success, result) =>
{
this.AddSynchronizedAction(() =>
{
if (success)
{
try
{
string jsonResult = Encoding.UTF8.GetString(result);
UserReport userReportResult = this.Platform.DeserializeJson<UserReport>(jsonResult);
if (userReportResult != null)
{
if (this.SendEventsToAnalytics)
{
Dictionary<string, object> eventData = new Dictionary<string, object>();
eventData.Add("UserReportIdentifier", userReport.Identifier);
this.Platform.SendAnalyticsEvent("UserReportingClient.SendUserReport", eventData);
}
callback(success, userReportResult);
}
else
{
callback(false, null);
}
}
catch (Exception ex)
{
this.LogEvent(UserReportEventLevel.Error, string.Format("Sending user report failed: {0}", ex.ToString()));
callback(false, null);
}
}
else
{
this.LogEvent(UserReportEventLevel.Error, "Sending user report failed.");
callback(false, null);
}
});
});
}
catch (Exception ex)
{
this.LogEvent(UserReportEventLevel.Error, string.Format("Sending user report failed: {0}", ex.ToString()));
callback(false, null);
}
}
/// <summary>
/// Takes a screenshot.
/// </summary>
/// <param name="maximumWidth">The maximum width.</param>
/// <param name="maximumHeight">The maximum height.</param>
/// <param name="callback">The callback. Provides the screenshot.</param>
public void TakeScreenshot(int maximumWidth, int maximumHeight, Action<UserReportScreenshot> callback)
{
this.TakeScreenshotFromSource(maximumWidth, maximumHeight, null, callback);
}
/// <summary>
/// Takes a screenshot.
/// </summary>
/// <param name="maximumWidth">The maximum width.</param>
/// <param name="maximumHeight">The maximum height.</param>
/// <param name="source">The source. Passing null will capture the screen. Passing a camera will capture the camera's view. Passing a render texture will capture the render texture.</param>
/// <param name="callback">The callback. Provides the screenshot.</param>
public void TakeScreenshotFromSource(int maximumWidth, int maximumHeight, object source, Action<UserReportScreenshot> callback)
{
this.LogEvent(UserReportEventLevel.Info, "Taking screenshot.");
this.screenshotsTaken++;
this.Platform.TakeScreenshot(this.frameNumber, maximumWidth, maximumHeight, source, (passedFrameNumber, data) =>
{
this.AddSynchronizedAction(() =>
{
lock (this.screenshots)
{
UserReportScreenshot userReportScreenshot = new UserReportScreenshot();
userReportScreenshot.FrameNumber = passedFrameNumber;
userReportScreenshot.DataBase64 = Convert.ToBase64String(data);
this.screenshots.Add(userReportScreenshot);
this.screenshotsSaved++;
callback(userReportScreenshot);
}
});
});
}
/// <summary>
/// Updates the user reporting client, which updates networking communication, screenshotting, and metrics gathering.
/// </summary>
public void Update()
{
// Stopwatch
this.updateStopwatch.Reset();
this.updateStopwatch.Start();
// Update Platform
this.Platform.Update(this);
// Measures
if (this.Configuration.MetricsGatheringMode != MetricsGatheringMode.Disabled)
{
this.isMeasureBoundary = false;
int framesPerMeasure = this.Configuration.FramesPerMeasure;
if (this.measureFrames >= framesPerMeasure)
{
lock (this.measures)
{
UserReportMeasure userReportMeasure = new UserReportMeasure();
userReportMeasure.StartFrameNumber = this.frameNumber - framesPerMeasure;
userReportMeasure.EndFrameNumber = this.frameNumber - 1;
UserReportMeasure evictedUserReportMeasure = this.measures.GetNextEviction();
if (evictedUserReportMeasure.Metrics != null)
{
userReportMeasure.Metadata = evictedUserReportMeasure.Metadata;
userReportMeasure.Metrics = evictedUserReportMeasure.Metrics;
}
else
{
userReportMeasure.Metadata = new List<UserReportNamedValue>();
userReportMeasure.Metrics = new List<UserReportMetric>();
}
userReportMeasure.Metadata.Clear();
userReportMeasure.Metrics.Clear();
foreach (KeyValuePair<string, string> kvp in this.currentMeasureMetadata)
{
UserReportNamedValue userReportNamedValue = new UserReportNamedValue();
userReportNamedValue.Name = kvp.Key;
userReportNamedValue.Value = kvp.Value;
userReportMeasure.Metadata.Add(userReportNamedValue);
}
foreach (KeyValuePair<string, UserReportMetric> kvp in this.currentMetrics)
{
userReportMeasure.Metrics.Add(kvp.Value);
}
this.currentMetrics.Clear();
this.measures.Add(userReportMeasure);
this.measureFrames = 0;
this.isMeasureBoundary = true;
}
}
this.measureFrames++;
}
else
{
this.isMeasureBoundary = true;
}
// Synchronization
lock (this.synchronizedActions)
{
foreach (Action synchronizedAction in this.synchronizedActions)
{
this.currentSynchronizedActions.Add(synchronizedAction);
}
this.synchronizedActions.Clear();
}
// Perform Synchronized Actions
foreach (Action synchronizedAction in this.currentSynchronizedActions)
{
synchronizedAction();
}
this.currentSynchronizedActions.Clear();
// Frame Number
this.frameNumber++;
// Stopwatch
this.updateStopwatch.Stop();
this.SampleClientMetric("UserReportingClient.Update", this.updateStopwatch.ElapsedMilliseconds);
}
/// <summary>
/// Updates the user reporting client at the end of the frame, which updates networking communication, screenshotting, and metrics gathering.
/// </summary>
public void UpdateOnEndOfFrame()
{
// Stopwatch
this.updateStopwatch.Reset();
this.updateStopwatch.Start();
// Update Platform
this.Platform.OnEndOfFrame(this);
// Stopwatch
this.updateStopwatch.Stop();
this.SampleClientMetric("UserReportingClient.UpdateOnEndOfFrame", this.updateStopwatch.ElapsedMilliseconds);
}
/// <summary>
/// Waits for perforation, a boundary between measures when no screenshots are in progress.
/// </summary>
/// <param name="currentScreenshotsTaken">The current screenshots taken.</param>
/// <param name="callback">The callback.</param>
private void WaitForPerforation(int currentScreenshotsTaken, Action callback)
{
if (this.screenshotsSaved >= currentScreenshotsTaken && this.isMeasureBoundary)
{
callback();
}
else
{
this.AddSynchronizedAction(() => { this.WaitForPerforation(currentScreenshotsTaken, callback); });
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,84 @@
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents configuration for the user reporting client.
/// </summary>
public class UserReportingClientConfiguration
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportingClientConfiguration"/> class.
/// </summary>
public UserReportingClientConfiguration()
{
this.MaximumEventCount = 100;
this.MaximumMeasureCount = 300;
this.FramesPerMeasure = 60;
this.MaximumScreenshotCount = 10;
}
/// <summary>
/// Creates a new instance of the <see cref="UserReportingClientConfiguration"/> class.
/// </summary>
/// <param name="maximumEventCount">The maximum event count. This is a rolling window.</param>
/// <param name="maximumMeasureCount">The maximum measure count. This is a rolling window.</param>
/// <param name="framesPerMeasure">The number of frames per measure. A user report is only created on the boundary between measures. A large number of frames per measure will increase user report creation time by this number of frames in the worst case.</param>
/// <param name="maximumScreenshotCount">The maximum screenshot count. This is a rolling window.</param>
public UserReportingClientConfiguration(int maximumEventCount, int maximumMeasureCount, int framesPerMeasure, int maximumScreenshotCount)
{
this.MaximumEventCount = maximumEventCount;
this.MaximumMeasureCount = maximumMeasureCount;
this.FramesPerMeasure = framesPerMeasure;
this.MaximumScreenshotCount = maximumScreenshotCount;
}
/// <summary>
/// Creates a new instance of the <see cref="UserReportingClientConfiguration"/> class.
/// </summary>
/// <param name="maximumEventCount">The maximum event count. This is a rolling window.</param>
/// <param name="metricsGatheringMode">The metrics gathering mode.</param>
/// <param name="maximumMeasureCount">The maximum measure count. This is a rolling window.</param>
/// <param name="framesPerMeasure">The number of frames per measure. A user report is only created on the boundary between measures. A large number of frames per measure will increase user report creation time by this number of frames in the worst case.</param>
/// <param name="maximumScreenshotCount">The maximum screenshot count. This is a rolling window.</param>
public UserReportingClientConfiguration(int maximumEventCount, MetricsGatheringMode metricsGatheringMode, int maximumMeasureCount, int framesPerMeasure, int maximumScreenshotCount)
{
this.MaximumEventCount = maximumEventCount;
this.MetricsGatheringMode = metricsGatheringMode;
this.MaximumMeasureCount = maximumMeasureCount;
this.FramesPerMeasure = framesPerMeasure;
this.MaximumScreenshotCount = maximumScreenshotCount;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the frames per measure.
/// </summary>
public int FramesPerMeasure { get; internal set; }
/// <summary>
/// Gets or sets the maximum event count.
/// </summary>
public int MaximumEventCount { get; internal set; }
/// <summary>
/// Gets or sets the maximum measure count.
/// </summary>
public int MaximumMeasureCount { get; internal set; }
/// <summary>
/// Gets or sets the maximum screenshot count.
/// </summary>
public int MaximumScreenshotCount { get; internal set; }
/// <summary>
/// Gets or sets the metrics gathering mode.
/// </summary>
public MetricsGatheringMode MetricsGatheringMode { get; internal set; }
#endregion
}
}

View File

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

View File

@@ -0,0 +1,60 @@
using UnityEngine;
/// <summary>
/// Represents a behavior that monitors the application for framerate issues and automatically submits a user report.
/// </summary>
public class FramerateMonitor : UserReportingMonitor
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="FramerateMonitor"/> class.
/// </summary>
public FramerateMonitor()
{
this.MaximumDurationInSeconds = 10;
this.MinimumFramerate = 15;
}
#endregion
#region Fields
private float duration;
/// <summary>
/// Gets or sets the maximum duration in seconds.
/// </summary>
public float MaximumDurationInSeconds;
/// <summary>
/// Gets or sets the minimum framerate.
/// </summary>
public float MinimumFramerate;
#endregion
#region Methods
private void Update()
{
float deltaTime = Time.deltaTime;
float framerate = 1.0f / deltaTime;
if (framerate < this.MinimumFramerate)
{
this.duration += deltaTime;
}
else
{
this.duration = 0;
}
if (this.duration > this.MaximumDurationInSeconds)
{
this.duration = 0;
this.Trigger();
}
}
#endregion
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: f9e6e43dfb1803f46ac36702ba56c7e4
timeCreated: 1517959061
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,13 @@
using UnityEngine;
namespace Assets.UserReporting.Scripts.Plugin
{
public interface ILogListener
{
#region Methods
void ReceiveLogMessage(string logString, string stackTrace, LogType logType);
#endregion
}
}

View File

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

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Assets.UserReporting.Scripts.Plugin
{
public static class LogDispatcher
{
#region Static Constructors
static LogDispatcher()
{
LogDispatcher.listeners = new List<WeakReference>();
Application.logMessageReceivedThreaded += (logString, stackTrace, logType) =>
{
lock (LogDispatcher.listeners)
{
int i = 0;
while (i < LogDispatcher.listeners.Count)
{
WeakReference listener = LogDispatcher.listeners[i];
ILogListener logListener = listener.Target as ILogListener;
if (logListener != null)
{
logListener.ReceiveLogMessage(logString, stackTrace, logType);
i++;
}
else
{
LogDispatcher.listeners.RemoveAt(i);
}
}
}
};
}
#endregion
#region Static Fields
private static List<WeakReference> listeners;
#endregion
#region Static Methods
public static void Register(ILogListener logListener)
{
lock (LogDispatcher.listeners)
{
LogDispatcher.listeners.Add(new WeakReference(logListener));
}
}
#endregion
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,32 @@
namespace Unity.Cloud.UserReporting.Plugin
{
/// <summary>
/// Provides static methods for parsing user reports.
/// </summary>
public static class UnityUserReportParser
{
#region Static Methods
/// <summary>
/// Parses a user report.
/// </summary>
/// <param name="json">The JSON.</param>
/// <returns>The user report.</returns>
public static UserReport ParseUserReport(string json)
{
return SimpleJson.SimpleJson.DeserializeObject<UserReport>(json);
}
/// <summary>
/// Parses a user report list.
/// </summary>
/// <param name="json">The JSON.</param>
/// <returns>The user report list.</returns>
public static UserReportList ParseUserReportList(string json)
{
return SimpleJson.SimpleJson.DeserializeObject<UserReportList>(json);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,151 @@
using Unity.Cloud.UserReporting.Client;
using UnityEngine;
namespace Unity.Cloud.UserReporting.Plugin
{
/// <summary>
/// Provides a starting point for Unity User Reporting.
/// </summary>
public static class UnityUserReporting
{
#region Static Fields
private static UserReportingClient currentClient;
#endregion
#region Static Properties
/// <summary>
/// Gets the current client.
/// </summary>
public static UserReportingClient CurrentClient
{
get
{
if (UnityUserReporting.currentClient == null)
{
UnityUserReporting.Configure();
}
return UnityUserReporting.currentClient;
}
private set { UnityUserReporting.currentClient = value; }
}
#endregion
#region Static Methods
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="projectIdentifier">The project identifier.</param>
/// <param name="platform">The plaform.</param>
/// <param name="configuration">The configuration.</param>
public static void Configure(string endpoint, string projectIdentifier, IUserReportingPlatform platform, UserReportingClientConfiguration configuration)
{
UnityUserReporting.CurrentClient = new UserReportingClient(endpoint, projectIdentifier, platform, configuration);
}
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
/// <param name="endpoint"></param>
/// <param name="projectIdentifier"></param>
/// <param name="configuration"></param>
public static void Configure(string endpoint, string projectIdentifier, UserReportingClientConfiguration configuration)
{
UnityUserReporting.CurrentClient = new UserReportingClient(endpoint, projectIdentifier, UnityUserReporting.GetPlatform(), configuration);
}
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
/// <param name="projectIdentifier"></param>
/// <param name="configuration"></param>
public static void Configure(string projectIdentifier, UserReportingClientConfiguration configuration)
{
UnityUserReporting.Configure("https://userreporting.cloud.unity3d.com", projectIdentifier, UnityUserReporting.GetPlatform(), configuration);
}
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
/// <param name="projectIdentifier"></param>
public static void Configure(string projectIdentifier)
{
UnityUserReporting.Configure("https://userreporting.cloud.unity3d.com", projectIdentifier, UnityUserReporting.GetPlatform(), new UserReportingClientConfiguration());
}
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
public static void Configure()
{
UnityUserReporting.Configure("https://userreporting.cloud.unity3d.com", Application.cloudProjectId, UnityUserReporting.GetPlatform(), new UserReportingClientConfiguration());
}
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
/// <param name="configuration"></param>
public static void Configure(UserReportingClientConfiguration configuration)
{
UnityUserReporting.Configure("https://userreporting.cloud.unity3d.com", Application.cloudProjectId, UnityUserReporting.GetPlatform(), configuration);
}
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
/// <param name="projectIdentifier"></param>
/// <param name="platform"></param>
/// <param name="configuration"></param>
public static void Configure(string projectIdentifier, IUserReportingPlatform platform, UserReportingClientConfiguration configuration)
{
UnityUserReporting.Configure("https://userreporting.cloud.unity3d.com", projectIdentifier, platform, configuration);
}
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
/// <param name="platform"></param>
/// <param name="configuration"></param>
public static void Configure(IUserReportingPlatform platform, UserReportingClientConfiguration configuration)
{
UnityUserReporting.Configure("https://userreporting.cloud.unity3d.com", Application.cloudProjectId, platform, configuration);
}
/// <summary>
/// Configures Unity User Reporting.
/// </summary>
/// <param name="platform"></param>
public static void Configure(IUserReportingPlatform platform)
{
UnityUserReporting.Configure("https://userreporting.cloud.unity3d.com", Application.cloudProjectId, platform, new UserReportingClientConfiguration());
}
/// <summary>
/// Gets the platform.
/// </summary>
/// <returns>The platform.</returns>
private static IUserReportingPlatform GetPlatform()
{
return new UnityUserReportingPlatform();
}
/// <summary>
/// Uses an existing client.
/// </summary>
/// <param name="client">The client.</param>
public static void Use(UserReportingClient client)
{
if (client != null)
{
UnityUserReporting.CurrentClient = client;
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,633 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using Assets.UserReporting.Scripts.Plugin;
using Unity.Cloud.UserReporting.Client;
using UnityEngine;
using UnityEngine.Analytics;
using UnityEngine.Networking;
using UnityEngine.Profiling;
using UnityEngine.SceneManagement;
namespace Unity.Cloud.UserReporting.Plugin
{
/// <summary>
/// Represents the Unity user reporting platform.
/// </summary>
public class UnityUserReportingPlatform : IUserReportingPlatform, ILogListener
{
#region Nested Types
/// <summary>
/// Represents a log message.
/// </summary>
private struct LogMessage
{
#region Fields
/// <summary>
/// Gets or sets the log string.
/// </summary>
public string LogString;
/// <summary>
/// Gets or sets the log type.
/// </summary>
public LogType LogType;
/// <summary>
/// Gets or sets the stack trace.
/// </summary>
public string StackTrace;
#endregion
}
/// <summary>
/// Represents a post operation.
/// </summary>
private class PostOperation
{
#region Properties
/// <summary>
/// Gets or sets the callback.
/// </summary>
public Action<bool, byte[]> Callback { get; set; }
/// <summary>
/// Gets or sets the progress callback.
/// </summary>
public Action<float, float> ProgressCallback { get; set; }
/// <summary>
/// Gets or sets the web request.
/// </summary>
public UnityWebRequest WebRequest { get; set; }
#endregion
}
/// <summary>
/// Represents a profiler sampler.
/// </summary>
private struct ProfilerSampler
{
#region Fields
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name;
/// <summary>
/// Gets or sets the recorder.
/// </summary>
public Recorder Recorder;
#endregion
#region Methods
/// <summary>
/// Gets the value of the sampler.
/// </summary>
/// <returns>The value of the sampler.</returns>
public double GetValue()
{
if (this.Recorder == null)
{
return 0;
}
return this.Recorder.elapsedNanoseconds / 1000000.0;
}
#endregion
}
/// <summary>
/// Represents a screenshot operation.
/// </summary>
private class ScreenshotOperation
{
#region Properties
/// <summary>
/// Gets or sets the callback.
/// </summary>
public Action<int, byte[]> Callback { get; set; }
/// <summary>
/// Gets or sets the frame number.
/// </summary>
public int FrameNumber { get; set; }
/// <summary>
/// Gets or sets the maximum height.
/// </summary>
public int MaximumHeight { get; set; }
/// <summary>
/// Gets or sets the maximum width.
/// </summary>
public int MaximumWidth { get; set; }
/// <summary>
/// Gets or sets the PNG data.
/// </summary>
public byte[] PngData { get; set; }
/// <summary>
/// Gets or sets the source.
/// </summary>
public object Source { get; set; }
/// <summary>
/// Gets or sets the stage.
/// </summary>
public ScreenshotStage Stage { get; set; }
/// <summary>
/// Gets or sets the texture.
/// </summary>
public Texture2D Texture { get; set; }
/// <summary>
/// Gets or sets the texture (resized).
/// </summary>
public Texture2D TextureResized { get; set; }
/// <summary>
/// Gets or sets the Unity frame.
/// </summary>
public int UnityFrame { get; set; }
/// <summary>
/// Gets or sets the wait frames.
/// </summary>
public int WaitFrames { get; set; }
#endregion
}
/// <summary>
/// Represents a screenshot stage.
/// </summary>
private enum ScreenshotStage
{
/// <summary>
/// Render.
/// </summary>
Render = 0,
/// <summary>
/// Read pixels.
/// </summary>
ReadPixels = 1,
/// <summary>
/// Gets pixels.
/// </summary>
GetPixels = 2,
/// <summary>
/// Encode to PNG.
/// </summary>
EncodeToPNG = 3,
/// <summary>
/// Done.
/// </summary>
Done = 4
}
#endregion
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UnityUserReportingPlatform"/> class.
/// </summary>
public UnityUserReportingPlatform()
{
this.logMessages = new List<LogMessage>();
this.postOperations = new List<PostOperation>();
this.screenshotOperations = new List<ScreenshotOperation>();
this.screenshotStopwatch = new Stopwatch();
// Recorders
this.profilerSamplers = new List<ProfilerSampler>();
Dictionary<string, string> samplerNames = this.GetSamplerNames();
foreach (KeyValuePair<string, string> kvp in samplerNames)
{
Sampler sampler = Sampler.Get(kvp.Key);
if (sampler.isValid)
{
Recorder recorder = sampler.GetRecorder();
recorder.enabled = true;
ProfilerSampler profilerSampler = new ProfilerSampler();
profilerSampler.Name = kvp.Value;
profilerSampler.Recorder = recorder;
this.profilerSamplers.Add(profilerSampler);
}
}
// Log Messages
LogDispatcher.Register(this);
}
#endregion
#region Fields
private List<LogMessage> logMessages;
private List<PostOperation> postOperations;
private List<ProfilerSampler> profilerSamplers;
private List<ScreenshotOperation> screenshotOperations;
private Stopwatch screenshotStopwatch;
private List<PostOperation> taskOperations;
#endregion
#region Methods
/// <inheritdoc cref="IUserReportingPlatform"/>
public T DeserializeJson<T>(string json)
{
return SimpleJson.SimpleJson.DeserializeObject<T>(json);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void OnEndOfFrame(UserReportingClient client)
{
// Screenshot Operations
int screenshotOperationIndex = 0;
while (screenshotOperationIndex < this.screenshotOperations.Count)
{
ScreenshotOperation screenshotOperation = this.screenshotOperations[screenshotOperationIndex];
if (screenshotOperation.Stage == ScreenshotStage.Render && screenshotOperation.WaitFrames < 1)
{
Camera cameraSource = screenshotOperation.Source as Camera;
if (cameraSource != null)
{
this.screenshotStopwatch.Reset();
this.screenshotStopwatch.Start();
RenderTexture renderTexture = new RenderTexture(screenshotOperation.MaximumWidth, screenshotOperation.MaximumHeight, 24);
RenderTexture originalTargetTexture = cameraSource.targetTexture;
cameraSource.targetTexture = renderTexture;
cameraSource.Render();
cameraSource.targetTexture = originalTargetTexture;
this.screenshotStopwatch.Stop();
client.SampleClientMetric("Screenshot.Render", this.screenshotStopwatch.ElapsedMilliseconds);
screenshotOperation.Source = renderTexture;
screenshotOperation.Stage = ScreenshotStage.ReadPixels;
screenshotOperation.WaitFrames = 15;
screenshotOperationIndex++;
continue;
}
else
{
screenshotOperation.Stage = ScreenshotStage.ReadPixels;
}
}
if (screenshotOperation.Stage == ScreenshotStage.ReadPixels && screenshotOperation.WaitFrames < 1)
{
this.screenshotStopwatch.Reset();
this.screenshotStopwatch.Start();
RenderTexture renderTextureSource = screenshotOperation.Source as RenderTexture;
if (renderTextureSource != null)
{
RenderTexture originalActiveTexture = RenderTexture.active;
RenderTexture.active = renderTextureSource;
screenshotOperation.Texture = new Texture2D(renderTextureSource.width, renderTextureSource.height, TextureFormat.ARGB32, true);
screenshotOperation.Texture.ReadPixels(new Rect(0, 0, renderTextureSource.width, renderTextureSource.height), 0, 0);
screenshotOperation.Texture.Apply();
RenderTexture.active = originalActiveTexture;
}
else
{
screenshotOperation.Texture = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, true);
screenshotOperation.Texture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
screenshotOperation.Texture.Apply();
}
this.screenshotStopwatch.Stop();
client.SampleClientMetric("Screenshot.ReadPixels", this.screenshotStopwatch.ElapsedMilliseconds);
screenshotOperation.Stage = ScreenshotStage.GetPixels;
screenshotOperation.WaitFrames = 15;
screenshotOperationIndex++;
continue;
}
if (screenshotOperation.Stage == ScreenshotStage.GetPixels && screenshotOperation.WaitFrames < 1)
{
this.screenshotStopwatch.Reset();
this.screenshotStopwatch.Start();
int maximumWidth = screenshotOperation.MaximumWidth > 32 ? screenshotOperation.MaximumWidth : 32;
int maximumHeight = screenshotOperation.MaximumHeight > 32 ? screenshotOperation.MaximumHeight : 32;
int width = screenshotOperation.Texture.width;
int height = screenshotOperation.Texture.height;
int mipLevel = 0;
while (width > maximumWidth || height > maximumHeight)
{
width /= 2;
height /= 2;
mipLevel++;
}
screenshotOperation.TextureResized = new Texture2D(width, height);
screenshotOperation.TextureResized.SetPixels(screenshotOperation.Texture.GetPixels(mipLevel));
screenshotOperation.TextureResized.Apply();
this.screenshotStopwatch.Stop();
client.SampleClientMetric("Screenshot.GetPixels", this.screenshotStopwatch.ElapsedMilliseconds);
screenshotOperation.Stage = ScreenshotStage.EncodeToPNG;
screenshotOperation.WaitFrames = 15;
screenshotOperationIndex++;
continue;
}
if (screenshotOperation.Stage == ScreenshotStage.EncodeToPNG && screenshotOperation.WaitFrames < 1)
{
this.screenshotStopwatch.Reset();
this.screenshotStopwatch.Start();
screenshotOperation.PngData = screenshotOperation.TextureResized.EncodeToPNG();
this.screenshotStopwatch.Stop();
client.SampleClientMetric("Screenshot.EncodeToPNG", this.screenshotStopwatch.ElapsedMilliseconds);
screenshotOperation.Stage = ScreenshotStage.Done;
screenshotOperationIndex++;
continue;
}
if (screenshotOperation.Stage == ScreenshotStage.Done && screenshotOperation.WaitFrames < 1)
{
screenshotOperation.Callback(screenshotOperation.FrameNumber, screenshotOperation.PngData);
UnityEngine.Object.Destroy(screenshotOperation.Texture);
UnityEngine.Object.Destroy(screenshotOperation.TextureResized);
this.screenshotOperations.Remove(screenshotOperation);
}
screenshotOperation.WaitFrames--;
}
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void Post(string endpoint, string contentType, byte[] content, Action<float, float> progressCallback, Action<bool, byte[]> callback)
{
UnityWebRequest webRequest = new UnityWebRequest(endpoint, "POST");
webRequest.uploadHandler = new UploadHandlerRaw(content);
webRequest.downloadHandler = new DownloadHandlerBuffer();
webRequest.SetRequestHeader("Content-Type", contentType);
webRequest.SendWebRequest();
UnityUserReportingPlatform.PostOperation postOperation = new UnityUserReportingPlatform.PostOperation();
postOperation.WebRequest = webRequest;
postOperation.Callback = callback;
postOperation.ProgressCallback = progressCallback;
this.postOperations.Add(postOperation);
}
public void ReceiveLogMessage(string logString, string stackTrace, LogType logType)
{
lock (this.logMessages)
{
LogMessage logMessage = new LogMessage();
logMessage.LogString = logString;
logMessage.StackTrace = stackTrace;
logMessage.LogType = logType;
this.logMessages.Add(logMessage);
}
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void RunTask(Func<object> task, Action<object> callback)
{
callback(task());
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void SendAnalyticsEvent(string eventName, Dictionary<string, object> eventData)
{
Analytics.CustomEvent(eventName, eventData);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public string SerializeJson(object instance)
{
return SimpleJson.SimpleJson.SerializeObject(instance);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void TakeScreenshot(int frameNumber, int maximumWidth, int maximumHeight, object source, Action<int, byte[]> callback)
{
ScreenshotOperation screenshotOperation = new ScreenshotOperation();
screenshotOperation.FrameNumber = frameNumber;
screenshotOperation.MaximumWidth = maximumWidth;
screenshotOperation.MaximumHeight = maximumHeight;
screenshotOperation.Source = source;
screenshotOperation.Callback = callback;
screenshotOperation.UnityFrame = Time.frameCount;
this.screenshotOperations.Add(screenshotOperation);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void Update(UserReportingClient client)
{
// Log Messages
lock (this.logMessages)
{
foreach (LogMessage logMessage in this.logMessages)
{
UserReportEventLevel eventLevel = UserReportEventLevel.Info;
if (logMessage.LogType == LogType.Warning)
{
eventLevel = UserReportEventLevel.Warning;
}
else if (logMessage.LogType == LogType.Error)
{
eventLevel = UserReportEventLevel.Error;
}
else if (logMessage.LogType == LogType.Exception)
{
eventLevel = UserReportEventLevel.Error;
}
else if (logMessage.LogType == LogType.Assert)
{
eventLevel = UserReportEventLevel.Error;
}
if (client.IsConnectedToLogger)
{
client.LogEvent(eventLevel, logMessage.LogString, logMessage.StackTrace);
}
}
this.logMessages.Clear();
}
// Metrics
if (client.Configuration.MetricsGatheringMode == MetricsGatheringMode.Automatic)
{
// Sample Automatic Metrics
this.SampleAutomaticMetrics(client);
// Profiler Samplers
foreach (ProfilerSampler profilerSampler in this.profilerSamplers)
{
client.SampleMetric(profilerSampler.Name, profilerSampler.GetValue());
}
}
// Post Operations
int postOperationIndex = 0;
while (postOperationIndex < this.postOperations.Count)
{
UnityUserReportingPlatform.PostOperation postOperation = this.postOperations[postOperationIndex];
if (postOperation.WebRequest.isDone)
{
bool isError = postOperation.WebRequest.error != null && postOperation.WebRequest.responseCode != 200;
if (isError)
{
string errorMessage = string.Format("UnityUserReportingPlatform.Post: {0} {1}", postOperation.WebRequest.responseCode, postOperation.WebRequest.error);
UnityEngine.Debug.Log(errorMessage);
client.LogEvent(UserReportEventLevel.Error, errorMessage);
}
postOperation.ProgressCallback(1, 1);
postOperation.Callback(!isError, postOperation.WebRequest.downloadHandler.data);
this.postOperations.Remove(postOperation);
}
else
{
postOperation.ProgressCallback(postOperation.WebRequest.uploadProgress, postOperation.WebRequest.downloadProgress);
postOperationIndex++;
}
}
}
#endregion
#region Virtual Methods
/// <inheritdoc cref="IUserReportingPlatform"/>
public virtual IDictionary<string, string> GetDeviceMetadata()
{
Dictionary<string, string> deviceMetadata = new Dictionary<string, string>();
// Unity
deviceMetadata.Add("BuildGUID", Application.buildGUID);
deviceMetadata.Add("DeviceModel", SystemInfo.deviceModel);
deviceMetadata.Add("DeviceType", SystemInfo.deviceType.ToString());
deviceMetadata.Add("DeviceUniqueIdentifier", SystemInfo.deviceUniqueIdentifier);
deviceMetadata.Add("DPI", Screen.dpi.ToString(CultureInfo.InvariantCulture));
deviceMetadata.Add("GraphicsDeviceName", SystemInfo.graphicsDeviceName);
deviceMetadata.Add("GraphicsDeviceType", SystemInfo.graphicsDeviceType.ToString());
deviceMetadata.Add("GraphicsDeviceVendor", SystemInfo.graphicsDeviceVendor);
deviceMetadata.Add("GraphicsDeviceVersion", SystemInfo.graphicsDeviceVersion);
deviceMetadata.Add("GraphicsMemorySize", SystemInfo.graphicsMemorySize.ToString());
deviceMetadata.Add("InstallerName", Application.installerName);
deviceMetadata.Add("InstallMode", Application.installMode.ToString());
deviceMetadata.Add("IsEditor", Application.isEditor.ToString());
deviceMetadata.Add("IsFullScreen", Screen.fullScreen.ToString());
deviceMetadata.Add("OperatingSystem", SystemInfo.operatingSystem);
deviceMetadata.Add("OperatingSystemFamily", SystemInfo.operatingSystemFamily.ToString());
deviceMetadata.Add("Orientation", Screen.orientation.ToString());
deviceMetadata.Add("Platform", Application.platform.ToString());
try
{
deviceMetadata.Add("QualityLevel", QualitySettings.names[QualitySettings.GetQualityLevel()]);
}
catch
{
// Empty
}
deviceMetadata.Add("ResolutionWidth", Screen.currentResolution.width.ToString());
deviceMetadata.Add("ResolutionHeight", Screen.currentResolution.height.ToString());
deviceMetadata.Add("ResolutionRefreshRate", Screen.currentResolution.refreshRate.ToString());
deviceMetadata.Add("SystemLanguage", Application.systemLanguage.ToString());
deviceMetadata.Add("SystemMemorySize", SystemInfo.systemMemorySize.ToString());
deviceMetadata.Add("TargetFrameRate", Application.targetFrameRate.ToString());
deviceMetadata.Add("UnityVersion", Application.unityVersion);
deviceMetadata.Add("Version", Application.version);
// Other
deviceMetadata.Add("Source", "Unity");
// Type
Type type = this.GetType();
deviceMetadata.Add("IUserReportingPlatform", type.Name);
// Return
return deviceMetadata;
}
public virtual Dictionary<string, string> GetSamplerNames()
{
Dictionary<string, string> samplerNames = new Dictionary<string, string>();
samplerNames.Add("AudioManager.FixedUpdate", "AudioManager.FixedUpdateInMilliseconds");
samplerNames.Add("AudioManager.Update", "AudioManager.UpdateInMilliseconds");
samplerNames.Add("LateBehaviourUpdate", "Behaviors.LateUpdateInMilliseconds");
samplerNames.Add("BehaviourUpdate", "Behaviors.UpdateInMilliseconds");
samplerNames.Add("Camera.Render", "Camera.RenderInMilliseconds");
samplerNames.Add("Overhead", "Engine.OverheadInMilliseconds");
samplerNames.Add("WaitForRenderJobs", "Engine.WaitForRenderJobsInMilliseconds");
samplerNames.Add("WaitForTargetFPS", "Engine.WaitForTargetFPSInMilliseconds");
samplerNames.Add("GUI.Repaint", "GUI.RepaintInMilliseconds");
samplerNames.Add("Network.Update", "Network.UpdateInMilliseconds");
samplerNames.Add("ParticleSystem.EndUpdateAll", "ParticleSystem.EndUpdateAllInMilliseconds");
samplerNames.Add("ParticleSystem.Update", "ParticleSystem.UpdateInMilliseconds");
samplerNames.Add("Physics.FetchResults", "Physics.FetchResultsInMilliseconds");
samplerNames.Add("Physics.Processing", "Physics.ProcessingInMilliseconds");
samplerNames.Add("Physics.ProcessReports", "Physics.ProcessReportsInMilliseconds");
samplerNames.Add("Physics.Simulate", "Physics.SimulateInMilliseconds");
samplerNames.Add("Physics.UpdateBodies", "Physics.UpdateBodiesInMilliseconds");
samplerNames.Add("Physics.Interpolation", "Physics.InterpolationInMilliseconds");
samplerNames.Add("Physics2D.DynamicUpdate", "Physics2D.DynamicUpdateInMilliseconds");
samplerNames.Add("Physics2D.FixedUpdate", "Physics2D.FixedUpdateInMilliseconds");
return samplerNames;
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public virtual void ModifyUserReport(UserReport userReport)
{
// Active Scene
Scene activeScene = SceneManager.GetActiveScene();
userReport.DeviceMetadata.Add(new UserReportNamedValue("ActiveSceneName", activeScene.name));
// Main Camera
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
userReport.DeviceMetadata.Add(new UserReportNamedValue("MainCameraName", mainCamera.name));
userReport.DeviceMetadata.Add(new UserReportNamedValue("MainCameraPosition", mainCamera.transform.position.ToString()));
userReport.DeviceMetadata.Add(new UserReportNamedValue("MainCameraForward", mainCamera.transform.forward.ToString()));
// Looking At
RaycastHit hit;
if (Physics.Raycast(mainCamera.transform.position, mainCamera.transform.forward, out hit))
{
GameObject lookingAt = hit.transform.gameObject;
userReport.DeviceMetadata.Add(new UserReportNamedValue("LookingAt", hit.point.ToString()));
userReport.DeviceMetadata.Add(new UserReportNamedValue("LookingAtGameObject", lookingAt.name));
userReport.DeviceMetadata.Add(new UserReportNamedValue("LookingAtGameObjectPosition", lookingAt.transform.position.ToString()));
}
}
}
/// <summary>
/// Samples automatic metrics.
/// </summary>
/// <param name="client">The client.</param>
public virtual void SampleAutomaticMetrics(UserReportingClient client)
{
// Graphics
client.SampleMetric("Graphics.FramesPerSecond", 1.0f / Time.deltaTime);
// Memory
client.SampleMetric("Memory.MonoUsedSizeInBytes", Profiler.GetMonoUsedSizeLong());
client.SampleMetric("Memory.TotalAllocatedMemoryInBytes", Profiler.GetTotalAllocatedMemoryLong());
client.SampleMetric("Memory.TotalReservedMemoryInBytes", Profiler.GetTotalReservedMemoryLong());
client.SampleMetric("Memory.TotalUnusedReservedMemoryInBytes", Profiler.GetTotalUnusedReservedMemoryLong());
// Battery
client.SampleMetric("Battery.BatteryLevelInPercent", SystemInfo.batteryLevel);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,81 @@
using System.Collections;
using UnityEngine;
namespace Unity.Cloud.UserReporting.Plugin
{
/// <summary>
/// Helps with updating the Unity User Reporting client.
/// </summary>
public class UnityUserReportingUpdater : IEnumerator
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UnityUserReportingUpdater"/> class.
/// </summary>
public UnityUserReportingUpdater()
{
this.waitForEndOfFrame = new WaitForEndOfFrame();
}
#endregion
#region Fields
private int step;
private WaitForEndOfFrame waitForEndOfFrame;
#endregion
#region Properties
/// <summary>
/// Gets the current item.
/// </summary>
public object Current { get; private set; }
#endregion
#region Methods
/// <summary>
/// Moves to the next item.
/// </summary>
/// <returns>A value indicating whether the move was successful.</returns>
public bool MoveNext()
{
if (this.step == 0)
{
UnityUserReporting.CurrentClient.Update();
this.Current = null;
this.step = 1;
return true;
}
if (this.step == 1)
{
this.Current = this.waitForEndOfFrame;
this.step = 2;
return true;
}
if (this.step == 2)
{
UnityUserReporting.CurrentClient.UpdateOnEndOfFrame();
this.Current = null;
this.step = 3;
return false;
}
return false;
}
/// <summary>
/// Resets the updater.
/// </summary>
public void Reset()
{
this.step = 0;
}
#endregion
}
}

View File

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

View File

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

View File

@@ -0,0 +1,426 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Assets.UserReporting.Scripts.Plugin;
using Unity.Cloud.UserReporting.Client;
using Unity.Screenshots;
using UnityEngine;
using UnityEngine.Analytics;
using UnityEngine.Networking;
using UnityEngine.Profiling;
using UnityEngine.SceneManagement;
namespace Unity.Cloud.UserReporting.Plugin.Version2018_3
{
/// <summary>
/// Represents a Unity user reporting platform that supports async operations for screetshotting and user report creation.
/// </summary>
public class AsyncUnityUserReportingPlatform : IUserReportingPlatform, ILogListener
{
#region Nested Types
/// <summary>
/// Represents a log message.
/// </summary>
private struct LogMessage
{
#region Fields
/// <summary>
/// Gets or sets the log string.
/// </summary>
public string LogString;
/// <summary>
/// Gets or sets the log type.
/// </summary>
public LogType LogType;
/// <summary>
/// Gets or sets the stack trace.
/// </summary>
public string StackTrace;
#endregion
}
/// <summary>
/// Represents a post operation.
/// </summary>
private class PostOperation
{
#region Properties
/// <summary>
/// Gets or sets the callback.
/// </summary>
public Action<bool, byte[]> Callback { get; set; }
/// <summary>
/// Gets or sets the progress callback.
/// </summary>
public Action<float, float> ProgressCallback { get; set; }
/// <summary>
/// Gets or sets the web request.
/// </summary>
public UnityWebRequest WebRequest { get; set; }
#endregion
}
/// <summary>
/// Represents a profiler sampler.
/// </summary>
private struct ProfilerSampler
{
#region Fields
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name;
/// <summary>
/// Gets or sets the recorder.
/// </summary>
public Recorder Recorder;
#endregion
#region Methods
/// <summary>
/// Gets the value of the sampler.
/// </summary>
/// <returns>The value of the sampler.</returns>
public double GetValue()
{
if (this.Recorder == null)
{
return 0;
}
return this.Recorder.elapsedNanoseconds / 1000000.0;
}
#endregion
}
#endregion
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UnityUserReportingPlatform"/> class.
/// </summary>
public AsyncUnityUserReportingPlatform()
{
this.logMessages = new List<AsyncUnityUserReportingPlatform.LogMessage>();
this.postOperations = new List<AsyncUnityUserReportingPlatform.PostOperation>();
this.screenshotManager = new ScreenshotManager();
// Recorders
this.profilerSamplers = new List<AsyncUnityUserReportingPlatform.ProfilerSampler>();
Dictionary<string, string> samplerNames = this.GetSamplerNames();
foreach (KeyValuePair<string, string> kvp in samplerNames)
{
Sampler sampler = Sampler.Get(kvp.Key);
if (sampler.isValid)
{
Recorder recorder = sampler.GetRecorder();
recorder.enabled = true;
AsyncUnityUserReportingPlatform.ProfilerSampler profilerSampler = new AsyncUnityUserReportingPlatform.ProfilerSampler();
profilerSampler.Name = kvp.Value;
profilerSampler.Recorder = recorder;
this.profilerSamplers.Add(profilerSampler);
}
}
// Log Messages
LogDispatcher.Register(this);
}
#endregion
#region Fields
private List<AsyncUnityUserReportingPlatform.LogMessage> logMessages;
private List<AsyncUnityUserReportingPlatform.PostOperation> postOperations;
private List<AsyncUnityUserReportingPlatform.ProfilerSampler> profilerSamplers;
private ScreenshotManager screenshotManager;
private List<AsyncUnityUserReportingPlatform.PostOperation> taskOperations;
#endregion
#region Methods
/// <inheritdoc cref="IUserReportingPlatform"/>
public T DeserializeJson<T>(string json)
{
return SimpleJson.SimpleJson.DeserializeObject<T>(json);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void OnEndOfFrame(UserReportingClient client)
{
this.screenshotManager.OnEndOfFrame();
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void Post(string endpoint, string contentType, byte[] content, Action<float, float> progressCallback, Action<bool, byte[]> callback)
{
UnityWebRequest webRequest = new UnityWebRequest(endpoint, "POST");
webRequest.uploadHandler = new UploadHandlerRaw(content);
webRequest.downloadHandler = new DownloadHandlerBuffer();
webRequest.SetRequestHeader("Content-Type", contentType);
webRequest.SendWebRequest();
AsyncUnityUserReportingPlatform.PostOperation postOperation = new AsyncUnityUserReportingPlatform.PostOperation();
postOperation.WebRequest = webRequest;
postOperation.Callback = callback;
postOperation.ProgressCallback = progressCallback;
this.postOperations.Add(postOperation);
}
public void ReceiveLogMessage(string logString, string stackTrace, LogType logType)
{
lock (this.logMessages)
{
LogMessage logMessage = new LogMessage();
logMessage.LogString = logString;
logMessage.StackTrace = stackTrace;
logMessage.LogType = logType;
this.logMessages.Add(logMessage);
}
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void RunTask(Func<object> task, Action<object> callback)
{
callback(task());
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void SendAnalyticsEvent(string eventName, Dictionary<string, object> eventData)
{
Analytics.CustomEvent(eventName, eventData);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public string SerializeJson(object instance)
{
return SimpleJson.SimpleJson.SerializeObject(instance);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void TakeScreenshot(int frameNumber, int maximumWidth, int maximumHeight, object source, Action<int, byte[]> callback)
{
this.screenshotManager.TakeScreenshot(source, frameNumber, maximumWidth, maximumHeight, callback);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void Update(UserReportingClient client)
{
// Log Messages
lock (this.logMessages)
{
foreach (AsyncUnityUserReportingPlatform.LogMessage logMessage in this.logMessages)
{
UserReportEventLevel eventLevel = UserReportEventLevel.Info;
if (logMessage.LogType == LogType.Warning)
{
eventLevel = UserReportEventLevel.Warning;
}
else if (logMessage.LogType == LogType.Error)
{
eventLevel = UserReportEventLevel.Error;
}
else if (logMessage.LogType == LogType.Exception)
{
eventLevel = UserReportEventLevel.Error;
}
else if (logMessage.LogType == LogType.Assert)
{
eventLevel = UserReportEventLevel.Error;
}
if (client.IsConnectedToLogger)
{
client.LogEvent(eventLevel, logMessage.LogString, logMessage.StackTrace);
}
}
this.logMessages.Clear();
}
// Metrics
if (client.Configuration.MetricsGatheringMode == MetricsGatheringMode.Automatic)
{
// Sample Automatic Metrics
this.SampleAutomaticMetrics(client);
// Profiler Samplers
foreach (AsyncUnityUserReportingPlatform.ProfilerSampler profilerSampler in this.profilerSamplers)
{
client.SampleMetric(profilerSampler.Name, profilerSampler.GetValue());
}
}
// Post Operations
int postOperationIndex = 0;
while (postOperationIndex < this.postOperations.Count)
{
AsyncUnityUserReportingPlatform.PostOperation postOperation = this.postOperations[postOperationIndex];
if (postOperation.WebRequest.isDone)
{
bool isError = postOperation.WebRequest.error != null && postOperation.WebRequest.responseCode != 200;
if (isError)
{
string errorMessage = string.Format("UnityUserReportingPlatform.Post: {0} {1}", postOperation.WebRequest.responseCode, postOperation.WebRequest.error);
UnityEngine.Debug.Log(errorMessage);
client.LogEvent(UserReportEventLevel.Error, errorMessage);
}
postOperation.ProgressCallback(1, 1);
postOperation.Callback(!isError, postOperation.WebRequest.downloadHandler.data);
this.postOperations.Remove(postOperation);
}
else
{
postOperation.ProgressCallback(postOperation.WebRequest.uploadProgress, postOperation.WebRequest.downloadProgress);
postOperationIndex++;
}
}
}
#endregion
#region Virtual Methods
/// <inheritdoc cref="IUserReportingPlatform"/>
public virtual IDictionary<string, string> GetDeviceMetadata()
{
Dictionary<string, string> deviceMetadata = new Dictionary<string, string>();
// Unity
deviceMetadata.Add("BuildGUID", Application.buildGUID);
deviceMetadata.Add("DeviceModel", SystemInfo.deviceModel);
deviceMetadata.Add("DeviceType", SystemInfo.deviceType.ToString());
deviceMetadata.Add("DeviceUniqueIdentifier", SystemInfo.deviceUniqueIdentifier);
deviceMetadata.Add("DPI", Screen.dpi.ToString(CultureInfo.InvariantCulture));
deviceMetadata.Add("GraphicsDeviceName", SystemInfo.graphicsDeviceName);
deviceMetadata.Add("GraphicsDeviceType", SystemInfo.graphicsDeviceType.ToString());
deviceMetadata.Add("GraphicsDeviceVendor", SystemInfo.graphicsDeviceVendor);
deviceMetadata.Add("GraphicsDeviceVersion", SystemInfo.graphicsDeviceVersion);
deviceMetadata.Add("GraphicsMemorySize", SystemInfo.graphicsMemorySize.ToString());
deviceMetadata.Add("InstallerName", Application.installerName);
deviceMetadata.Add("InstallMode", Application.installMode.ToString());
deviceMetadata.Add("IsEditor", Application.isEditor.ToString());
deviceMetadata.Add("IsFullScreen", Screen.fullScreen.ToString());
deviceMetadata.Add("OperatingSystem", SystemInfo.operatingSystem);
deviceMetadata.Add("OperatingSystemFamily", SystemInfo.operatingSystemFamily.ToString());
deviceMetadata.Add("Orientation", Screen.orientation.ToString());
deviceMetadata.Add("Platform", Application.platform.ToString());
try
{
deviceMetadata.Add("QualityLevel", QualitySettings.names[QualitySettings.GetQualityLevel()]);
}
catch
{
// Empty
}
deviceMetadata.Add("ResolutionWidth", Screen.currentResolution.width.ToString());
deviceMetadata.Add("ResolutionHeight", Screen.currentResolution.height.ToString());
deviceMetadata.Add("ResolutionRefreshRate", Screen.currentResolution.refreshRate.ToString());
deviceMetadata.Add("SystemLanguage", Application.systemLanguage.ToString());
deviceMetadata.Add("SystemMemorySize", SystemInfo.systemMemorySize.ToString());
deviceMetadata.Add("TargetFrameRate", Application.targetFrameRate.ToString());
deviceMetadata.Add("UnityVersion", Application.unityVersion);
deviceMetadata.Add("Version", Application.version);
// Other
deviceMetadata.Add("Source", "Unity");
// Type
Type type = this.GetType();
deviceMetadata.Add("IUserReportingPlatform", type.Name);
// Return
return deviceMetadata;
}
public virtual Dictionary<string, string> GetSamplerNames()
{
Dictionary<string, string> samplerNames = new Dictionary<string, string>();
samplerNames.Add("AudioManager.FixedUpdate", "AudioManager.FixedUpdateInMilliseconds");
samplerNames.Add("AudioManager.Update", "AudioManager.UpdateInMilliseconds");
samplerNames.Add("LateBehaviourUpdate", "Behaviors.LateUpdateInMilliseconds");
samplerNames.Add("BehaviourUpdate", "Behaviors.UpdateInMilliseconds");
samplerNames.Add("Camera.Render", "Camera.RenderInMilliseconds");
samplerNames.Add("Overhead", "Engine.OverheadInMilliseconds");
samplerNames.Add("WaitForRenderJobs", "Engine.WaitForRenderJobsInMilliseconds");
samplerNames.Add("WaitForTargetFPS", "Engine.WaitForTargetFPSInMilliseconds");
samplerNames.Add("GUI.Repaint", "GUI.RepaintInMilliseconds");
samplerNames.Add("Network.Update", "Network.UpdateInMilliseconds");
samplerNames.Add("ParticleSystem.EndUpdateAll", "ParticleSystem.EndUpdateAllInMilliseconds");
samplerNames.Add("ParticleSystem.Update", "ParticleSystem.UpdateInMilliseconds");
samplerNames.Add("Physics.FetchResults", "Physics.FetchResultsInMilliseconds");
samplerNames.Add("Physics.Processing", "Physics.ProcessingInMilliseconds");
samplerNames.Add("Physics.ProcessReports", "Physics.ProcessReportsInMilliseconds");
samplerNames.Add("Physics.Simulate", "Physics.SimulateInMilliseconds");
samplerNames.Add("Physics.UpdateBodies", "Physics.UpdateBodiesInMilliseconds");
samplerNames.Add("Physics.Interpolation", "Physics.InterpolationInMilliseconds");
samplerNames.Add("Physics2D.DynamicUpdate", "Physics2D.DynamicUpdateInMilliseconds");
samplerNames.Add("Physics2D.FixedUpdate", "Physics2D.FixedUpdateInMilliseconds");
return samplerNames;
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public virtual void ModifyUserReport(UserReport userReport)
{
// Active Scene
Scene activeScene = SceneManager.GetActiveScene();
userReport.DeviceMetadata.Add(new UserReportNamedValue("ActiveSceneName", activeScene.name));
// Main Camera
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
userReport.DeviceMetadata.Add(new UserReportNamedValue("MainCameraName", mainCamera.name));
userReport.DeviceMetadata.Add(new UserReportNamedValue("MainCameraPosition", mainCamera.transform.position.ToString()));
userReport.DeviceMetadata.Add(new UserReportNamedValue("MainCameraForward", mainCamera.transform.forward.ToString()));
// Looking At
RaycastHit hit;
if (Physics.Raycast(mainCamera.transform.position, mainCamera.transform.forward, out hit))
{
GameObject lookingAt = hit.transform.gameObject;
userReport.DeviceMetadata.Add(new UserReportNamedValue("LookingAt", hit.point.ToString()));
userReport.DeviceMetadata.Add(new UserReportNamedValue("LookingAtGameObject", lookingAt.name));
userReport.DeviceMetadata.Add(new UserReportNamedValue("LookingAtGameObjectPosition", lookingAt.transform.position.ToString()));
}
}
}
/// <summary>
/// Samples automatic metrics.
/// </summary>
/// <param name="client">The client.</param>
public virtual void SampleAutomaticMetrics(UserReportingClient client)
{
// Graphics
client.SampleMetric("Graphics.FramesPerSecond", 1.0f / Time.deltaTime);
// Memory
client.SampleMetric("Memory.MonoUsedSizeInBytes", Profiler.GetMonoUsedSizeLong());
client.SampleMetric("Memory.TotalAllocatedMemoryInBytes", Profiler.GetTotalAllocatedMemoryLong());
client.SampleMetric("Memory.TotalReservedMemoryInBytes", Profiler.GetTotalReservedMemoryLong());
client.SampleMetric("Memory.TotalUnusedReservedMemoryInBytes", Profiler.GetTotalUnusedReservedMemoryLong());
// Battery
client.SampleMetric("Battery.BatteryLevelInPercent", SystemInfo.batteryLevel);
}
#endregion
}
}

View File

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

View File

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

View File

@@ -0,0 +1,125 @@
using System;
namespace Unity.Screenshots
{
public static class Downsampler
{
#region Static Methods
public static byte[] Downsample(byte[] dataRgba, int stride, int maximumWidth, int maximumHeight, out int downsampledStride)
{
// Preconditions
if (stride == 0)
{
throw new ArgumentException("The stride must be greater than 0.");
}
if (stride % 4 != 0)
{
throw new ArgumentException("The stride must be evenly divisible by 4.");
}
if (dataRgba == null)
{
throw new ArgumentNullException("dataRgba");
}
if (dataRgba.Length == 0)
{
throw new ArgumentException("The data length must be greater than 0.");
}
if (dataRgba.Length % 4 != 0)
{
throw new ArgumentException("The data must be evenly divisible by 4.");
}
if (dataRgba.Length % stride != 0)
{
throw new ArgumentException("The data must be evenly divisible by the stride.");
}
// Implementation
int width = stride / 4;
int height = dataRgba.Length / stride;
float ratioX = maximumWidth / (float) width;
float ratioY = maximumHeight / (float) height;
float ratio = Math.Min(ratioX, ratioY);
if (ratio < 1)
{
int downsampledWidth = (int) Math.Round(width * ratio);
int downsampledHeight = (int) Math.Round(height * ratio);
float[] downsampledData = new float[downsampledWidth * downsampledHeight * 4];
float sampleWidth = width / (float) downsampledWidth;
float sampleHeight = height / (float) downsampledHeight;
int kernelWidth = (int) Math.Floor(sampleWidth);
int kernelHeight = (int) Math.Floor(sampleHeight);
int kernelSize = kernelWidth * kernelHeight;
for (int y = 0; y < downsampledHeight; y++)
{
for (int x = 0; x < downsampledWidth; x++)
{
int destinationIndex = y * downsampledWidth * 4 + x * 4;
int sampleLowerX = (int) Math.Floor(x * sampleWidth);
int sampleLowerY = (int) Math.Floor(y * sampleHeight);
int sampleUpperX = sampleLowerX + kernelWidth;
int sampleUpperY = sampleLowerY + kernelHeight;
for (int sampleY = sampleLowerY; sampleY < sampleUpperY; sampleY++)
{
if (sampleY >= height)
{
continue;
}
for (int sampleX = sampleLowerX; sampleX < sampleUpperX; sampleX++)
{
if (sampleX >= width)
{
continue;
}
int sourceIndex = sampleY * width * 4 + sampleX * 4;
downsampledData[destinationIndex] += dataRgba[sourceIndex];
downsampledData[destinationIndex + 1] += dataRgba[sourceIndex + 1];
downsampledData[destinationIndex + 2] += dataRgba[sourceIndex + 2];
downsampledData[destinationIndex + 3] += dataRgba[sourceIndex + 3];
}
}
downsampledData[destinationIndex] /= kernelSize;
downsampledData[destinationIndex + 1] /= kernelSize;
downsampledData[destinationIndex + 2] /= kernelSize;
downsampledData[destinationIndex + 3] /= kernelSize;
}
}
byte[] flippedData = new byte[downsampledWidth * downsampledHeight * 4];
for (int y = 0; y < downsampledHeight; y++)
{
for (int x = 0; x < downsampledWidth; x++)
{
int sourceIndex = (downsampledHeight - 1 - y) * downsampledWidth * 4 + x * 4;
int destinationIndex = y * downsampledWidth * 4 + x * 4;
flippedData[destinationIndex] += (byte) downsampledData[sourceIndex];
flippedData[destinationIndex + 1] += (byte) downsampledData[sourceIndex + 1];
flippedData[destinationIndex + 2] += (byte) downsampledData[sourceIndex + 2];
flippedData[destinationIndex + 3] += (byte) downsampledData[sourceIndex + 3];
}
}
downsampledStride = downsampledWidth * 4;
return flippedData;
}
else
{
byte[] flippedData = new byte[dataRgba.Length];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int sourceIndex = (height - 1 - y) * width * 4 + x * 4;
int destinationIndex = y * width * 4 + x * 4;
flippedData[destinationIndex] += (byte) dataRgba[sourceIndex];
flippedData[destinationIndex + 1] += (byte) dataRgba[sourceIndex + 1];
flippedData[destinationIndex + 2] += (byte) dataRgba[sourceIndex + 2];
flippedData[destinationIndex + 3] += (byte) dataRgba[sourceIndex + 3];
}
}
downsampledStride = width * 4;
return flippedData;
}
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,303 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
namespace Unity.Screenshots
{
/// <summary>
/// Provides PNG encoding.
/// </summary>
/// <remarks>This is a minimal implementation of a PNG encoder with no scanline filtering or additional features.</remarks>
public static class PngEncoder
{
#region Nested Types
public class Crc32
{
#region Static Fields
private static UInt32 generator = 0xEDB88320;
#endregion
#region Constructors
public Crc32()
{
this.checksumTable = Enumerable.Range(0, 256)
.Select(i =>
{
uint tableEntry = (uint)i;
for (int j = 0; j < 8; j++)
{
tableEntry = (tableEntry & 1) != 0 ? Crc32.generator ^ (tableEntry >> 1) : tableEntry >> 1;
}
return tableEntry;
})
.ToArray();
}
#endregion
#region Fields
private readonly UInt32[] checksumTable;
#endregion
#region Methods
public UInt32 Calculate<T>(IEnumerable<T> byteStream)
{
return ~byteStream.Aggregate(0xFFFFFFFF, (checksumRegister, currentByte) => this.checksumTable[(checksumRegister & 0xFF) ^ Convert.ToByte(currentByte)] ^ (checksumRegister >> 8));
}
#endregion
}
#endregion
#region Static Constructors
static PngEncoder()
{
PngEncoder.crc32 = new Crc32();
}
#endregion
#region Static Fields
private static Crc32 crc32;
#endregion
#region Static Methods
private static uint Adler32(byte[] bytes)
{
const int mod = 65521;
uint a = 1, b = 0;
foreach (byte byteValue in bytes)
{
a = (a + byteValue) % mod;
b = (b + a) % mod;
}
return (b << 16) | a;
}
private static void AppendByte(this byte[] data, ref int position, byte value)
{
data[position] = value;
position++;
}
private static void AppendBytes(this byte[] data, ref int position, byte[] value)
{
foreach (byte valueByte in value)
{
data.AppendByte(ref position, valueByte);
}
}
private static void AppendChunk(this byte[] data, ref int position, string chunkType, byte[] chunkData)
{
byte[] chunkTypeBytes = PngEncoder.GetChunkTypeBytes(chunkType);
if (chunkTypeBytes != null)
{
data.AppendInt(ref position, chunkData.Length);
data.AppendBytes(ref position, chunkTypeBytes);
data.AppendBytes(ref position, chunkData);
data.AppendUInt(ref position, PngEncoder.GetChunkCrc(chunkTypeBytes, chunkData));
}
}
private static void AppendInt(this byte[] data, ref int position, int value)
{
byte[] valueBytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(valueBytes);
}
data.AppendBytes(ref position, valueBytes);
}
private static void AppendUInt(this byte[] data, ref int position, uint value)
{
byte[] valueBytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(valueBytes);
}
data.AppendBytes(ref position, valueBytes);
}
private static byte[] Compress(byte[] bytes)
{
using (MemoryStream outStream = new MemoryStream())
{
using (DeflateStream gZipStream = new DeflateStream(outStream, CompressionMode.Compress))
using (MemoryStream mStream = new MemoryStream(bytes))
{
mStream.WriteTo(gZipStream);
}
byte[] compressedBytes = outStream.ToArray();
return compressedBytes;
}
}
public static byte[] Encode(byte[] dataRgba, int stride)
{
// Preconditions
if (dataRgba == null)
{
throw new ArgumentNullException("dataRgba");
}
if (dataRgba.Length == 0)
{
throw new ArgumentException("The data length must be greater than 0.");
}
if (stride == 0)
{
throw new ArgumentException("The stride must be greater than 0.");
}
if (stride % 4 != 0)
{
throw new ArgumentException("The stride must be evenly divisible by 4.");
}
if (dataRgba.Length % 4 != 0)
{
throw new ArgumentException("The data must be evenly divisible by 4.");
}
if (dataRgba.Length % stride != 0)
{
throw new ArgumentException("The data must be evenly divisible by the stride.");
}
// Dimensions
int pixels = dataRgba.Length / 4;
int width = stride / 4;
int height = pixels / width;
// IHDR Chunk
byte[] ihdrData = new byte[13];
int ihdrPosition = 0;
ihdrData.AppendInt(ref ihdrPosition, width);
ihdrData.AppendInt(ref ihdrPosition, height);
ihdrData.AppendByte(ref ihdrPosition, 8); // Bit depth
ihdrData.AppendByte(ref ihdrPosition, 6); // Color type (color + alpha)
ihdrData.AppendByte(ref ihdrPosition, 0); // Compression method (always 0)
ihdrData.AppendByte(ref ihdrPosition, 0); // Filter method (always 0)
ihdrData.AppendByte(ref ihdrPosition, 0); // Interlace method (no interlacing)
// IDAT Chunk
byte[] scanlineData = new byte[dataRgba.Length + height];
int scanlineDataPosition = 0;
int scanlinePosition = 0;
for (int i = 0; i < dataRgba.Length; i++)
{
if (scanlinePosition >= stride)
{
scanlinePosition = 0;
}
if (scanlinePosition == 0)
{
scanlineData.AppendByte(ref scanlineDataPosition, 0);
}
scanlineData.AppendByte(ref scanlineDataPosition, dataRgba[i]);
scanlinePosition++;
}
byte[] compressedScanlineData = PngEncoder.Compress(scanlineData);
byte[] idatData = new byte[1 + 1 + compressedScanlineData.Length + 4];
int idatPosition = 0;
idatData.AppendByte(ref idatPosition, 0x78); // Zlib header
idatData.AppendByte(ref idatPosition, 0x9C); // Zlib header
idatData.AppendBytes(ref idatPosition, compressedScanlineData); // Data
idatData.AppendUInt(ref idatPosition, PngEncoder.Adler32(scanlineData)); // Adler32 checksum
// Png
byte[] png = new byte[8 + ihdrData.Length + 12 + idatData.Length + 12 + 12];
// Position
int position = 0;
// Signature
png.AppendByte(ref position, 0x89); // High bit set
png.AppendByte(ref position, 0x50); // P
png.AppendByte(ref position, 0x4E); // N
png.AppendByte(ref position, 0x47); // G
png.AppendByte(ref position, 0x0D); // DOS line ending
png.AppendByte(ref position, 0x0A); // DOS line ending
png.AppendByte(ref position, 0x1A); // DOS end of file
png.AppendByte(ref position, 0x0A); // Unix line ending
// Assemble
png.AppendChunk(ref position, "IHDR", ihdrData);
png.AppendChunk(ref position, "IDAT", idatData);
png.AppendChunk(ref position, "IEND", new byte[0]);
// Return
return png;
}
public static void EncodeAsync(byte[] dataRgba, int stride, Action<Exception, byte[]> callback)
{
ThreadPool.QueueUserWorkItem((state) =>
{
try
{
byte[] png = PngEncoder.Encode(dataRgba, stride);
callback(null, png);
}
catch (Exception ex)
{
callback(ex, null);
throw;
}
}, null);
}
private static uint GetChunkCrc(byte[] chunkTypeBytes, byte[] chunkData)
{
byte[] combinedBytes = new byte[chunkTypeBytes.Length + chunkData.Length];
Array.Copy(chunkTypeBytes, 0, combinedBytes, 0, chunkTypeBytes.Length);
Array.Copy(chunkData, 0, combinedBytes, 4, chunkData.Length);
return PngEncoder.crc32.Calculate(combinedBytes);
}
private static byte[] GetChunkTypeBytes(string value)
{
char[] characters = value.ToCharArray();
if (characters.Length < 4)
{
return null;
}
byte[] type = new byte[4];
for (int i = 0; i < type.Length; i++)
{
type[i] = (byte)characters[i];
}
return type;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Screenshots
{
public class ScreenshotManager
{
#region Nested Types
private class ScreenshotOperation
{
#region Properties
public Action<int, byte[]> Callback { get; set; }
public byte[] Data { get; set; }
public int FrameNumber { get; set; }
public bool IsAwaiting { get; set; }
public bool IsComplete { get; set; }
public bool IsInUse { get; set; }
public int MaximumHeight { get; set; }
public int MaximumWidth { get; set; }
public object Source { get; set; }
#endregion
#region Methods
public void Use()
{
this.Callback = null;
this.Data = null;
this.FrameNumber = 0;
this.IsAwaiting = true;
this.IsComplete = false;
this.IsInUse = true;
this.MaximumHeight = 0;
this.MaximumWidth = 0;
this.Source = null;
}
#endregion
}
#endregion
#region Constructors
public ScreenshotManager()
{
this.screenshotRecorder = new ScreenshotRecorder();
this.screenshotCallbackDelegate = this.ScreenshotCallback;
this.screenshotOperations = new List<ScreenshotOperation>();
}
#endregion
#region Fields
private Action<byte[], object> screenshotCallbackDelegate;
private List<ScreenshotOperation> screenshotOperations;
private ScreenshotRecorder screenshotRecorder;
#endregion
#region Methods
private ScreenshotOperation GetScreenshotOperation()
{
foreach (ScreenshotOperation screenshotOperation in this.screenshotOperations)
{
if (!screenshotOperation.IsInUse)
{
screenshotOperation.Use();
return screenshotOperation;
}
}
ScreenshotOperation newScreenshotOperation = new ScreenshotOperation();
newScreenshotOperation.Use();
this.screenshotOperations.Add(newScreenshotOperation);
return newScreenshotOperation;
}
public void OnEndOfFrame()
{
foreach (ScreenshotOperation screenshotOperation in this.screenshotOperations)
{
if (screenshotOperation.IsInUse)
{
if (screenshotOperation.IsAwaiting)
{
screenshotOperation.IsAwaiting = false;
if (screenshotOperation.Source == null)
{
this.screenshotRecorder.Screenshot(screenshotOperation.MaximumWidth, screenshotOperation.MaximumHeight, ScreenshotType.Png, this.screenshotCallbackDelegate, screenshotOperation);
}
else if (screenshotOperation.Source is Camera)
{
this.screenshotRecorder.Screenshot(screenshotOperation.Source as Camera, screenshotOperation.MaximumWidth, screenshotOperation.MaximumHeight, ScreenshotType.Png, this.screenshotCallbackDelegate, screenshotOperation);
}
else if (screenshotOperation.Source is RenderTexture)
{
this.screenshotRecorder.Screenshot(screenshotOperation.Source as RenderTexture, screenshotOperation.MaximumWidth, screenshotOperation.MaximumHeight, ScreenshotType.Png, this.screenshotCallbackDelegate, screenshotOperation);
}
else if (screenshotOperation.Source is Texture2D)
{
this.screenshotRecorder.Screenshot(screenshotOperation.Source as Texture2D, screenshotOperation.MaximumWidth, screenshotOperation.MaximumHeight, ScreenshotType.Png, this.screenshotCallbackDelegate, screenshotOperation);
}
else
{
this.ScreenshotCallback(null, screenshotOperation);
}
}
else if (screenshotOperation.IsComplete)
{
screenshotOperation.IsInUse = false;
try
{
if (screenshotOperation != null && screenshotOperation.Callback != null)
{
screenshotOperation.Callback(screenshotOperation.FrameNumber, screenshotOperation.Data);
}
}
catch
{
// Do Nothing
}
}
}
}
}
private void ScreenshotCallback(byte[] data, object state)
{
ScreenshotOperation screenshotOperation = state as ScreenshotOperation;
if (screenshotOperation != null)
{
screenshotOperation.Data = data;
screenshotOperation.IsComplete = true;
}
}
public void TakeScreenshot(object source, int frameNumber, int maximumWidth, int maximumHeight, Action<int, byte[]> callback)
{
ScreenshotOperation screenshotOperation = this.GetScreenshotOperation();
screenshotOperation.FrameNumber = frameNumber;
screenshotOperation.MaximumWidth = maximumWidth;
screenshotOperation.MaximumHeight = maximumHeight;
screenshotOperation.Source = source;
screenshotOperation.Callback = callback;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
namespace Unity.Screenshots
{
public class ScreenshotRecorder
{
#region Nested Types
private class ScreenshotOperation
{
#region Constructors
public ScreenshotOperation()
{
this.ScreenshotCallbackDelegate = this.ScreenshotCallback;
this.EncodeCallbackDelegate = this.EncodeCallback;
}
#endregion
#region Fields
public WaitCallback EncodeCallbackDelegate;
public Action<AsyncGPUReadbackRequest> ScreenshotCallbackDelegate;
#endregion
#region Properties
public Action<byte[], object> Callback { get; set; }
public int Height { get; set; }
public int Identifier { get; set; }
public bool IsInUse { get; set; }
public int MaximumHeight { get; set; }
public int MaximumWidth { get; set; }
public NativeArray<byte> NativeData { get; set; }
public Texture Source { get; set; }
public object State { get; set; }
public ScreenshotType Type { get; set; }
public int Width { get; set; }
#endregion
#region Methods
private void EncodeCallback(object state)
{
byte[] byteData = this.NativeData.ToArray();
int downsampledStride;
byteData = Downsampler.Downsample(byteData, this.Width * 4, this.MaximumWidth, this.MaximumHeight, out downsampledStride);
if (this.Type == ScreenshotType.Png)
{
byteData = PngEncoder.Encode(byteData, downsampledStride);
}
if (this.Callback != null)
{
this.Callback(byteData, this.State);
}
this.NativeData.Dispose();
this.IsInUse = false;
}
private void SavePngToDisk(byte[] byteData)
{
if (!Directory.Exists("Screenshots"))
{
Directory.CreateDirectory("Screenshots");
}
File.WriteAllBytes(string.Format("Screenshots/{0}.png", this.Identifier % 60), byteData);
}
private void ScreenshotCallback(AsyncGPUReadbackRequest request)
{
if (!request.hasError)
{
NativeLeakDetection.Mode = NativeLeakDetectionMode.Disabled;
NativeArray<byte> data = request.GetData<byte>();
NativeArray<byte> persistentData = new NativeArray<byte>(data, Allocator.Persistent);
this.Width = request.width;
this.Height = request.height;
this.NativeData = persistentData;
ThreadPool.QueueUserWorkItem(this.EncodeCallbackDelegate, null);
}
else
{
if (this.Callback != null)
{
this.Callback(null, this.State);
}
}
if (this.Source != null)
{
UnityEngine.Object.Destroy(this.Source);
}
}
#endregion
}
#endregion
#region Static Fields
private static int nextIdentifier;
#endregion
#region Constructors
public ScreenshotRecorder()
{
this.operationPool = new List<ScreenshotOperation>();
}
#endregion
#region Fields
private List<ScreenshotOperation> operationPool;
#endregion
#region Methods
private ScreenshotOperation GetOperation()
{
foreach (ScreenshotOperation operation in this.operationPool)
{
if (!operation.IsInUse)
{
operation.IsInUse = true;
return operation;
}
}
ScreenshotOperation newOperation = new ScreenshotOperation();
newOperation.IsInUse = true;
this.operationPool.Add(newOperation);
return newOperation;
}
public void Screenshot(int maximumWidth, int maximumHeight, ScreenshotType type, Action<byte[], object> callback, object state)
{
Texture2D texture = ScreenCapture.CaptureScreenshotAsTexture();
this.Screenshot(texture, maximumWidth, maximumHeight, type, callback, state);
}
public void Screenshot(Camera source, int maximumWidth, int maximumHeight, ScreenshotType type, Action<byte[], object> callback, object state)
{
RenderTexture renderTexture = new RenderTexture(maximumWidth, maximumHeight, 24);
RenderTexture originalTargetTexture = source.targetTexture;
source.targetTexture = renderTexture;
source.Render();
source.targetTexture = originalTargetTexture;
this.Screenshot(renderTexture, maximumWidth, maximumHeight, type, callback, state);
}
public void Screenshot(RenderTexture source, int maximumWidth, int maximumHeight, ScreenshotType type, Action<byte[], object> callback, object state)
{
this.ScreenshotInternal(source, maximumWidth, maximumHeight, type, callback, state);
}
public void Screenshot(Texture2D source, int maximumWidth, int maximumHeight, ScreenshotType type, Action<byte[], object> callback, object state)
{
this.ScreenshotInternal(source, maximumWidth, maximumHeight, type, callback, state);
}
private void ScreenshotInternal(Texture source, int maximumWidth, int maximumHeight, ScreenshotType type, Action<byte[], object> callback, object state)
{
ScreenshotOperation operation = this.GetOperation();
operation.Identifier = ScreenshotRecorder.nextIdentifier++;
operation.Source = source;
operation.MaximumWidth = maximumWidth;
operation.MaximumHeight = maximumHeight;
operation.Type = type;
operation.Callback = callback;
operation.State = state;
AsyncGPUReadback.Request(source, 0, TextureFormat.RGBA32, operation.ScreenshotCallbackDelegate);
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,9 @@
namespace Unity.Screenshots
{
public enum ScreenshotType
{
Raw = 0,
Png = 1
}
}

View File

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

View File

@@ -0,0 +1,20 @@
using Unity.Cloud.UserReporting.Plugin;
using UnityEngine;
/// <summary>
/// Represents a behavior that configures user reporting, but does not provide any additional functionality.
/// </summary>
public class UserReportingConfigureOnly : MonoBehaviour
{
#region Methods
private void Start()
{
if (UnityUserReporting.CurrentClient == null)
{
UnityUserReporting.Configure();
}
}
#endregion
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: b5a1e6d739a0210438da570da30e4a8c
timeCreated: 1518037533
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,121 @@
using System;
using Unity.Cloud.UserReporting;
using Unity.Cloud.UserReporting.Plugin;
using UnityEngine;
/// <summary>
/// Represents a behavior that monitors the application for issues and automatically submits a user report.
/// </summary>
public class UserReportingMonitor : MonoBehaviour
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportingMonitor"/> class.
/// </summary>
public UserReportingMonitor()
{
this.IsEnabled = true;
this.IsHiddenWithoutDimension = true;
Type type = this.GetType();
this.MonitorName = type.Name;
}
#endregion
#region Fields
/// <summary>
/// Gets or sets a value indicating whether the monitor is enabled.
/// </summary>
public bool IsEnabled;
/// <summary>
/// Gets or sets a value indicating whether the monitor is enabled after it is triggered.
/// </summary>
public bool IsEnabledAfterTrigger;
/// <summary>
/// Gets or sets a value indicating whether the user report has IsHiddenWithoutDimension set.
/// </summary>
public bool IsHiddenWithoutDimension;
/// <summary>
/// Gets or sets the monitor name.
/// </summary>
public string MonitorName;
/// <summary>
/// Gets or sets the summary.
/// </summary>
public string Summary;
#endregion
#region Methods
private void Start()
{
if (UnityUserReporting.CurrentClient == null)
{
UnityUserReporting.Configure();
}
}
/// <summary>
/// Triggers the monitor.
/// </summary>
public void Trigger()
{
if (!this.IsEnabledAfterTrigger)
{
this.IsEnabled = false;
}
UnityUserReporting.CurrentClient.TakeScreenshot(2048, 2048, s => { });
UnityUserReporting.CurrentClient.TakeScreenshot(512, 512, s => { });
UnityUserReporting.CurrentClient.CreateUserReport((br) =>
{
if (string.IsNullOrEmpty(br.ProjectIdentifier))
{
Debug.LogWarning("The user report's project identifier is not set. Please setup cloud services using the Services tab or manually specify a project identifier when calling UnityUserReporting.Configure().");
}
br.Summary = this.Summary;
br.DeviceMetadata.Add(new UserReportNamedValue("Monitor", this.MonitorName));
string platform = "Unknown";
string version = "0.0";
foreach (UserReportNamedValue deviceMetadata in br.DeviceMetadata)
{
if (deviceMetadata.Name == "Platform")
{
platform = deviceMetadata.Value;
}
if (deviceMetadata.Name == "Version")
{
version = deviceMetadata.Value;
}
}
br.Dimensions.Add(new UserReportNamedValue("Monitor.Platform.Version", string.Format("{0}.{1}.{2}", this.MonitorName, platform, version)));
br.Dimensions.Add(new UserReportNamedValue("Monitor", this.MonitorName));
br.IsHiddenWithoutDimension = this.IsHiddenWithoutDimension;
UnityUserReporting.CurrentClient.SendUserReport(br, (success, br2) => { this.Triggered(); });
});
}
#endregion
#region Virtual Methods
/// <summary>
/// Called when the monitor is triggered.
/// </summary>
protected virtual void Triggered()
{
// Empty
}
#endregion
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 841e6b5c52ae68e48951d215043a53ee
timeCreated: 1517959061
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
using System;
[Serializable]
public enum UserReportingPlatformType
{
Default,
Async
}

View File

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

View File

@@ -0,0 +1,471 @@
using System;
using System.Collections;
using System.Reflection;
using System.Text;
using Unity.Cloud.UserReporting;
using Unity.Cloud.UserReporting.Client;
using Unity.Cloud.UserReporting.Plugin;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// Represents a behavior for working with the user reporting client.
/// </summary>
/// <remarks>
/// This script is provided as a sample and isn't necessarily the most optimal solution for your project.
/// You may want to consider replacing with this script with your own script in the future.
/// </remarks>
public class UserReportingScript : MonoBehaviour
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportingScript"/> class.
/// </summary>
public UserReportingScript()
{
this.UserReportSubmitting = new UnityEvent();
this.unityUserReportingUpdater = new UnityUserReportingUpdater();
}
#endregion
#region Fields
/// <summary>
/// Gets or sets the category dropdown.
/// </summary>
[Tooltip("The category dropdown.")] public Dropdown CategoryDropdown;
/// <summary>
/// Gets or sets the description input on the user report form.
/// </summary>
[Tooltip("The description input on the user report form.")]
public InputField DescriptionInput;
/// <summary>
/// Gets or sets the UI shown when there's an error.
/// </summary>
[Tooltip("The UI shown when there's an error.")]
public Canvas ErrorPopup;
private bool isCreatingUserReport;
/// <summary>
/// Gets or sets a value indicating whether the hotkey is enabled (Left Alt + Left Shift + B).
/// </summary>
[Tooltip("A value indicating whether the hotkey is enabled (Left Alt + Left Shift + B).")]
public bool IsHotkeyEnabled;
/// <summary>
/// Gets or sets a value indicating whether the prefab is in silent mode. Silent mode does not show the user report form.
/// </summary>
[Tooltip("A value indicating whether the prefab is in silent mode. Silent mode does not show the user report form.")]
public bool IsInSilentMode;
/// <summary>
/// Gets or sets a value indicating whether the user report client reports metrics about itself.
/// </summary>
[Tooltip("A value indicating whether the user report client reports metrics about itself.")]
public bool IsSelfReporting;
private bool isShowingError;
private bool isSubmitting;
/// <summary>
/// Gets or sets the display text for the progress text.
/// </summary>
[Tooltip("The display text for the progress text.")]
public Text ProgressText;
/// <summary>
/// Gets or sets a value indicating whether the user report client send events to analytics.
/// </summary>
[Tooltip("A value indicating whether the user report client send events to analytics.")]
public bool SendEventsToAnalytics;
/// <summary>
/// Gets or sets the UI shown while submitting.
/// </summary>
[Tooltip("The UI shown while submitting.")]
public Canvas SubmittingPopup;
/// <summary>
/// Gets or sets the summary input on the user report form.
/// </summary>
[Tooltip("The summary input on the user report form.")]
public InputField SummaryInput;
/// <summary>
/// Gets or sets the thumbnail viewer on the user report form.
/// </summary>
[Tooltip("The thumbnail viewer on the user report form.")]
public Image ThumbnailViewer;
private UnityUserReportingUpdater unityUserReportingUpdater;
/// <summary>
/// Gets or sets the user report button used to create a user report.
/// </summary>
[Tooltip("The user report button used to create a user report.")]
public Button UserReportButton;
/// <summary>
/// Gets or sets the UI for the user report form. Shown after a user report is created.
/// </summary>
[Tooltip("The UI for the user report form. Shown after a user report is created.")]
public Canvas UserReportForm;
/// <summary>
/// Gets or sets the User Reporting platform. Different platforms have different features but may require certain Unity versions or target platforms. The Async platform adds async screenshotting and report creation, but requires Unity 2018.3 and above, the package manager version of Unity User Reporting, and a target platform that supports asynchronous GPU readback such as DirectX.
/// </summary>
[Tooltip("The User Reporting platform. Different platforms have different features but may require certain Unity versions or target platforms. The Async platform adds async screenshotting and report creation, but requires Unity 2018.3 and above, the package manager version of Unity User Reporting, and a target platform that supports asynchronous GPU readback such as DirectX.")]
public UserReportingPlatformType UserReportingPlatform;
/// <summary>
/// Gets or sets the UI for the event raised when a user report is submitting.
/// </summary>
[Tooltip("The event raised when a user report is submitting.")]
public UnityEvent UserReportSubmitting;
#endregion
#region Properties
/// <summary>
/// Gets the current user report.
/// </summary>
public UserReport CurrentUserReport { get; private set; }
/// <summary>
/// Gets the current state.
/// </summary>
public UserReportingState State
{
get
{
if (this.CurrentUserReport != null)
{
if (this.IsInSilentMode)
{
return UserReportingState.Idle;
}
else if (this.isSubmitting)
{
return UserReportingState.SubmittingForm;
}
else
{
return UserReportingState.ShowingForm;
}
}
else
{
if (this.isCreatingUserReport)
{
return UserReportingState.CreatingUserReport;
}
else
{
return UserReportingState.Idle;
}
}
}
}
#endregion
#region Methods
/// <summary>
/// Cancels the user report.
/// </summary>
public void CancelUserReport()
{
this.CurrentUserReport = null;
this.ClearForm();
}
private IEnumerator ClearError()
{
yield return new WaitForSeconds(10);
this.isShowingError = false;
}
private void ClearForm()
{
this.SummaryInput.text = null;
this.DescriptionInput.text = null;
}
/// <summary>
/// Creates a user report.
/// </summary>
public void CreateUserReport()
{
// Check Creating Flag
if (this.isCreatingUserReport)
{
return;
}
// Set Creating Flag
this.isCreatingUserReport = true;
// Take Main Screenshot
UnityUserReporting.CurrentClient.TakeScreenshot(2048, 2048, s => { });
// Take Thumbnail Screenshot
UnityUserReporting.CurrentClient.TakeScreenshot(512, 512, s => { });
// Create Report
UnityUserReporting.CurrentClient.CreateUserReport((br) =>
{
// Ensure Project Identifier
if (string.IsNullOrEmpty(br.ProjectIdentifier))
{
Debug.LogWarning("The user report's project identifier is not set. Please setup cloud services using the Services tab or manually specify a project identifier when calling UnityUserReporting.Configure().");
}
// Attachments
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++)
{
sb.AppendLine("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque pharetra dui id mauris convallis dignissim. In id tortor ut augue aliquam molestie. Curabitur placerat, enim id suscipit feugiat, orci turpis malesuada diam, quis elementum purus sapien at orci. Vivamus efficitur, eros mattis suscipit mollis, lorem lectus efficitur massa, et egestas lectus tellus eu mauris. Suspendisse venenatis tempus interdum. In sed ultrices magna, a aliquet erat. Donec imperdiet nulla purus, vel rhoncus turpis fermentum et. Sed quis scelerisque velit. Integer ac urna arcu. Integer erat tellus, mollis id malesuada sed, eleifend nec justo. Donec vestibulum, lacus non volutpat elementum, ligula turpis aliquet diam, et dictum lectus metus vel mi. Sed lobortis lectus id rhoncus pharetra. Fusce ac imperdiet dolor, in rutrum lorem. Nam molestie diam tellus, a laoreet velit finibus et. Nam auctor metus purus, in elementum ante finibus at. Donec nunc lectus, dapibus quis augue sit amet, vulputate commodo felis. Morbi ut est sed.");
}
br.Attachments.Add(new UserReportAttachment("Sample Attachment.json", "SampleAttachment.json", "application/json", System.Text.Encoding.UTF8.GetBytes(sb.ToString())));
// Dimensions
string platform = "Unknown";
string version = "0.0";
foreach (UserReportNamedValue deviceMetadata in br.DeviceMetadata)
{
if (deviceMetadata.Name == "Platform")
{
platform = deviceMetadata.Value;
}
if (deviceMetadata.Name == "Version")
{
version = deviceMetadata.Value;
}
}
br.Dimensions.Add(new UserReportNamedValue("Platform.Version", string.Format("{0}.{1}", platform, version)));
// Set Current Report
this.CurrentUserReport = br;
// Set Creating Flag
this.isCreatingUserReport = false;
// Set Thumbnail
this.SetThumbnail(br);
// Submit Immediately in Silent Mode
if (this.IsInSilentMode)
{
this.SubmitUserReport();
}
});
}
private UserReportingClientConfiguration GetConfiguration()
{
return new UserReportingClientConfiguration();
}
/// <summary>
/// Gets a value indicating whether the user report is submitting.
/// </summary>
/// <returns>A value indicating whether the user report is submitting.</returns>
public bool IsSubmitting()
{
return this.isSubmitting;
}
private void SetThumbnail(UserReport userReport)
{
if (userReport != null && this.ThumbnailViewer != null)
{
byte[] data = Convert.FromBase64String(userReport.Thumbnail.DataBase64);
Texture2D texture = new Texture2D(1, 1);
texture.LoadImage(data);
this.ThumbnailViewer.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5F, 0.5F));
this.ThumbnailViewer.preserveAspect = true;
}
}
private void Start()
{
// Set Up Event System
if (Application.isPlaying)
{
EventSystem sceneEventSystem = UnityEngine.Object.FindObjectOfType<EventSystem>();
if (sceneEventSystem == null)
{
GameObject eventSystem = new GameObject("EventSystem");
eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
}
}
// Configure Client
bool configured = false;
if (this.UserReportingPlatform == UserReportingPlatformType.Async)
{
Assembly assembly = Assembly.GetExecutingAssembly();
Type asyncUnityUserReportingPlatformType = assembly.GetType("Unity.Cloud.UserReporting.Plugin.Version2018_3.AsyncUnityUserReportingPlatform");
if (asyncUnityUserReportingPlatformType != null)
{
object activatedObject = Activator.CreateInstance(asyncUnityUserReportingPlatformType);
IUserReportingPlatform asyncUnityUserReportingPlatform = activatedObject as IUserReportingPlatform;
if (asyncUnityUserReportingPlatform != null)
{
UnityUserReporting.Configure(asyncUnityUserReportingPlatform, this.GetConfiguration());
configured = true;
}
}
}
if (!configured)
{
UnityUserReporting.Configure(this.GetConfiguration());
}
// Ping
string url = string.Format("https://userreporting.cloud.unity3d.com/api/userreporting/projects/{0}/ping", UnityUserReporting.CurrentClient.ProjectIdentifier);
UnityUserReporting.CurrentClient.Platform.Post(url, "application/json", Encoding.UTF8.GetBytes("\"Ping\""), (upload, download) => { }, (result, bytes) => { });
}
/// <summary>
/// Submits the user report.
/// </summary>
public void SubmitUserReport()
{
// Preconditions
if (this.isSubmitting || this.CurrentUserReport == null)
{
return;
}
// Set Submitting Flag
this.isSubmitting = true;
// Set Summary
if (this.SummaryInput != null)
{
this.CurrentUserReport.Summary = this.SummaryInput.text;
}
// Set Category
if (this.CategoryDropdown != null)
{
Dropdown.OptionData optionData = this.CategoryDropdown.options[this.CategoryDropdown.value];
string category = optionData.text;
this.CurrentUserReport.Dimensions.Add(new UserReportNamedValue("Category", category));
this.CurrentUserReport.Fields.Add(new UserReportNamedValue("Category", category));
}
// Set Description
// This is how you would add additional fields.
if (this.DescriptionInput != null)
{
UserReportNamedValue userReportField = new UserReportNamedValue();
userReportField.Name = "Description";
userReportField.Value = this.DescriptionInput.text;
this.CurrentUserReport.Fields.Add(userReportField);
}
// Clear Form
this.ClearForm();
// Raise Event
this.RaiseUserReportSubmitting();
// Send Report
UnityUserReporting.CurrentClient.SendUserReport(this.CurrentUserReport, (uploadProgress, downloadProgress) =>
{
if (this.ProgressText != null)
{
string progressText = string.Format("{0:P}", uploadProgress);
this.ProgressText.text = progressText;
}
}, (success, br2) =>
{
if (!success)
{
this.isShowingError = true;
this.StartCoroutine(this.ClearError());
}
this.CurrentUserReport = null;
this.isSubmitting = false;
});
}
private void Update()
{
// Hotkey Support
if (this.IsHotkeyEnabled)
{
if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.LeftAlt))
{
if (Input.GetKeyDown(KeyCode.B))
{
this.CreateUserReport();
}
}
}
// Update Client
UnityUserReporting.CurrentClient.IsSelfReporting = this.IsSelfReporting;
UnityUserReporting.CurrentClient.SendEventsToAnalytics = this.SendEventsToAnalytics;
// Update UI
if (this.UserReportButton != null)
{
this.UserReportButton.interactable = this.State == UserReportingState.Idle;
}
if (this.UserReportForm != null)
{
this.UserReportForm.enabled = this.State == UserReportingState.ShowingForm;
}
if (this.SubmittingPopup != null)
{
this.SubmittingPopup.enabled = this.State == UserReportingState.SubmittingForm;
}
if (this.ErrorPopup != null)
{
this.ErrorPopup.enabled = this.isShowingError;
}
// Update Client
// The UnityUserReportingUpdater updates the client at multiple points during the current frame.
this.unityUserReportingUpdater.Reset();
this.StartCoroutine(this.unityUserReportingUpdater);
}
#endregion
#region Virtual Methods
/// <summary>
/// Occurs when a user report is submitting.
/// </summary>
protected virtual void RaiseUserReportSubmitting()
{
if (this.UserReportSubmitting != null)
{
this.UserReportSubmitting.Invoke();
}
}
#endregion
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 984a50ddbd1a62640808daa709e4fdfa
timeCreated: 1501037397
licenseType: Pro
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
/// <summary>
/// Represents a user reporting state.
/// </summary>
public enum UserReportingState
{
/// <summary>
/// Idle.
/// </summary>
Idle = 0,
/// <summary>
/// Creating bug report.
/// </summary>
CreatingUserReport = 1,
/// <summary>
/// Showing form.
/// </summary>
ShowingForm = 2,
/// <summary>
/// Submitting form.
/// </summary>
SubmittingForm = 3
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 65f8095a06be7a74786eb1ae2e1dcb61
timeCreated: 1501609378
licenseType: Pro
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using Unity.Cloud.UserReporting.Plugin;
using UnityEngine;
using UnityEngine.XR;
/// <summary>
/// Represents a behavior that collects XR information for user reports.
/// </summary>
/// <remarks>If you're using an older version of Unity and don't need XR support, feel free to delete this script.</remarks>
public class UserReportingXRExtensions : MonoBehaviour
{
#region Methods
private static bool XRIsPresent()
{
var xrDisplaySubsystems = new List<XRDisplaySubsystem>();
SubsystemManager.GetInstances<XRDisplaySubsystem>(xrDisplaySubsystems);
foreach (var xrDisplay in xrDisplaySubsystems)
{
if (xrDisplay.running)
{
return true;
}
}
return false;
}
private void Start()
{
if (XRIsPresent())
{
UnityUserReporting.CurrentClient.AddDeviceMetadata("XRDeviceModel", XRSettings.loadedDeviceName);
}
}
private void Update()
{
if (XRIsPresent())
{
int droppedFrameCount;
if (XRStats.TryGetDroppedFrameCount(out droppedFrameCount))
{
UnityUserReporting.CurrentClient.SampleMetric("XR.DroppedFrameCount", droppedFrameCount);
}
int framePresentCount;
if (XRStats.TryGetFramePresentCount(out framePresentCount))
{
UnityUserReporting.CurrentClient.SampleMetric("XR.FramePresentCount", framePresentCount);
}
float gpuTimeLastFrame;
if (XRStats.TryGetGPUTimeLastFrame(out gpuTimeLastFrame))
{
UnityUserReporting.CurrentClient.SampleMetric("XR.GPUTimeLastFrame", gpuTimeLastFrame);
}
}
}
#endregion
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 4f8ca9e9099f7fc43b1df7fce0284433
timeCreated: 1540254443
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: