forked from Shardstone/trail-into-darkness
Added a bunch of utilities and modfief the character data structue
This commit is contained in:
270
Packages/com.jovian.logger/Runtime/RemoteLogSender.cs
Normal file
270
Packages/com.jovian.logger/Runtime/RemoteLogSender.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.Logger {
|
||||
/// <summary>
|
||||
/// Sends log messages to the Custom Console editor window over TCP.
|
||||
/// Add this component to a GameObject in your scene (or create one at runtime)
|
||||
/// to enable remote logging from device builds.
|
||||
/// </summary>
|
||||
public class RemoteLogSender : MonoBehaviour {
|
||||
[Tooltip("IP address of the machine running the Unity Editor")]
|
||||
[SerializeField] private string editorHost = "127.0.0.1";
|
||||
[Tooltip("Port the Custom Console is listening on")]
|
||||
[SerializeField] private int editorPort = 9876;
|
||||
[Tooltip("Automatically connect on start")]
|
||||
[SerializeField] private bool autoConnect = true;
|
||||
[Tooltip("Retry connection every N seconds when disconnected")]
|
||||
[SerializeField] private float reconnectInterval = 5f;
|
||||
|
||||
private TcpClient client;
|
||||
private NetworkStream stream;
|
||||
private readonly ConcurrentQueue<string> sendQueue = new ConcurrentQueue<string>();
|
||||
private Thread sendThread;
|
||||
private volatile bool running;
|
||||
private volatile bool connected;
|
||||
private float reconnectTimer;
|
||||
|
||||
private static RemoteLogSender instance;
|
||||
|
||||
/// <summary>Whether the sender is currently connected to the editor.</summary>
|
||||
public bool IsConnected => connected;
|
||||
|
||||
/// <summary>The editor host address.</summary>
|
||||
public string EditorHost {
|
||||
get => editorHost;
|
||||
set => editorHost = value;
|
||||
}
|
||||
|
||||
/// <summary>The editor port.</summary>
|
||||
public int EditorPort {
|
||||
get => editorPort;
|
||||
set => editorPort = value;
|
||||
}
|
||||
|
||||
private void Awake() {
|
||||
if (instance != null && instance != this) {
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private void OnEnable() {
|
||||
Application.logMessageReceivedThreaded += OnUnityLogReceived;
|
||||
LoggerUtility.FormattedLogCallback += OnJovianLogReceived;
|
||||
LoggerUtility.WatchCallback += OnWatch;
|
||||
LoggerUtility.UnwatchCallback += OnUnwatch;
|
||||
|
||||
if (autoConnect) {
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable() {
|
||||
Application.logMessageReceivedThreaded -= OnUnityLogReceived;
|
||||
LoggerUtility.FormattedLogCallback -= OnJovianLogReceived;
|
||||
LoggerUtility.WatchCallback -= OnWatch;
|
||||
LoggerUtility.UnwatchCallback -= OnUnwatch;
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private void Update() {
|
||||
if (!connected && autoConnect) {
|
||||
reconnectTimer += Time.unscaledDeltaTime;
|
||||
if (reconnectTimer >= reconnectInterval) {
|
||||
reconnectTimer = 0f;
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Connect to the Custom Console editor.</summary>
|
||||
public void Connect() {
|
||||
if (connected) return;
|
||||
|
||||
try {
|
||||
client = new TcpClient();
|
||||
client.NoDelay = true;
|
||||
client.SendTimeout = 2000;
|
||||
var result = client.BeginConnect(editorHost, editorPort, null, null);
|
||||
bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(2));
|
||||
if (!success || !client.Connected) {
|
||||
client.Close();
|
||||
client = null;
|
||||
return;
|
||||
}
|
||||
client.EndConnect(result);
|
||||
stream = client.GetStream();
|
||||
connected = true;
|
||||
running = true;
|
||||
|
||||
sendThread = new Thread(SendLoop) {
|
||||
IsBackground = true,
|
||||
Name = "RemoteLogSender"
|
||||
};
|
||||
sendThread.Start();
|
||||
|
||||
// Send handshake
|
||||
EnqueueMessage("{\"handshake\":true,\"app\":\"" + Application.productName + "\",\"platform\":\"" + Application.platform + "\"}");
|
||||
Debug.Log($"[RemoteLog] Connected to editor at {editorHost}:{editorPort}");
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"[RemoteLog] Failed to connect: {e.Message}");
|
||||
CleanupConnection();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disconnect from the editor.</summary>
|
||||
public void Disconnect() {
|
||||
running = false;
|
||||
CleanupConnection();
|
||||
}
|
||||
|
||||
private void CleanupConnection() {
|
||||
connected = false;
|
||||
try { stream?.Close(); } catch { }
|
||||
try { client?.Close(); } catch { }
|
||||
stream = null;
|
||||
client = null;
|
||||
}
|
||||
|
||||
private void OnJovianLogReceived((JovianLogType logType, LogCategory logCategory, string message) log) {
|
||||
if (!connected) return;
|
||||
var json = BuildLogJson(log.message, "", (int)log.logType, (int)log.logCategory, true,
|
||||
DateTime.Now.ToString("HH:mm:ss.fff"), Time.frameCount);
|
||||
EnqueueMessage(json);
|
||||
}
|
||||
|
||||
private static readonly string[] LoggerPrefixes = {
|
||||
"INFO -> ", "ERROR -> ", "WARNING -> ",
|
||||
"EXCEPTION -> ", "ASSERT -> ", "SPAM -> "
|
||||
};
|
||||
|
||||
private static bool IsLoggerFormattedMessage(string condition) {
|
||||
foreach (var prefix in LoggerPrefixes) {
|
||||
if (condition.StartsWith(prefix, StringComparison.Ordinal)) return true;
|
||||
}
|
||||
if (condition.StartsWith("<color=#", StringComparison.Ordinal) && condition.Length > 22) {
|
||||
var afterTag = condition.AsSpan(15);
|
||||
foreach (var prefix in LoggerPrefixes) {
|
||||
if (afterTag.StartsWith(prefix.AsSpan(), StringComparison.Ordinal)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnUnityLogReceived(string condition, string stackTrace, UnityEngine.LogType type) {
|
||||
if (!connected) return;
|
||||
// Skip logger-formatted messages — those come via OnCustomLog
|
||||
if (IsLoggerFormattedMessage(condition)) return;
|
||||
|
||||
int loggerTyper = type switch {
|
||||
LogType.Error => (int)JovianLogType.Error,
|
||||
LogType.Assert => (int)JovianLogType.Assert,
|
||||
LogType.Warning => (int)JovianLogType.Warning,
|
||||
LogType.Exception => (int)JovianLogType.Exception,
|
||||
_ => (int)JovianLogType.Info,
|
||||
};
|
||||
|
||||
string message = condition;
|
||||
if (type is LogType.Error or LogType.Assert or LogType.Exception && !string.IsNullOrEmpty(stackTrace)) {
|
||||
message = $"{condition}\n{stackTrace}";
|
||||
}
|
||||
|
||||
var json = BuildLogJson(message, stackTrace ?? "", loggerTyper, (int)LogCategory.General, false,
|
||||
DateTime.Now.ToString("HH:mm:ss.fff"), Time.frameCount);
|
||||
EnqueueMessage(json);
|
||||
}
|
||||
|
||||
private void OnWatch((string key, string value, LogCategory category) watch) {
|
||||
if (!connected) return;
|
||||
var sb = new StringBuilder(128);
|
||||
sb.Append("{\"watch\":true,\"wk\":");
|
||||
AppendJsonString(sb, watch.key);
|
||||
sb.Append(",\"wv\":");
|
||||
AppendJsonString(sb, watch.value);
|
||||
sb.Append(",\"c\":").Append((int)watch.category);
|
||||
sb.Append(",\"ts\":");
|
||||
AppendJsonString(sb, DateTime.Now.ToString("HH:mm:ss.fff"));
|
||||
sb.Append(",\"fc\":").Append(Time.frameCount);
|
||||
sb.Append('}');
|
||||
EnqueueMessage(sb.ToString());
|
||||
}
|
||||
|
||||
private void OnUnwatch(string key) {
|
||||
if (!connected) return;
|
||||
var sb = new StringBuilder(64);
|
||||
sb.Append("{\"unwatch\":true,\"wk\":");
|
||||
AppendJsonString(sb, key);
|
||||
sb.Append('}');
|
||||
EnqueueMessage(sb.ToString());
|
||||
}
|
||||
|
||||
private static string BuildLogJson(string message, string stackTrace, int type, int category, bool isCustom, string timestamp, int frame) {
|
||||
// Manual JSON building to avoid allocations from JsonUtility
|
||||
var sb = new StringBuilder(256);
|
||||
sb.Append("{\"m\":");
|
||||
AppendJsonString(sb, message);
|
||||
sb.Append(",\"s\":");
|
||||
AppendJsonString(sb, stackTrace);
|
||||
sb.Append(",\"t\":").Append(type);
|
||||
sb.Append(",\"c\":").Append(category);
|
||||
sb.Append(",\"f\":").Append(isCustom ? "true" : "false");
|
||||
sb.Append(",\"ts\":");
|
||||
AppendJsonString(sb, timestamp);
|
||||
sb.Append(",\"fc\":").Append(frame);
|
||||
sb.Append('}');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void AppendJsonString(StringBuilder sb, string value) {
|
||||
if (value == null) { sb.Append("\"\""); return; }
|
||||
sb.Append('"');
|
||||
foreach (char c in value) {
|
||||
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: sb.Append(c); break;
|
||||
}
|
||||
}
|
||||
sb.Append('"');
|
||||
}
|
||||
|
||||
private void EnqueueMessage(string json) {
|
||||
// Cap queue size to prevent memory issues if sending is slow
|
||||
if (sendQueue.Count < 10000) {
|
||||
sendQueue.Enqueue(json);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendLoop() {
|
||||
while (running) {
|
||||
try {
|
||||
if (sendQueue.TryDequeue(out string json)) {
|
||||
byte[] data = Encoding.UTF8.GetBytes(json + "\n");
|
||||
stream.Write(data, 0, data.Length);
|
||||
} else {
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
} catch (Exception) {
|
||||
running = false;
|
||||
connected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy() {
|
||||
if (instance == this) instance = null;
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user