using System; using System.Collections.Generic; using System.Text; using UnityEngine; namespace BugReporter { /// Everything a single report carries. Serialized by hand — Unity's JsonUtility can't do dictionaries. internal sealed class ReportPayload { public string title; public string description; public string severity; public string buildVersion; public string game; public string session; public string scene; public string logs; public byte[] clip; // packed clip frames (uploaded as a binary part, not in the JSON) public int clipFps; // capture rate, so the dashboard plays the clip at real speed public Dictionary metadata; public DeviceInfo device; /// The JSON part of the multipart upload. The screenshot and logs travel as separate parts. public string ToJson() { var sb = new StringBuilder(512); sb.Append('{'); Field(sb, "title", title); sb.Append(','); Field(sb, "description", description); sb.Append(','); Field(sb, "severity", severity); sb.Append(','); Field(sb, "buildVersion", buildVersion); sb.Append(','); Field(sb, "game", game); sb.Append(','); Field(sb, "session", session); sb.Append(','); Field(sb, "scene", scene); sb.Append(','); Field(sb, "platform", device.platform); sb.Append(','); Field(sb, "deviceModel", device.deviceModel); sb.Append(','); Field(sb, "osVersion", device.osVersion); sb.Append(','); Field(sb, "screenResolution", device.screenResolution); sb.Append(','); sb.Append("\"memoryMB\":").Append(device.memoryMB).Append(','); sb.Append("\"clipFps\":").Append(clipFps).Append(','); sb.Append("\"metadata\":").Append(MetadataJson()); sb.Append('}'); return sb.ToString(); } private string MetadataJson() { if (metadata == null || metadata.Count == 0) return "{}"; var sb = new StringBuilder(128); sb.Append('{'); bool first = true; foreach (var kv in metadata) { if (!first) sb.Append(','); first = false; Escape(sb, kv.Key); sb.Append(':'); AppendValue(sb, kv.Value); } sb.Append('}'); return sb.ToString(); } private static void AppendValue(StringBuilder sb, object v) { switch (v) { case null: sb.Append("null"); break; case bool b: sb.Append(b ? "true" : "false"); break; // Invariant culture: a locale with ',' as the decimal separator would emit invalid JSON. case float f: sb.Append(f.ToString("R", System.Globalization.CultureInfo.InvariantCulture)); break; case double d: sb.Append(d.ToString("R", System.Globalization.CultureInfo.InvariantCulture)); break; case int i: sb.Append(i.ToString(System.Globalization.CultureInfo.InvariantCulture)); break; case long l: sb.Append(l.ToString(System.Globalization.CultureInfo.InvariantCulture)); break; default: Escape(sb, v.ToString()); break; } } private static void Field(StringBuilder sb, string key, string value) { Escape(sb, key); sb.Append(':'); Escape(sb, value ?? string.Empty); } private static void Escape(StringBuilder sb, string s) { sb.Append('"'); foreach (char c in s) { switch (c) { case '"': sb.Append("\\\""); break; case '\\': sb.Append("\\\\"); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; default: if (c < 0x20) sb.Append("\\u").Append(((int)c).ToString("x4")); else sb.Append(c); break; } } sb.Append('"'); } } [Serializable] internal struct DeviceInfo { public string platform; public string deviceModel; public string osVersion; public string screenResolution; public int memoryMB; public static DeviceInfo Capture() => new DeviceInfo { platform = Application.platform.ToString(), deviceModel = SystemInfo.deviceModel, osVersion = SystemInfo.operatingSystem, screenResolution = $"{Screen.width}x{Screen.height}", memoryMB = SystemInfo.systemMemorySize, }; } }