Added a bunch of utilities and modfief the character data structue

This commit is contained in:
Sebastian Bularca
2026-03-29 18:31:03 +02:00
parent 4a9c00212a
commit ee97b2fec3
110 changed files with 6752 additions and 169 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fd8ce1f3fd630a147b25c4578865941e

View File

@@ -0,0 +1,18 @@
{
"name": "Jovian.LoggerEditor",
"rootNamespace": "",
"references": [
"GUID:9e11523c9d4d45445a0938098559d830"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 206337e0b2cdd1b448d7d752a7ca77e8
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,67 @@
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace Jovian.ProjectSettings {
/// <summary>
/// Reads and writes settings to ProjectSettings/CustomLoggerSettings.json.
/// Flat key-value store. Use prefixed keys by convention (e.g. "logger.myKey").
/// </summary>
internal static class JovianProjectSettings {
private const string FileName = "CustomLoggerSettings.json";
private static string FilePath {
get {
var fullName = Directory.GetParent(Application.dataPath)?.FullName;
return Path.Combine(fullName ?? Application.dataPath, "ProjectSettings", FileName);
}
}
internal static T Get<T>(string packagePrefix, string setting, T defaultValue) {
var root = LoadRoot();
var key = $"{packagePrefix}.{setting}";
if (root.TryGetValue(key, out var token)) {
try {
return token.ToObject<T>();
} catch {
return defaultValue;
}
}
return defaultValue;
}
internal static void Set<T>(string packagePrefix, string setting, T value) {
var root = LoadRoot();
var key = $"{packagePrefix}.{setting}";
root[key] = value != null ? JToken.FromObject(value) : JValue.CreateNull();
SaveRoot(root);
}
private static JObject LoadRoot() {
string path = FilePath;
if (!File.Exists(path)) {
return new JObject();
}
try {
string json = File.ReadAllText(path);
return JObject.Parse(json);
} catch {
return new JObject();
}
}
private static void SaveRoot(JObject root) {
string path = FilePath;
string dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) {
Directory.CreateDirectory(dir);
}
string json = root.ToString(Formatting.Indented);
File.WriteAllText(path, json);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eff2fe1736b6efc40b7e52153c3b1010

View File

@@ -0,0 +1,277 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Newtonsoft.Json;
namespace Jovian.Logger {
[CustomEditor(typeof(LoggerSettings))]
internal class LoggerSettingsEditor : Editor {
private static SerializedObject LoggerSettingsObj;
private SerializedProperty enableGlobalLoggingProp;
private SerializedProperty uploadBaseUrlProp;
private SerializedProperty minimumTimeBetweenUploadsProp;
private SerializedProperty loggerColorsProp;
// For loggerColors sub-fields
private SerializedProperty infoColorProp;
private SerializedProperty warningColorProp;
private SerializedProperty errorColorProp;
private SerializedProperty assertColorProp;
private SerializedProperty exceptionColorProp;
private SerializedProperty spamColorProp;
private static Editor editor;
private bool showLocalFilters = true;
private bool showAllFilters = true;
private bool showGlobalFilters = true;
private string globalCallerNames = "";
private string localCallerNames = "";
private bool hasGlobalSavedFilter = true;
private bool hasLocalSavedFilter = true;
private class ListOfFilters {
public List<Filters> filters;
}
private void LoadLocalFilters() {
var loggerSettings = (LoggerSettings)target;
var filters = EditorPrefs.GetString("LoggerFilters");
if(UnityIsHeadless()) {
filters = null;
}
if(!string.IsNullOrEmpty(filters)) {
loggerSettings.LocalFilters = JsonConvert.DeserializeObject<ListOfFilters>(filters).filters;
}
if(loggerSettings.LocalFilters == null) {
loggerSettings.LocalFilters = new List<Filters>(0);
}
}
private bool UnityIsHeadless() {
if(Environment.CommandLine.Contains("-batchmode")) {
return true;
}
return false;
}
private void OnEnable() {
enableGlobalLoggingProp = serializedObject.FindProperty("enableGlobalLogging");
uploadBaseUrlProp = serializedObject.FindProperty("uploadBaseUrl");
minimumTimeBetweenUploadsProp = serializedObject.FindProperty("minimumTimeBetweenUploads");
loggerColorsProp = serializedObject.FindProperty("loggerColors");
infoColorProp = loggerColorsProp.FindPropertyRelative("infoColor");
warningColorProp = loggerColorsProp.FindPropertyRelative("warningColor");
errorColorProp = loggerColorsProp.FindPropertyRelative("errorColor");
assertColorProp = loggerColorsProp.FindPropertyRelative("assertColor");
exceptionColorProp = loggerColorsProp.FindPropertyRelative("exceptionColor");
spamColorProp = loggerColorsProp.FindPropertyRelative("spamColor");
LoadLocalFilters();
}
public static void ShowSettings() {
var loggerSettings = new AssetSettingsLoader<LoggerSettings>().GetSettings(LoggerSettingsProvider.SETTINGS_FILE);
LoggerSettingsObj = new SerializedObject(loggerSettings);
LoggerSettingsObj.Update();
editor ??= CreateEditor(loggerSettings);
editor.OnInspectorGUI();
}
public override void OnInspectorGUI() {
serializedObject.Update();
var loggerSettings = (LoggerSettings)target;
EditorGUILayout.Space(15);
EditorGUILayout.LabelField("Custom Logger Settings", EditorStyles.whiteLargeLabel);
EditorGUILayout.Space(15);
EditorGUILayout.HelpBox("This will enable/disable logging for the entire project. It has a code level method as well.", MessageType.Warning);
EditorGUILayout.Space(3);
EditorGUILayout.PropertyField(enableGlobalLoggingProp, new GUIContent("Enable Global Logging"));
EditorGUILayout.Space(15);
EditorGUILayout.LabelField("Log Message Colors", EditorStyles.whiteLargeLabel);
EditorGUILayout.Space(3);
EditorGUILayout.PropertyField(infoColorProp, new GUIContent("Info Color"));
EditorGUILayout.PropertyField(warningColorProp, new GUIContent("Warning Color"));
EditorGUILayout.PropertyField(errorColorProp, new GUIContent("Error Color"));
EditorGUILayout.PropertyField(assertColorProp, new GUIContent("Assert Color"));
EditorGUILayout.PropertyField(exceptionColorProp, new GUIContent("Exception Color"));
EditorGUILayout.PropertyField(spamColorProp, new GUIContent("Spam Color"));
EditorGUILayout.Space(10);
if(GUILayout.Button("Reset To Default Colors")) {
loggerSettings.ResetColorsToDefault();
EditorUtility.SetDirty(loggerSettings);
}
EditorGUILayout.Space(20);
EditorGUILayout.LabelField("Log Uploader Settings", EditorStyles.whiteLargeLabel);
EditorGUILayout.Space(3);
EditorGUILayout.PropertyField(uploadBaseUrlProp, new GUIContent("Upload Base URL"));
EditorGUILayout.PropertyField(minimumTimeBetweenUploadsProp, new GUIContent("Min Time Between Uploads (sec)"));
EditorGUILayout.Space(30);
showAllFilters = EditorGUILayout.Foldout(showAllFilters, "Logger Filters");
if(showAllFilters) {
EditorGUILayout.Space(20);
EditorGUILayout.LabelField("Global Project Level Filters", EditorStyles.whiteLargeLabel);
EditorGUILayout.Space(3);
EditorGUILayout.HelpBox("Global filters will be saved with the asset and, if commited, applied to everyone using the project", MessageType.Warning);
EditorGUILayout.Space(3);
showGlobalFilters = EditorGUILayout.Foldout(showGlobalFilters, "Active Filters");
if(showGlobalFilters) {
for(int i = 0; i < loggerSettings.globalFilters.Length; i++) {
EditorGUILayout.BeginVertical("box");
loggerSettings.globalFilters[i].logCategory = (LogCategory)EditorGUILayout.EnumFlagsField("Log Category", loggerSettings.globalFilters[i].logCategory);
loggerSettings.globalFilters[i].jovianLogType = (JovianLogType)EditorGUILayout.EnumPopup("Log Type", loggerSettings.globalFilters[i].jovianLogType);
EditorGUILayout.BeginHorizontal();
globalCallerNames = GetCallersAsString(loggerSettings.globalFilters[i].callerNames);
globalCallerNames = EditorGUILayout.TextField("Caller Name", globalCallerNames);
loggerSettings.globalFilters[i].callerListingType = (CallerListingType)EditorGUILayout.EnumPopup("", loggerSettings.globalFilters[i].callerListingType, GUILayout.Width(120));
EditorGUILayout.EndHorizontal();
loggerSettings.globalFilters[i].callerNames = globalCallerNames.Split(",").ToList();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if(GUILayout.Button("Remove This Filter")) {
var temp = loggerSettings.globalFilters.ToList();
temp.RemoveAt(i);
loggerSettings.globalFilters = temp.ToArray();
EditorUtility.SetDirty(loggerSettings);
AssetDatabase.SaveAssets();
EditorUtility.RequestScriptReload();
}
if(GUILayout.Button("Save Changes")) {
hasGlobalSavedFilter = true;
EditorUtility.SetDirty(loggerSettings);
AssetDatabase.SaveAssets();
EditorUtility.RequestScriptReload();
}
EditorGUILayout.EndHorizontal();
if (!hasGlobalSavedFilter) {
GUILayout.Space(10);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
EditorGUILayout.HelpBox("Filter Not Saved...", MessageType.Error);
EditorGUILayout.EndHorizontal();
GUILayout.Space(5);
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if(GUILayout.Button("Add Global Filter")) {
var temp = loggerSettings.globalFilters.ToList();
temp.Add(new Filters());
loggerSettings.globalFilters = temp.ToArray();
hasGlobalSavedFilter = false;
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space(20);
EditorGUILayout.LabelField("Local Editor Level Filters (Not Serialized)", EditorStyles.whiteLargeLabel);
EditorGUILayout.Space(3);
EditorGUILayout.HelpBox("Local filters are local and will not be saved with the asset but rather with the editor settings. Use these to set personal filters.", MessageType.Info);
EditorGUILayout.Space(3);
showLocalFilters = EditorGUILayout.Foldout(showLocalFilters, "Active Filters");
if(showLocalFilters) {
for(int i = 0; i < loggerSettings.LocalFilters.Count; i++) {
EditorGUILayout.BeginVertical("box");
EditorGUI.BeginChangeCheck();
loggerSettings.LocalFilters[i].logCategory = (LogCategory)EditorGUILayout.EnumFlagsField("Log Category", loggerSettings.LocalFilters[i].logCategory);
loggerSettings.LocalFilters[i].jovianLogType = (JovianLogType)EditorGUILayout.EnumPopup("Log Type", loggerSettings.LocalFilters[i].jovianLogType);
EditorGUILayout.BeginHorizontal();
localCallerNames = GetCallersAsString(loggerSettings.LocalFilters[i].callerNames);
localCallerNames = EditorGUILayout.TextField("Caller Name", localCallerNames, GUILayout.MinWidth(400));
loggerSettings.LocalFilters[i].callerListingType = (CallerListingType)EditorGUILayout.EnumPopup("", loggerSettings.LocalFilters[i].callerListingType, GUILayout.Width(120));
EditorGUILayout.EndHorizontal();
loggerSettings.LocalFilters[i].callerNames = localCallerNames.Split(",").ToList();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if(GUILayout.Button("Remove This Filter")) {
var temp = new List<Filters>(loggerSettings.LocalFilters);
temp.RemoveAt(i);
loggerSettings.LocalFilters = temp;
var filters = JsonConvert.SerializeObject(new ListOfFilters() { filters = loggerSettings.LocalFilters });
EditorPrefs.SetString("LoggerFilters", filters);
EditorUtility.RequestScriptReload();
}
if(GUILayout.Button("Save Changes")) {
var filters = JsonConvert.SerializeObject(new ListOfFilters() { filters = loggerSettings.LocalFilters });
EditorPrefs.SetString("LoggerFilters", filters);
EditorUtility.RequestScriptReload();
}
EditorGUILayout.EndHorizontal();
if (!hasLocalSavedFilter) {
GUILayout.Space(10);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
EditorGUILayout.HelpBox("Filter Not Saved...", MessageType.Error);
EditorGUILayout.EndHorizontal();
GUILayout.Space(5);
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if(GUILayout.Button("Add Local Filter")) {
loggerSettings.LocalFilters.Add(new Filters());
hasLocalSavedFilter = false;
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
}
serializedObject.ApplyModifiedProperties();
}
private string GetCallersAsString(List<string> callerNames) {
if(callerNames == null || callerNames.Count == 0) {
return "";
}
string result = "";
for(int i = 0; i < callerNames.Count; i++) {
result += callerNames[i];
if(i != callerNames.Count - 1) {
result += ",";
}
}
return result;
}
private void OnDisable() {
AssetDatabase.SaveAssets();
}
private void OnDestroy() {
AssetDatabase.SaveAssets();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9e97c6cb58b8e0f4f990c51f841842af

View File

@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace Jovian.Logger {
internal sealed class LoggerSettingsProvider : SettingsProvider {
public LoggerSettingsProvider(string path, SettingsScope scopes, IEnumerable<string> keywords = null) : base(path, scopes, keywords) { }
public const string SETTINGS_FILE = "Assets/Settings/Resources/logger-settings.asset";
[SettingsProvider]
public static SettingsProvider CreateSettingsProvider() {
var provider = new SettingsProvider("Project/Jovian/Logger", SettingsScope.Project) {
guiHandler = (searchContext) => { LoggerSettingsEditor.ShowSettings(); }
};
return provider;
}
private class ConfigLoader : AssetPostprocessor {
private static AssetSettingsLoader<LoggerSettings> loader;
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
loader ??= new AssetSettingsLoader<LoggerSettings>();
loader.GetSettings(SETTINGS_FILE);
}
}
}
public class AssetSettingsLoader<T> where T : ScriptableObject {
private T settings;
/// <summary>
/// Get the settings file if cached/existent. If it doesn't exist, it will create one.
/// </summary>
/// <param name="settingsFilePath"></param>
/// <returns></returns>
public T GetSettings(string settingsFilePath) {
return settings ?? SetSettings(settingsFilePath);
}
private T SetSettings(string settingsFilePath) {
if(!Directory.Exists(Path.GetDirectoryName(settingsFilePath))) {
Directory.CreateDirectory(Path.GetDirectoryName(settingsFilePath));
}
settings = AssetDatabase.LoadAssetAtPath<T>(settingsFilePath);
if(settings != null) {
return settings;
}
settings = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(settings, settingsFilePath);
AssetDatabase.SaveAssets();
settings = AssetDatabase.LoadAssetAtPath<T>(settingsFilePath);
return settings;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 360fda9a575ab7b4eaf523256ac39663

View File

@@ -0,0 +1,245 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEditor;
using UnityEngine;
namespace Jovian.Logger {
/// <summary>
/// TCP server that receives log messages from RemoteLogSender on device builds.
/// Managed by Custom Console — not intended for standalone use.
/// </summary>
internal static class RemoteLogReceiver {
public const int DefaultPort = 9876;
public struct RemoteLogEntry {
public string message;
public string stackTrace;
public string timestamp;
public int frameCount;
public JovianLogType type;
public LogCategory logCategory;
public bool isCustomLog;
}
public struct RemoteWatchEntry {
public string key;
public string value;
public LogCategory logCategory;
public string timestamp;
public int frameCount;
}
public static event Action<RemoteLogEntry> OnRemoteLog;
public static event Action<RemoteWatchEntry> OnRemoteWatch;
public static event Action<string> OnRemoteUnwatch;
public static event Action<string> OnClientConnected;
public static event Action OnClientDisconnected;
private static TcpListener listener;
private static Thread listenThread;
private static volatile bool running;
private static int port = DefaultPort;
public static bool IsRunning => running;
public static int Port => port;
public static string ConnectedClientName { get; private set; } = "";
public static bool HasClient { get; private set; }
private static readonly ConcurrentQueue<Action> mainThreadQueue = new ConcurrentQueue<Action>();
public static void Start(int listenPort = DefaultPort) {
if (running) return;
port = listenPort;
try {
listener = new TcpListener(IPAddress.Any, port);
listener.Start();
running = true;
listenThread = new Thread(ListenLoop) {
IsBackground = true,
Name = "RemoteLogReceiver"
};
listenThread.Start();
EditorApplication.update += ProcessMainThreadQueue;
Debug.Log($"[RemoteLog] Listening on port {port}");
} catch (Exception e) {
Debug.LogError($"[RemoteLog] Failed to start listener: {e.Message}");
running = false;
}
}
public static void Stop() {
running = false;
HasClient = false;
ConnectedClientName = "";
try { listener?.Stop(); } catch { }
listener = null;
EditorApplication.update -= ProcessMainThreadQueue;
}
private static void ProcessMainThreadQueue() {
while (mainThreadQueue.TryDequeue(out var action)) {
action?.Invoke();
}
}
private static void ListenLoop() {
while (running) {
try {
if (!listener.Pending()) {
Thread.Sleep(100);
continue;
}
var client = listener.AcceptTcpClient();
client.NoDelay = true;
client.ReceiveTimeout = 0;
HandleClient(client);
} catch (SocketException) {
if (!running) break;
} catch (ObjectDisposedException) {
break;
}
}
}
private static void HandleClient(TcpClient client) {
var thread = new Thread(() => ClientReadLoop(client)) {
IsBackground = true,
Name = "RemoteLogClient"
};
thread.Start();
}
private static void ClientReadLoop(TcpClient client) {
try {
using var stream = client.GetStream();
using var reader = new StreamReader(stream, Encoding.UTF8);
HasClient = true;
while (running && client.Connected) {
string line = reader.ReadLine();
if (line == null) break;
if (string.IsNullOrWhiteSpace(line)) continue;
ParseAndDispatch(line);
}
} catch (IOException) {
// Connection lost
} catch (ObjectDisposedException) {
// Shutting down
} finally {
try { client.Close(); } catch { }
HasClient = false;
ConnectedClientName = "";
mainThreadQueue.Enqueue(() => OnClientDisconnected?.Invoke());
}
}
private static void ParseAndDispatch(string json) {
try {
// Check for handshake
if (json.Contains("\"handshake\"")) {
string appName = ExtractJsonString(json, "app");
string platform = ExtractJsonString(json, "platform");
ConnectedClientName = $"{appName} ({platform})";
mainThreadQueue.Enqueue(() => OnClientConnected?.Invoke(ConnectedClientName));
return;
}
// Check for watch
if (json.Contains("\"watch\"")) {
var watchEntry = new RemoteWatchEntry {
key = ExtractJsonString(json, "wk"),
value = ExtractJsonString(json, "wv"),
logCategory = (LogCategory)ExtractJsonInt(json, "c"),
timestamp = ExtractJsonString(json, "ts"),
frameCount = ExtractJsonInt(json, "fc"),
};
mainThreadQueue.Enqueue(() => OnRemoteWatch?.Invoke(watchEntry));
return;
}
// Check for unwatch
if (json.Contains("\"unwatch\"")) {
string key = ExtractJsonString(json, "wk");
mainThreadQueue.Enqueue(() => OnRemoteUnwatch?.Invoke(key));
return;
}
var entry = new RemoteLogEntry {
message = ExtractJsonString(json, "m"),
stackTrace = ExtractJsonString(json, "s"),
timestamp = ExtractJsonString(json, "ts"),
frameCount = ExtractJsonInt(json, "fc"),
type = (JovianLogType)ExtractJsonInt(json, "t"),
logCategory = (LogCategory)ExtractJsonInt(json, "c"),
isCustomLog = ExtractJsonBool(json, "f"),
};
mainThreadQueue.Enqueue(() => OnRemoteLog?.Invoke(entry));
} catch (Exception e) {
Debug.LogWarning($"[RemoteLog] Failed to parse: {e.Message}");
}
}
// Lightweight JSON field extractors (avoids dependency on full JSON parser for simple protocol)
private static string ExtractJsonString(string json, string key) {
string pattern = "\"" + key + "\":\"";
int start = json.IndexOf(pattern, StringComparison.Ordinal);
if (start < 0) return "";
start += pattern.Length;
var sb = new StringBuilder();
for (int i = start; i < json.Length; i++) {
char c = json[i];
if (c == '\\' && i + 1 < json.Length) {
char next = json[i + 1];
switch (next) {
case '"': sb.Append('"'); i++; break;
case '\\': sb.Append('\\'); i++; break;
case 'n': sb.Append('\n'); i++; break;
case 'r': sb.Append('\r'); i++; break;
case 't': sb.Append('\t'); i++; break;
default: sb.Append(c); break;
}
} else if (c == '"') {
break;
} else {
sb.Append(c);
}
}
return sb.ToString();
}
private static int ExtractJsonInt(string json, string key) {
string pattern = "\"" + key + "\":";
int start = json.IndexOf(pattern, StringComparison.Ordinal);
if (start < 0) return 0;
start += pattern.Length;
int end = start;
while (end < json.Length && (char.IsDigit(json[end]) || json[end] == '-')) end++;
if (end == start) return 0;
return int.TryParse(json.AsSpan(start, end - start), out int val) ? val : 0;
}
private static bool ExtractJsonBool(string json, string key) {
string pattern = "\"" + key + "\":";
int start = json.IndexOf(pattern, StringComparison.Ordinal);
if (start < 0) return false;
start += pattern.Length;
return start < json.Length && json[start] == 't';
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 13353efce044337428c052b7e37f0445