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,
};
}
}