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

View File

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

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

View File

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

View File

@@ -0,0 +1,181 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Jovian.Logger {
/// <summary>
/// Default logger for Custom Logger in case you don't want to create a new instance of CustomLogger
/// Recommended is to create a new instance of CustomLogger
/// </summary>
public static class GlobalLogger {
/// <summary>
/// Logs messages that are meant to only be seen in the editor
/// </summary>
/// <param name="msg">Log message</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogSpam(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogSpam($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs messages that are meant to only be seen in the editor with a context object
/// </summary>
/// <param name="msg">Log message</param>
/// <param name="context">Object to select in scene/project</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogSpam(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogSpam($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Info/Debug messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogInfo(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogInfo($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs standard Info/Debug messagesc with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="context">Object to select in scene/project</param>
/// <param name="logCat"></param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogInfo(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogInfo($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Warning messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogWarning(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogWarning($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs standard Warning messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="context">Object to select in scene/project</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogWarning(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogWarning($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Error messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogError(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogError($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs standard Error messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="context">Object to select in scene/project</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogError(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogError($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Assert messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogAssert(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogAssert($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs standard Assert messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="context">Object to select in scene/project</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogAssert(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogAssert($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs string Exception messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogException(string msg, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogException($"{GetCaller()?.Name}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs string Exception messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="context">Object to select in scene/project</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogException(string msg, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogException($"{GetCaller()?.Name}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Exception messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogException(Exception e, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogException($"{GetCaller()?.Name}.{callerMethod}", e.Message, logCat);
}
/// <summary>
/// Logs standard Exception messages with a context object
/// </summary>
/// <param name="e"></param>
/// <param name="context">Object to select in scene/project</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogException(Exception e, UnityEngine.Object context, LogCategory logCat, [CallerMemberName] string callerMethod = "") {
InternalLogger.LogException($"{GetCaller()?.Name}.{callerMethod}", e.Message, logCat, context);
}
private static Type GetCaller() {
var stackTrace = new StackTrace();
var stackFrame = stackTrace.GetFrame(2);
var caller = stackFrame?.GetMethod()?.DeclaringType;
return caller;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 351a08a96ffb9654b94e50b31c1058ba

View File

@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace Jovian.Logger {
internal class LoggerSettingsData {
public bool enableGlobalLogging = true;
public Filters[] globalFilters = Array.Empty<Filters>();
public LoggerColors loggerColors = new();
public bool isLoaded = false;
#if UNITY_EDITOR
public List<Filters> LocalFilters { get; set; } = new();
#endif
}
internal static class InternalLogger {
private static readonly LoggerSettingsData loggerSettingsData = new();
private static bool enableGlobalLogging = true;
internal static bool IsMainThread => LoggerUtility.IsMainThread;
internal static void LoadSettings() {
var loggerSettings = LoggerUtility.LoadCustomLoggerSettings();
if(!loggerSettings) {
return;
}
var colors = loggerSettings.loggerColors;
loggerSettingsData.globalFilters = loggerSettings.globalFilters;
enableGlobalLogging = loggerSettings.enableGlobalLogging;
loggerSettingsData.loggerColors.infoColor = colors.infoColor;
loggerSettingsData.loggerColors.warningColor = colors.warningColor;
loggerSettingsData.loggerColors.errorColor = colors.errorColor;
loggerSettingsData.loggerColors.assertColor = colors.assertColor;
loggerSettingsData.loggerColors.exceptionColor = colors.exceptionColor;
loggerSettingsData.loggerColors.spamColor = colors.spamColor;
#if UNITY_EDITOR
if(!Environment.CommandLine.Contains("-batchmode")) {
loggerSettingsData.LocalFilters = loggerSettings.LocalFilters;
} else {
loggerSettings.LocalFilters.Clear();
}
Debug.Log("[CustomLogger] Local Filters: " + loggerSettingsData.LocalFilters.Count);
#endif
loggerSettingsData.isLoaded = true;
Debug.Log("[CustomLogger] Global Filters: " + loggerSettingsData.globalFilters.Length);
Debug.Log("[CustomLogger] Settings loaded");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void LogInternal(JovianLogType jovianLogType, LogCategory logcat, string classType, string msg, Color color, string args = "", UnityEngine.Object reference = null) {
if(!enableGlobalLogging) {
return;
}
if(!loggerSettingsData.isLoaded && IsMainThread) {
LoadSettings();
}
// Check if any filters are present and apply filters
foreach(var filter in loggerSettingsData.globalFilters) {
if(filter == null) {
continue;
}
if((filter.jovianLogType < jovianLogType || !filter.logCategory.HasFlag(logcat)) || !FilterCaller(filter.callerNames, filter.callerListingType)) {
return;
}
}
#if UNITY_EDITOR
if(!Environment.CommandLine.Contains("-batchmode")) {
foreach(var filter in loggerSettingsData.LocalFilters) {
if(filter == null) {
continue;
}
if((filter.jovianLogType < jovianLogType || !filter.logCategory.HasFlag(logcat)) || !FilterCaller(filter.callerNames, filter.callerListingType)) {
return;
}
}
} else {
if(jovianLogType == JovianLogType.Spam) {
return;
}
}
#endif
StringBuilder sb = new(500);
switch(jovianLogType) {
case JovianLogType.Spam:
sb.Append("SPAM -> ");
break;
case JovianLogType.Info:
sb.Append("INFO -> ");
break;
case JovianLogType.Warning:
sb.Append("WARNING -> ");
break;
case JovianLogType.Error:
sb.Append("ERROR -> ");
break;
case JovianLogType.Assert:
sb.Append("ASSERT -> ");
break;
case JovianLogType.Exception:
sb.Append("EXCEPTION -> ");
break;
default:
return;
}
var isFrameCountEnabled = LoggerUtility.IsFrameCountEnabled;
// As exceptions can be reported to Unity Exception tracking, we do not want frame numbers in the message. It will prevent Unity from grouping reports
var logTypeShouldIncludeFrameCount = jovianLogType != JovianLogType.Exception;
if(isFrameCountEnabled && logTypeShouldIncludeFrameCount) {
sb.Append("F:");
sb.Append(LoggerUtility.FrameCount);
sb.Append(" |");
}
sb.Append(" [");
sb.Append(logcat);
sb.Append("] ");
sb.Append("[");
sb.Append(classType);
sb.Append("] ");
sb.Append(msg);
var message = sb.ToString();
LoggerUtility.FormattedLogCallback?.Invoke((jovianLogType, logcat, message));
#if UNITY_EDITOR
//remove the color when not in the editor to avoid cluttering the log files
if(!Environment.CommandLine.Contains("-batchmode") && IsMainThread) {
message = $"<color=#{ColorUtility.ToHtmlStringRGB(color)}>{message}</color>";
}
#endif
Debug.unityLogger.Log(LoggerUtility.GetLogType(jovianLogType), (object)message, reference);
return;
bool FilterCaller(List<string> filterCallerNames, CallerListingType filterCallerListingType) {
foreach(var caller in filterCallerNames) {
if(!string.IsNullOrEmpty(caller)) {
switch(filterCallerListingType) {
case CallerListingType.Blacklist_Caller:
return !classType.Contains(caller);
case CallerListingType.Whitelist_Caller:
return classType.Contains(caller);
}
}
}
return true;
}
}
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void LogSpam(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
LogInternal(JovianLogType.Spam, logcat, caller, msg, loggerSettingsData.loggerColors.spamColor, "spam", reference);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void LogInfo(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
LogInternal(JovianLogType.Info, logcat, caller, msg, loggerSettingsData.loggerColors.infoColor, "", reference);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void LogWarning(string classType, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
LogInternal(JovianLogType.Warning, logcat, classType, msg, loggerSettingsData.loggerColors.warningColor, "", reference);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void LogError(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
LogInternal(JovianLogType.Error, logcat, caller, msg, loggerSettingsData.loggerColors.errorColor, "", reference);
}
internal static void LogException(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
LogInternal(JovianLogType.Exception, logcat, caller, msg, loggerSettingsData.loggerColors.exceptionColor, "", reference);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void LogAssert(string caller, string msg, LogCategory logcat, UnityEngine.Object reference = null) {
LogInternal(JovianLogType.Assert, logcat, caller, msg, loggerSettingsData.loggerColors.assertColor, "", reference);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1463245ed08381f4688ab74cc4296ba1

View File

@@ -0,0 +1,14 @@
{
"name": "Jovian.Logger",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

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

View File

@@ -0,0 +1,36 @@
using System;
namespace Jovian.Logger {
[Flags]
public enum LogCategory {
None = 0,
General = 1,
Editor = 2,
Core = 4,
GameLogic = 8,
UI = 16,
Input = 32,
Network = 64,
Analytics = 128,
Audio = 256,
Graphics = 512,
Physics = 1024,
AI = 2048,
Internal = 4096,
Testing = 8192
}
public enum CallerListingType {
Blacklist_Caller,
Whitelist_Caller
}
public enum JovianLogType {
Exception,
Assert,
Error,
Warning,
Info,
Spam
}
}

View File

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

View File

@@ -0,0 +1,280 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Jovian.Logger {
/// <summary>
/// The recommended way to log messages in Jovian Logger.
/// Create a new instance of this struct in the class you want to log messages.
/// </summary>
public struct Logger {
private string caller;
private LogCategory logCategory;
public Logger(Type caller, LogCategory logCategory) : this() {
this.logCategory = logCategory;
this.caller = caller.Name;
LoggerUtility.PreloadLoggerSettings();
}
private string Caller {
get {
if(string.IsNullOrEmpty(caller)) {
caller = "Unspecified";
}
return caller;
}
}
private LogCategory LogCategory {
get {
if(logCategory == LogCategory.None) {
logCategory = LogCategory.General;
}
return logCategory;
}
}
/// <summary>
/// Logs messages that are meant to only be seen in the editor
/// </summary>
/// <param name="msg">Log message</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogSpam(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogSpam($"{Caller}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs messages that are meant to only be seen in the editor with a context object
/// </summary>
/// <param name="msg">Log message</param>
/// <param name="context">Object to focus on in scene/project window</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[Conditional("UNITY_EDITOR")] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogSpam(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogSpam($"{Caller}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Info/Debug messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogInfo(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogInfo($"{Caller}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs standard Info/Debug messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="context">Object to focus on in scene/project window</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogInfo(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogInfo($"{Caller}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Warning messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogWarning(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogWarning($"{Caller}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs standard Warning messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="context">Object to focus on in scene/project window</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogWarning(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogWarning($"{Caller}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Error messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogError(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogError($"{Caller}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs standard Error messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="context">Object to focus on in scene/project window</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogError(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogError($"{Caller}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Assert messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogAssert(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogAssert($"{Caller}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs standard Assert messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="context">Object to focus on in scene/project window</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogAssert(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogAssert($"{Caller}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs string Exception messages
/// </summary>
/// <param name="msg"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogException(string msg, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogException($"{Caller}.{callerMethod}", msg, logCat);
}
/// <summary>
/// Logs string Exception messages with a context object
/// </summary>
/// <param name="msg"></param>
/// <param name="context">Object to focus on in scene/project window</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogException(string msg, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogException($"{Caller}.{callerMethod}", msg, logCat, context);
}
/// <summary>
/// Logs standard Exception messages
/// </summary>
/// <param name="e"></param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogException(Exception e, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogException($"{Caller}.{callerMethod}", e.Message, logCat);
}
/// <summary>
/// Logs standard Exception messages with a context object
/// </summary>
/// <param name="e"></param>
/// <param name="context">Object to focus on in scene/project window</param>
/// <param name="logCat">Optional Log Category</param>
/// <param name="callerMethod">Implicit, do not provide</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogException(Exception e, UnityEngine.Object context, LogCategory logCat = LogCategory.None, [CallerMemberName] string callerMethod = "") {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
InternalLogger.LogException($"{Caller}.{callerMethod}", e.Message, logCat, context);
}
/// <summary>
/// Logs a watch variable that updates in place in the Custom Console instead of creating new entries.
/// Ideal for values that change every frame (positions, FPS, state names, etc.).
/// </summary>
/// <param name="key">Unique identifier for this watch (e.g. "playerPos", "fps")</param>
/// <param name="value">The current value to display</param>
/// <param name="logCat">Optional Log Category</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Watch(string key, object value, LogCategory logCat = LogCategory.None) {
if(logCat == LogCategory.None) {
logCat = LogCategory;
}
LoggerUtility.WatchCallback?.Invoke((key, value?.ToString() ?? "null", logCat));
}
/// <summary>
/// Removes a watch variable from the Custom Console.
/// </summary>
/// <param name="key">The watch key to remove</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Unwatch(string key) {
LoggerUtility.UnwatchCallback?.Invoke(key);
}
}
}

View File

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

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.Logger {
public class LoggerSettings : ScriptableObject {
public bool enableGlobalLogging = true;
public Filters[] globalFilters = Array.Empty<Filters>();
public LoggerColors loggerColors = new();
#if UNITY_EDITOR
public List<Filters> LocalFilters { get; set; } = new();
#endif
public void ResetColorsToDefault() {
loggerColors = new LoggerColors();
}
}
[Serializable]
public class LoggerColors {
public Color infoColor = Color.white;
public Color warningColor = Color.yellow;
public Color errorColor = Color.red;
public Color assertColor = new(1f, 0.3f, 0.2f);
public Color exceptionColor = new(1f, 0.0f, 0.7f);
public Color spamColor = Color.grey;
}
[Serializable]
public class Filters {
public LogCategory logCategory = (LogCategory)~0;
public JovianLogType jovianLogType = JovianLogType.Spam;
public List<string> callerNames = new();
public CallerListingType callerListingType = CallerListingType.Blacklist_Caller;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 67fe3f48aa2b3b349a7b99381bebb12d

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
namespace Jovian.Logger {
public static class LoggerUtility {
private static LoggerSettings loggerSettings;
private static bool setByFrameCount = true;
private static int frameCount;
private static bool isLoaded;
private static readonly int mainThreadId = Thread.CurrentThread.ManagedThreadId;
/// <summary>
/// Returns true if the current code is executing on the main Unity thread.
/// </summary>
public static bool IsMainThread => Thread.CurrentThread.ManagedThreadId == mainThreadId;
/// <summary>
/// Callback to post formatted log messages.
/// </summary>
public static Action<(JovianLogType, LogCategory, string)> FormattedLogCallback { get; set; }
/// <summary>
/// Callback for watch-mode logs that update in place instead of creating new entries.
/// Parameters: (watchKey, value, logCategory)
/// </summary>
public static Action<(string key, string value, LogCategory category)> WatchCallback { get; set; }
/// <summary>
/// Callback when a watch key is removed.
/// </summary>
public static Action<string> UnwatchCallback { get; set; }
/// <summary>
/// If enabled it will show either the Unity's Time.frameCount or the custom frame count set by the user. This is a global setting.
/// </summary>
public static int FrameCount {
get {
if(setByFrameCount) {
try {
frameCount = Time.frameCount;
} catch(UnityException) {
// Time.frameCount can only be called from the main thread.
// Return last known value when called from a background thread.
}
}
return frameCount;
}
set {
frameCount = value;
setByFrameCount = false;
}
}
public static LoggerSettings LoadCustomLoggerSettings() {
try {
if(isLoaded) {
return loggerSettings;
}
loggerSettings = Resources.Load<LoggerSettings>("logger-settings");
isLoaded = true;
return loggerSettings;
} catch {
//Debug.Log($"[Exception] LoggerSettings could not be loaded.");
}
return null;
}
/// <summary>
/// Toggle the frame counting feature to be included with the logs on/off
/// </summary>
/// <param name="enable"></param>
public static void ToggleFrameCount(bool enable) {
IsFrameCountEnabled = enable;
}
/// <summary>
/// Enable/Disable logging globally
/// </summary>
/// <param name="enable"></param>
public static void ToggleLogging(bool enable) {
loggerSettings.enableGlobalLogging = enable;
}
/// <summary>
/// Check if the frame count feature is enabled
/// </summary>
public static bool IsFrameCountEnabled { get; private set; }
/// <summary>
/// Load settings preemptively to avoid asynchronous loading for unity assets.
/// </summary>
public static void PreloadLoggerSettings() {
try {
LoadCustomLoggerSettings();
} catch(Exception e) {
Debug.Log($"[Exception] LoggerSettings could not be loaded. {e}");
}
}
/// <summary>
/// Adds a custom filter programmatically. Useful for setting up remote filters
/// </summary>
/// <param name="logCategory">Required set of log categories</param>
/// <param name="jovianLogLevel">The log level, default being level 4, Exception(in unity it is the base log level)</param>
/// <param name="callerNames">A list of classes that should be watched, default null</param>
/// <param name="clearAll">If all preexisting filters should be first removed</param>
public static void AddFilter(LogCategory logCategory, JovianLogType jovianLogLevel = JovianLogType.Spam, List<string> callerNames = null, bool clearAll = false) {
var filter = new Filters() {
jovianLogType = jovianLogLevel,
logCategory = logCategory,
callerNames = callerNames
};
var loggerSettings = LoadCustomLoggerSettings();
if(clearAll) {
loggerSettings.globalFilters = new Filters[1];
loggerSettings.globalFilters[0] = filter;
InternalLogger.LoadSettings();
return;
}
var filters = new Filters[loggerSettings.globalFilters.Length + 1];
for(var i = 0; i < loggerSettings.globalFilters.Length; i++) {
filters[i] = loggerSettings.globalFilters[i];
}
filters[^1] = filter;
loggerSettings.globalFilters = filters;
InternalLogger.LoadSettings();
}
public static void ReloadLoggerSettings() {
InternalLogger.LoadSettings();
}
public static UnityEngine.LogType GetLogType(JovianLogType jovianLogType) {
return jovianLogType switch {
JovianLogType.Info => UnityEngine.LogType.Log,
JovianLogType.Warning => UnityEngine.LogType.Warning,
JovianLogType.Error => UnityEngine.LogType.Error,
JovianLogType.Assert => UnityEngine.LogType.Assert,
JovianLogType.Exception => UnityEngine.LogType.Exception,
JovianLogType.Spam => UnityEngine.LogType.Log,
_ => UnityEngine.LogType.Log
};
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 89f12f254c1d11e418ec326092b0b049

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 690a449164ff86c49b1c0c9c3b4ef3b1

View File

@@ -0,0 +1,9 @@
{
"name": "com.jovian.logger",
"displayName": "Jovian Logger",
"version": "1.0.0",
"description": "A custom logger package based logger that I created some time in the past.",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.1"
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 701d375fdedcd4741aed611b3054cbc9
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,387 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
namespace Jovian.Recents {
public class RecentAssets : EditorWindow {
[System.Serializable]
private class AssetSelection {
public bool expanded;
public string assetPath;
[System.NonSerialized]
private Object actualAsset;
public Object Asset {
get {
if(actualAsset == null) {
actualAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
}
return actualAsset;
}
set => actualAsset = value;
}
public List<SubAssetSelection> subAssets;
public bool isPinned;
public string FileName => Path.GetFileName(assetPath);
}
[System.Serializable]
public struct SubAssetSelection {
// In the current structure of this tool, to reload this object is quite messy.
// The SubAsset doesn't know if the current context, e.g. scene, is the one it was previously selected from.
// There might be another asset in this other scene also called "Main Camera" etc
[System.NonSerialized]
public Object subAsset;
public string subAssetPath;
}
private bool subscribeToSelectionEvents;
private bool SubscribeToSelectionEvents {
set {
if(value) {
Selection.selectionChanged -= OnSelectionChanged;
Selection.selectionChanged += OnSelectionChanged;
}
else {
Selection.selectionChanged -= OnSelectionChanged;
}
subscribeToSelectionEvents = value;
}
}
private bool subscribeToPrefabOpenEvents;
private bool SubscribeToPrefabOpen {
set {
PrefabStage.prefabStageOpened -= OnPrefabStageOpened;
PrefabStage.prefabStageOpened += OnPrefabStageOpened;
PrefabStage.prefabStageClosing -= OnPrefabStageClosing;
PrefabStage.prefabStageClosing += OnPrefabStageClosing;
subscribeToPrefabOpenEvents = value;
}
}
private bool subscribeToSceneOpenEvents;
private bool SubscribeToSceneOpenEvents {
set {
if(value) {
EditorSceneManager.activeSceneChangedInEditMode -= OnActiveSceneChangedInEditMode;
EditorSceneManager.activeSceneChangedInEditMode += OnActiveSceneChangedInEditMode;
}
else {
EditorSceneManager.activeSceneChangedInEditMode -= OnActiveSceneChangedInEditMode;
}
subscribeToSceneOpenEvents = value;
}
}
private string EditorPrefsSettingsSelectionKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Selections";
private string EditorPrefsSettingsPrefabsKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Prefabs";
private string EditorPrefsSettingsScenesKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Scenes";
private string EditorPrefsSettingsCountKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_MaxHistory";
private string EditorPrefsSettingsHistoryKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections";
private const string PinnedIcon = "IN LockButton on";
private const string UnPinnedIcon = "IN LockButton";
[System.Serializable]
private class JsonWrapper {
public JsonWrapper(List<AssetSelection> data) {
this.data = data;
}
public List<AssetSelection> data;
}
private List<AssetSelection> selectionHistory = new();
private int maxHistory = 10;
private Vector2 scrollPos;
private PrefabStage prefabStage;
private GUIStyle guiStyle;
[MenuItem("Jovian/Assets History...", false, 20)]
private static void Init() {
var window = GetWindow<RecentAssets>(false, "Assets History");
window.minSize = new Vector2(100f, 100f);
window.Focus();
}
private void OnEnable() {
// In case of Editor reload, perhaps because of recompile, events needs to be resubscribed
LoadSettings();
SubscribeToSelectionEvents = subscribeToSelectionEvents;
SubscribeToSceneOpenEvents = subscribeToSceneOpenEvents;
SubscribeToPrefabOpen = subscribeToPrefabOpenEvents;
LoadEntries();
}
private void OnDisable() {
SaveEntries();
SaveSettings();
}
private void OnDestroy() {
SubscribeToSelectionEvents = false;
SubscribeToSceneOpenEvents = false;
SubscribeToPrefabOpen = false;
}
private void SaveEntries() {
var jsonList = JsonUtility.ToJson(new JsonWrapper(selectionHistory));
EditorPrefs.SetString(EditorPrefsSettingsHistoryKey, jsonList);
}
private void SaveSettings() {
EditorPrefs.SetBool(EditorPrefsSettingsSelectionKey, subscribeToSelectionEvents);
EditorPrefs.SetBool(EditorPrefsSettingsPrefabsKey, subscribeToPrefabOpenEvents);
EditorPrefs.SetBool(EditorPrefsSettingsScenesKey, subscribeToSceneOpenEvents);
EditorPrefs.SetInt(EditorPrefsSettingsCountKey, maxHistory);
}
private void LoadEntries() {
var jsonList = EditorPrefs.GetString(EditorPrefsSettingsHistoryKey);
var oldList = JsonUtility.FromJson<JsonWrapper>(jsonList)?.data;
if(oldList != null) {
selectionHistory = oldList;
}
}
private void LoadSettings() {
if(EditorPrefs.HasKey(EditorPrefsSettingsSelectionKey)) {
subscribeToSelectionEvents = EditorPrefs.GetBool(EditorPrefsSettingsSelectionKey);
}
if(EditorPrefs.HasKey(EditorPrefsSettingsPrefabsKey)) {
subscribeToPrefabOpenEvents = EditorPrefs.GetBool(EditorPrefsSettingsPrefabsKey);
}
if(EditorPrefs.HasKey(EditorPrefsSettingsScenesKey)) {
subscribeToSceneOpenEvents = EditorPrefs.GetBool(EditorPrefsSettingsScenesKey);
}
if(EditorPrefs.HasKey(EditorPrefsSettingsCountKey)) {
maxHistory = EditorPrefs.GetInt(EditorPrefsSettingsCountKey);
}
}
private void OnActiveSceneChangedInEditMode(Scene previousScene, Scene newScene) {
var selectedObject = AssetDatabase.LoadMainAssetAtPath(newScene.path);
if(selectedObject != null) {
OnAssetInteracted(selectedObject, newScene.path);
}
}
private void OnPrefabStageOpened(PrefabStage prefab) {
// We actually need to know if we're in prefabStage because of how asset parenting works during prefab editing, e.g. when _subscribeToSelectionEvents==true;
prefabStage = prefab;
if(subscribeToPrefabOpenEvents) {
OnAssetInteracted(prefab.prefabContentsRoot, prefab.assetPath);
}
}
private void OnPrefabStageClosing(PrefabStage prefab) {
prefabStage = null;
}
private void OnSelectionChanged() {
var selectedObject = Selection.activeObject;
if(!selectedObject) {
return; // When you select something in e.g. Project View that isn't an asset or object, perhaps the category header "Packages"
}
string assetPath;
if(prefabStage != null && selectedObject is GameObject selectedGameObject && prefabStage.IsPartOfPrefabContents(selectedGameObject)) {
assetPath = prefabStage.assetPath; // If we're in a PrefabStage and selecting a child GO, the assetPath is the actual prefab itself, not this potentially nested entity
}
else {
assetPath = AssetDatabase.GetAssetOrScenePath(selectedObject);
}
OnAssetInteracted(selectedObject, assetPath);
}
private void OnAssetInteracted(Object selectedObject, string assetPath) {
AssetSelection selection = null;
for(var i = 0; i < selectionHistory.Count; i++) {
if(selectionHistory[i].assetPath == assetPath) {
selection = selectionHistory[i];
selectionHistory.RemoveAt(i);
break;
}
}
if(selection == null) {
// This is a new asset selection
selection = new AssetSelection {
expanded = false,
assetPath = assetPath,
Asset = AssetDatabase.LoadMainAssetAtPath(assetPath),
subAssets = new List<SubAssetSelection>()
};
}
selectionHistory.Insert(0, selection); // Selection is now first in list
var subAssetSelection = new SubAssetSelection {
subAsset = selectedObject,
subAssetPath = selectedObject.name
};
for(var i = 0; i < selection.subAssets.Count; i++) {
if(selection.subAssets[i].subAsset == selectedObject ||
// In case of unloading and reloading a scene, we try to "reuse" entries to the same object.
// If several objects exist by the same name, they're already null and become a new entry "anyway"
(selection.subAssets[i].subAsset == null && selection.subAssets[i].subAssetPath == subAssetSelection.subAssetPath)) {
selection.subAssets.RemoveAt(i);
break;
}
}
selection.subAssets.Insert(0, subAssetSelection);
// while instead of if, since you can change the number of tracked assets
while(selectionHistory.Count > maxHistory) {
selectionHistory.RemoveAt(selectionHistory.Count - 1);
}
UpdatePinned();
Repaint();
}
private void UpdatePinned() {
var numberOfAlreadyPinnedEntries = 0;
for(var i = 0; i < selectionHistory.Count; i++) {
var currSelection = selectionHistory[i];
if(currSelection.isPinned) {
if(i == numberOfAlreadyPinnedEntries) {
numberOfAlreadyPinnedEntries++;
continue;
}
selectionHistory.RemoveAt(i);
selectionHistory.Insert(numberOfAlreadyPinnedEntries, currSelection);
numberOfAlreadyPinnedEntries++;
}
}
}
private void OnGUI() {
var originalIconSize = EditorGUIUtility.GetIconSize();
EditorGUIUtility.SetIconSize(new Vector2(16f, 16f));
if(guiStyle == null) {
guiStyle = new GUIStyle(GUI.skin.button) {
alignment = TextAnchor.MiddleLeft
};
}
EditorGUILayout.BeginHorizontal();
TightLabel("History count", "The number of recent assets currently tracked");
TightLabel(selectionHistory.Count.ToString());
if(GUILayout.Button("Clear", GUILayout.ExpandWidth(false))) {
selectionHistory.Clear();
}
GUILayout.FlexibleSpace();
TightLabel("What to track", "Select for what interactions you want this list to be updated");
var onSelection = GUILayout.Toggle(subscribeToSelectionEvents, "On selection", GUILayout.ExpandWidth(false));
if(onSelection != subscribeToSelectionEvents) {
SubscribeToSelectionEvents = onSelection;
}
EditorGUI.BeginDisabledGroup(subscribeToSelectionEvents); // If we're listening to selection the other two doesn't matter
subscribeToPrefabOpenEvents = GUILayout.Toggle(subscribeToPrefabOpenEvents, "Open prefabs", GUILayout.ExpandWidth(false));
SubscribeToSceneOpenEvents = GUILayout.Toggle(subscribeToSceneOpenEvents, "Open scenes", GUILayout.ExpandWidth(false));
EditorGUI.EndDisabledGroup();
GUILayout.FlexibleSpace();
TightLabel("Max history", "The number of recent assets to track");
maxHistory = EditorGUILayout.DelayedIntField(maxHistory, GUILayout.Width(50f));
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
var pinStatusChanged = false;
for(var i = 0; i < selectionHistory.Count; i++) {
var selection = selectionHistory[i];
EditorGUILayout.BeginHorizontal();
// Pinning
var pinIcon = selection.isPinned ? PinnedIcon : UnPinnedIcon;
if(GUILayout.Button(EditorGUIUtility.IconContent(pinIcon), GUILayout.Width(20f), GUILayout.ExpandWidth(false))) {
selection.isPinned = !selection.isPinned;
pinStatusChanged = true;
}
// Open
if(GUILayout.Button(EditorGUIUtility.IconContent("d_editicon.sml"), GUILayout.Width(26f), GUILayout.ExpandWidth(false))) {
if(Event.current.button == 1) {
// On right click we only ping the object.
EditorGUIUtility.PingObject(selection.Asset);
}
else {
AssetDatabase.OpenAsset(selection.Asset);
}
}
// Object
GUI.enabled = false;
if(selection.Asset != null) {
EditorGUILayout.ObjectField(selection.Asset, typeof(Object), false);
}
else {
var labelNotFound = new GUIContent($"{selection.FileName} not found.",
EditorGUIUtility.ObjectContent(selection.Asset, typeof(Object)).image, selection.assetPath);
EditorGUILayout.LabelField(labelNotFound);
}
GUI.enabled = true;
// Sub entries
GUILayout.Label(GUIContent.none, GUILayout.MinWidth(80f), GUILayout.MaxWidth(80f));
var rect = GUILayoutUtility.GetLastRect();
selection.expanded = EditorGUI.Foldout(rect, selection.expanded, "Sub entries", true);
EditorGUILayout.EndHorizontal();
if(selection.expanded) {
EditorGUILayout.BeginVertical();
EditorGUI.indentLevel++;
for(var y = 0; y < selection.subAssets.Count; y++) {
var subAsset = selection.subAssets[y];
EditorGUILayout.BeginHorizontal();
EditorGUILayout.Space(10f, false); // As an indent
var label = new GUIContent(subAsset.subAssetPath, EditorGUIUtility.ObjectContent(subAsset.subAsset, typeof(Object)).image);
if(GUILayout.Button(label, guiStyle, GUILayout.MinWidth(50f), GUILayout.MaxWidth(200f), GUILayout.ExpandWidth(false))) {
if(Event.current.button == 1) {
// On right click we only ping the object.
EditorGUIUtility.PingObject(subAsset.subAsset);
}
else {
Selection.activeObject = subAsset.subAsset;
}
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
EditorGUI.indentLevel--;
}
}
EditorGUILayout.EndScrollView();
EditorGUIUtility.SetIconSize(originalIconSize);
if(pinStatusChanged) {
UpdatePinned();
}
}
public static void TightLabel(string labelStr) {
var label = new GUIContent(labelStr);
TightLabel(label);
}
public static void TightLabel(string labelStr, string tooltip) {
var label = new GUIContent(labelStr, tooltip);
TightLabel(label);
}
public static void TightLabel(GUIContent label) {
//This is the important bit, we set the width to the calculated width of the content in the GUIStyle of the control
EditorGUILayout.LabelField(label, GUILayout.Width(GUI.skin.label.CalcSize(label).x));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 537374a9d1704fb408713e76df4b85f9

View File

@@ -0,0 +1,6 @@
{
"name": "com.jovian.assets-history",
"displayName": "Assets History Tracker",
"version": "1.0.0",
"description": "Helper to track recently used assets in the editor and display them in a menu."
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 74e2fe99929f7e84a92d5d34512b6942
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,144 @@
using System;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace Jovian.Utilities.Editor {
[CustomEditor(typeof(CollisionExtractor))]
public class CollisionExtractorEditor : UnityEditor.Editor {
public override void OnInspectorGUI() {
base.OnInspectorGUI();
serializedObject.Update();
if(GUILayout.Button("Extract Colliders")) {
ExtractCollidersIntoPrefab();
}
}
private void ExtractCollidersIntoPrefab() {
CollisionExtractor collisionExtractor = (CollisionExtractor)target;
GameObject source = collisionExtractor.root;
GameObject targetPrefab = collisionExtractor.targetPrefab;
// Verify input
if(source == null) {
Debug.LogWarning("[CollisionExtractor] no Root assigned, please assign a root then try again!");
return;
}
if(targetPrefab == null) {
Debug.LogWarning("[CollisionExtractor] no TargetPrefab assigned, please assign a root then try again!");
return;
}
else if(targetPrefab.scene.name != null) {
Debug.LogWarning("[CollisionExtractor] TargetPrefab is not a prefab instance, please assign a proper prefab instance!");
return;
}
string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(targetPrefab);
GameObject prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath);
// Remove any old children and build up a new hierarchy matching the source
prefabRoot.transform.DetachChildren();
// Try to remove all components on the prefab root (several iterations due to components sometime requiring each other, thus requiring a certain order of deletion)
for(int i = 0; i < 6; i++) {
foreach(var comp in prefabRoot.GetComponents<Component>()) {
//Don't remove the Transform component
if(!(comp is Transform)) {
DestroyImmediate(comp);
}
}
}
// We make a copy of the source object to ensure it does not get changed, when accessing the properties using reflection Unity notes some of them as changed even though we only read from it.
// For colliders this happens to the physics material, if it is set to "None" it will become an empty value on the source in the editor but properly pick the None value for the target.
GameObject sourceCopy = Instantiate(source);
try {
CopyCollidersRecursive(sourceCopy, prefabRoot);
}
finally {
DestroyImmediate(sourceCopy);
}
Debug.Log("[CollisionExtractor] extraction into target prefab complete.");
PrefabUtility.SaveAsPrefabAsset(prefabRoot, prefabPath);
PrefabUtility.UnloadPrefabContents(prefabRoot);
}
private void CopyCollidersRecursive(GameObject sourceNode, GameObject targetNode) {
// Copy all transform settings
targetNode.transform.SetPositionAndRotation(sourceNode.transform.position, sourceNode.transform.rotation);
targetNode.transform.localScale = sourceNode.transform.localScale;
GameObjectUtility.SetStaticEditorFlags(targetNode, GameObjectUtility.GetStaticEditorFlags(sourceNode));
targetNode.tag = sourceNode.tag;
targetNode.layer = sourceNode.layer;
// Copy all collider components
Collider[] colliders = sourceNode.GetComponents<Collider>();
for(int i = 0; i < colliders.Length; ++i) {
switch(colliders[i]) {
case BoxCollider sourceBoxCollider:
BoxCollider targetBoxCollider = targetNode.AddComponent<BoxCollider>();
GetCopyOf(targetBoxCollider, sourceBoxCollider);
break;
case SphereCollider sourceSphereCollider:
SphereCollider targetSphereCollider = targetNode.AddComponent<SphereCollider>();
GetCopyOf(targetSphereCollider, sourceSphereCollider);
break;
case CapsuleCollider sourceCapsuleCollider:
CapsuleCollider targetCapsuleCollider = targetNode.AddComponent<CapsuleCollider>();
GetCopyOf(targetCapsuleCollider, sourceCapsuleCollider);
break;
case MeshCollider sourceMeshCollider:
MeshCollider targetMeshCollider = targetNode.AddComponent<MeshCollider>();
GetCopyOf(targetMeshCollider, sourceMeshCollider);
break;
default:
Debug.LogError($"[CollisionExtractor] found unsupported collider type on game object {sourceNode.name}!");
break;
}
}
// Continue with all the child nodes
for(int i = 0; i < sourceNode.transform.childCount; ++i) {
Transform sourceChildNode = sourceNode.transform.GetChild(i);
GameObject targetChildNode = new GameObject();
targetChildNode.name = sourceChildNode.name;
targetChildNode.transform.parent = targetNode.transform;
CopyCollidersRecursive(sourceChildNode.gameObject, targetChildNode);
}
// If we are a leaf node without colliders we are not useful, delete this node
if(colliders.Length == 0 && targetNode.transform.childCount == 0) {
DestroyImmediate(targetNode);
}
}
// Taken from https://answers.unity.com/questions/530178/how-to-get-a-component-from-an-object-and-add-it-t.html?_ga=2.50760041.112217741.1608192858-1956498980.1555671355
private T GetCopyOf<T>(Component target, T source) where T : Component {
Type type = target.GetType();
if(type != source.GetType()) return null; // type mis-match
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default;
var pinfos = from property in type.GetProperties(flags)
where !property.CustomAttributes.Any(attribute => attribute.AttributeType == typeof(ObsoleteAttribute))
select property;
foreach(var pinfo in pinfos) {
if(pinfo.CanWrite) {
try {
pinfo.SetValue(target, pinfo.GetValue(source, null), null);
}
catch { } // In case of NotImplementedException being thrown. For some reason specifying that exception didn't seem to catch it, so I didn't catch anything specific.
}
}
FieldInfo[] finfos = type.GetFields(flags);
foreach(var finfo in finfos) {
finfo.SetValue(target, finfo.GetValue(source));
}
return target as T;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 945d56cc182956e4a9dfb70d9051c0d1

View File

@@ -0,0 +1,84 @@
using UnityEngine;
using UnityEditor;
namespace Jovian.Utilities.Editor {
[CustomEditor(typeof(CustomRenderQueueMaterialList))]
public class CustomRenderQueueMaterialListEditor : UnityEditor.Editor {
private const int MAXNAMELENGTH = 30;
public override void OnInspectorGUI() {
if(((CustomRenderQueueMaterialList)target).updateMaterialsInEditor) {
EditorGUILayout.HelpBox("Enabling 'updateMaterialsInEditor' will cause the Editor to change material files. Only use in debug and please review your changelist before submitting.", MessageType.Warning);
}
DrawAddGUI();
base.OnInspectorGUI();
DrawAddGUI();
}
private void DrawAddGUI() {
GUILayout.BeginHorizontal();
if(GUILayout.Button("Add Empty")) {
AddEmpty();
}
if(Selection.activeGameObject) {
if(GUILayout.Button($"Add from '{GetNiceName(Selection.activeGameObject)}'")) {
AddGameObject(Selection.activeGameObject);
}
}
if(Selection.activeObject is Material) {
if(GUILayout.Button($"Add '{GetNiceName(Selection.activeObject)}'")) {
AddMaterial(Selection.activeObject as Material);
}
}
GUILayout.EndHorizontal();
}
private string GetNiceName(Object obj) {
var objectName = obj.name;
if(objectName.Length > MAXNAMELENGTH) {
objectName = objectName.Substring(0, MAXNAMELENGTH - 2) + "..";
}
return objectName;
}
private void AddEmpty() {
AddElement(null, -1);
}
private void AddGameObject(GameObject gameObject) {
var renderer = gameObject.GetComponentInChildren<Renderer>();
if(renderer == null) {
Debug.LogError($"Cannot add Renderer from {gameObject}, there is none.");
return;
}
AddMaterial(renderer.sharedMaterial);
}
private void AddMaterial(Material material) {
if(material == null) {
Debug.LogError($"Cannot add Material, it is null.");
return;
}
if(((CustomRenderQueueMaterialList)target).DoesMaterialExistInList(material)) {
Debug.LogError($"Cannot add Material, it is already listed.");
return;
}
AddElement(material, material.renderQueue);
}
private void AddElement(Material material, int renderQueue) {
var listProperty = serializedObject.FindProperty("materialRenderQueueList");
var count = listProperty.arraySize;
listProperty.arraySize = count + 1;
var elementProperty = listProperty.GetArrayElementAtIndex(count);
var materialProperty = elementProperty.FindPropertyRelative("material");
var renderQueueProperty = elementProperty.FindPropertyRelative("renderQueue");
materialProperty.objectReferenceValue = material;
renderQueueProperty.intValue = renderQueue;
serializedObject.ApplyModifiedProperties();
}
}
}

View File

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

View File

@@ -0,0 +1,26 @@
using UnityEngine;
using UnityEditor;
namespace Jovian.Utilities.Editor {
[CustomPropertyDrawer(typeof(CustomRenderQueueMaterialList.MaterialRenderQueue))]
public class CustomRenderQueueMaterialListPropertyDrawer : PropertyDrawer {
private const float RENDERQUEUE_PROPERTY_WIDTH = 45f;
private const float GUI_PADDING = 2f;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
var indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var materialRect = new Rect(position.x, position.y, position.width - RENDERQUEUE_PROPERTY_WIDTH, position.height);
var renderQueueRect = new Rect(materialRect.xMax + GUI_PADDING, position.y, RENDERQUEUE_PROPERTY_WIDTH - GUI_PADDING, position.height);
EditorGUI.PropertyField(materialRect, property.FindPropertyRelative("material"), new GUIContent(string.Empty, "Material"));
EditorGUI.PropertyField(renderQueueRect, property.FindPropertyRelative("renderQueue"), new GUIContent(string.Empty, "RenderQueue"));
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
}
}

View File

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

View File

@@ -0,0 +1,29 @@
using UnityEngine;
using UnityEditor;
namespace Jovian.Utilities.Editor {
public class NumberRangePropertyDrawer : PropertyDrawer {
private const float PADDING = 1f;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
float width = position.width * 0.5f - PADDING;
Rect minRect = new Rect(position.x, position.y, width, position.height);
Rect maxRect = new Rect(minRect.xMax + PADDING * 2f, position.y, width, position.height);
int indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUI.PropertyField(minRect, property.FindPropertyRelative("min"), GUIContent.none);
EditorGUI.PropertyField(maxRect, property.FindPropertyRelative("max"), GUIContent.none);
EditorGUI.indentLevel = indentLevel;
EditorGUI.EndProperty();
}
}
[CustomPropertyDrawer(typeof(FloatRange))]
public class FloatRangePropertyDrawer : NumberRangePropertyDrawer { }
[CustomPropertyDrawer(typeof(IntRange))]
public class IntRangePropertyDrawer : NumberRangePropertyDrawer { }
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 225b3cfe03be1ff41b3f72b06ae2fa23

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
using System.Collections;
using System.Text;
namespace Jovian.Utilities {
public static class ArrayUtility {
public static string ListToString(this IList list, bool newLinePerEntry = false) {
if(list == null) {
return "<NULL>";
}
StringBuilder sb = new();
sb.Append("[");
sb.Append(list.Count);
sb.Append("]{");
if(newLinePerEntry) {
sb.AppendLine();
}
for(int i = 0, c = list.Count; i < c; i++) {
sb.Append(list[i]);
if(i < c - 1) {
if(newLinePerEntry) {
sb.AppendLine(",");
}
else {
sb.Append(", ");
}
}
}
if(newLinePerEntry) {
sb.AppendLine();
}
sb.Append("}");
return sb.ToString();
}
}
}

View File

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

View File

@@ -0,0 +1,42 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace Jovian.Utilities {
public static class BowserLog {
private const string PREFIX = "Bowser:";
[MethodImpl(MethodImplOptions.AggressiveInlining), DebuggerHidden]
public static void Log(string log, object obj = null) {
Debug.Log(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
}
[MethodImpl(MethodImplOptions.AggressiveInlining), DebuggerHidden]
public static void LogWarning(string log, object obj = null) {
Debug.LogWarning(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
}
[MethodImpl(MethodImplOptions.AggressiveInlining), DebuggerHidden]
public static void LogError(string log, object obj = null) {
Debug.LogError(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
}
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD"), DebuggerHidden]
public static void LogDebug(string log, object obj = null) {
Debug.Log(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
}
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD"), DebuggerHidden]
public static void LogWarningDebug(string log, object obj = null) {
Debug.LogWarning(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
}
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD"), DebuggerHidden]
public static void LogErrorDebug(string log, object obj = null) {
Debug.LogError(obj == null ? $"{PREFIX}{log}" : $"{PREFIX}[{obj.GetType().Name}] {log}", obj as Object);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 79607e8a014ff1841b183019d9102aff

View File

@@ -0,0 +1,38 @@
using UnityEngine;
namespace Jovian.Utilities {
public static class CachedMainCamera {
private static int lastFrame = -1;
private static Camera mainCamera;
private static Transform mainCameraTransform;
public static Camera MainCamera {
get {
if(mainCamera) {
return mainCamera;
}
AssignCameraReferences();
return mainCamera;
}
}
public static Transform MainCameraTransform {
get {
if(mainCameraTransform) {
return mainCameraTransform;
}
AssignCameraReferences();
return mainCameraTransform;
}
}
private static void AssignCameraReferences() {
int frame = Time.frameCount;
if(lastFrame != frame) {
mainCamera = Camera.main;
mainCameraTransform = (mainCamera ? mainCamera.transform : null);
lastFrame = frame;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0701523b57a704b4780d0a226f0d0d1b

View File

@@ -0,0 +1,24 @@
using UnityEngine;
namespace Jovian.Utilities {
[RequireComponent(typeof(Canvas))]
public class CanvasAutoAssignWorldCamera : MonoBehaviour {
public Canvas canvas;
public bool autoDisableOnceCameraFound = false;
#if UNITY_EDITOR
public void Reset() {
SerializedObjectUtility.SaveObjectProperties(this, nameof(canvas), GetComponent<Canvas>());
}
#endif
private void Update() {
if(canvas && !canvas.worldCamera) {
canvas.worldCamera = CachedMainCamera.MainCamera;
if(canvas.worldCamera && autoDisableOnceCameraFound) {
enabled = false;
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 59842e0a1599f854cae803d724ecb1e7

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Random = UnityEngine.Random;
namespace Jovian.Utilities.Utilities {
public static class CollectionUtility {
public static T RandomElementFromCollection<T>(this ICollection<T> enumerableObject) {
int count = enumerableObject.Count;
if (count == 0) {
throw new IndexOutOfRangeException("Cannot get RandomElement, collection size is 0.");
}
int index = Random.Range(0, count);
return enumerableObject.ElementAt(index);
}
}
}
public static class EnumerableUtility {
private static System.Random random;
public static T RandomElement<T>(this IEnumerable<T> source) {
random ??= new System.Random();
return source.RandomElement(random);
}
//https://stackoverflow.com/a/648240/584774
public static T RandomElement<T>(this IEnumerable<T> source, System.Random rng) {
T current = default(T);
int count = 0;
foreach (T element in source) {
count++;
if (rng.Next(count) == 0) {
current = element;
}
}
if (count == 0) {
throw new InvalidOperationException("Sequence was empty");
}
return current;
}
public static bool TryGetRandomElement<T>(this IEnumerable<T> source, out T outElement) {
random ??= new System.Random();
return source.TryGetRandomElement(random, out outElement);
}
public static bool TryGetRandomElement<T>(this IEnumerable<T> source, System.Random rng, out T outElement) {
T current = default(T);
int count = 0;
foreach (T element in source) {
count++;
if (rng.Next(count) == 0) {
current = element;
}
}
outElement = current;
if (count == 0) {
return false;
}
return true;
}
public static string EnumerableToString(this IEnumerable enumerable, bool newLinePerEntry = false) {
if (enumerable == null) {
return "<NULL>";
}
StringBuilder sb = new();
sb.Append("{");
if (newLinePerEntry) {
sb.AppendLine();
}
foreach (object item in enumerable) {
sb.Append(item);
if (newLinePerEntry) {
sb.AppendLine(",");
}
else {
sb.Append(", ");
}
}
if (newLinePerEntry) {
sb.AppendLine();
}
sb.Append("}");
return sb.ToString();
}
}

View File

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

View File

@@ -0,0 +1,9 @@
using UnityEngine;
namespace Jovian.Utilities {
public static class ColliderUtilities {
public static bool ContainsPoint(this Collider collider, Vector3 point) {
return (collider.ClosestPoint(point) - point).sqrMagnitude < Mathf.Epsilon;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 033c7401511b25948be41b7ab4774a03

View File

@@ -0,0 +1,18 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.Utilities {
public class CollisionExtractor : MonoBehaviour {
public GameObject root;
public GameObject targetPrefab;
// Script only used inside the editor
private void Awake() {
if(!Application.isEditor) {
Destroy(this);
}
}
}
}

View File

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

View File

@@ -0,0 +1,64 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.Utilities {
public class CustomRenderQueueMaterialList : MonoBehaviour {
[System.Serializable]
public class MaterialRenderQueue {
public Material material;
public int renderQueue;
public int storedRenderQueue;
}
public bool updateMaterialsInEditor;
[SerializeField]
private MaterialRenderQueue[] materialRenderQueueList;
private void Awake() {
#if UNITY_EDITOR
if(updateMaterialsInEditor) {
Debug.LogWarning("Updating Materials will cause asset files to change. Please review your change log to ensure only valid changes are submitted.");
}
else {
return;
}
#endif
foreach(var materialRenderQueue in materialRenderQueueList) {
if(materialRenderQueue.material) {
materialRenderQueue.storedRenderQueue = materialRenderQueue.material.renderQueue; // store the materials original render queue
materialRenderQueue.material.renderQueue = materialRenderQueue.renderQueue; // overwrite the render queue
}
}
}
private void OnDestroy() {
#if UNITY_EDITOR
if(updateMaterialsInEditor) {
Debug.LogWarning("Updating Materials will cause asset files to change. Please review your change log to ensure only valid changes are submitted.");
}
else {
return;
}
#endif
foreach(var materialRenderQueue in materialRenderQueueList) {
if(materialRenderQueue.material) {
materialRenderQueue.material.renderQueue = materialRenderQueue.storedRenderQueue;
}
}
}
public bool DoesMaterialExistInList(Material material) {
foreach(var materialRenderQueue in materialRenderQueueList) {
if(materialRenderQueue.material == material) {
return true;
}
}
return false;
}
}
}

View File

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

View File

@@ -0,0 +1,43 @@
using UnityEngine;
namespace Jovian.Utilities {
public class CustomRenderQueueRenderer : MonoBehaviour {
[SerializeField, Tooltip("Override RenderQueue for shader, 2000 = Opaque, 3000 = Transparent, -1 = Shader Default")]
private int renderQueue = -1;
[SerializeField]
private MeshRenderer meshRenderer;
[SerializeField, Tooltip("If true, only this renderer is affected because a new material is created.")]
private bool createMaterialInstance;
private void Awake() {
var material = createMaterialInstance ? meshRenderer.material : meshRenderer.sharedMaterial;
material.renderQueue = renderQueue;
}
#if UNITY_EDITOR
private void Reset() {
var serializedObject = new UnityEditor.SerializedObject(this);
var meshRendererProperty = serializedObject.FindProperty("meshRenderer");
var meshRenderer = gameObject.GetComponent<MeshRenderer>();
if(meshRenderer != null) {
meshRendererProperty.objectReferenceValue = meshRenderer;
serializedObject.ApplyModifiedProperties();
CopyRenderQueueFromMaterial(); // applies serializedObject modified properties
}
}
[ContextMenu("Copy RenderQueue from Material")]
private void CopyRenderQueueFromMaterial() {
if(meshRenderer == null) {
Debug.LogError(@"MeshRenderer is null, cannot copy RenderQueue", this);
return;
}
var serializedObject = new UnityEditor.SerializedObject(this);
var renderQueueProperty = serializedObject.FindProperty("renderQueue");
renderQueueProperty.intValue = meshRenderer.sharedMaterial.renderQueue;
serializedObject.ApplyModifiedProperties();
}
#endif
}
}

View File

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

View File

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

View File

@@ -0,0 +1,299 @@
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Diagnostics;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Jovian.Utilities {
public static class AssetUtility {
public static TAsset FindAssetInProject<TAsset>(string assetName = "") where TAsset : Object {
#if UNITY_EDITOR
var filter = $"t:{typeof(TAsset).Name} {assetName}";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<TAsset>(path);
if (asset != null)
{
return asset;
}
else
{
if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
return default(TAsset);
}
public static Object FindAssetInProject(Type assetType, string assetName = "") {
#if UNITY_EDITOR
var filter = $"t:{assetType.Name} {assetName}";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (asset != null)
{
return asset;
}
else
{
if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
return default(Object);
}
public static List<TAsset> FindAllAssetsInProject<TAsset>(string assetName = "") where TAsset : Object {
var list = new List<TAsset>();
#if UNITY_EDITOR
var filter = $"t:{typeof(TAsset).Name} {assetName}";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<TAsset>(path);
if (asset != null)
{
list.Add(asset);
}
else
{
if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
return list;
}
public static List<Object> FindAllAssetsInProject(Type assetType, string assetName = "") {
var list = new List<Object>();
#if UNITY_EDITOR
var filter = $"t:{assetType.Name} {assetName}";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (asset != null)
{
list.Add(asset);
}
else
{
if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
return list;
}
public static List<Object> FindAllObjectsInProject(Type objectType, string filter) {
List<Object> list = new List<Object>();
#if UNITY_EDITOR
string[] guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
Object asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (typeof(Component).IsAssignableFrom(objectType) &&
TryGetTypeObjectWithPrefab(asset, objectType, out Component _))
{
list.Add(asset);
}
else if (asset.GetType().IsAssignableFrom(objectType))
{
list.Add(asset);
}
else if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
return list;
}
// Prefabs
public static TAsset FindPrefabInProject<TAsset>(string assetName = "") where TAsset : Component {
#if UNITY_EDITOR
var filter = $"t:Prefab {assetName}";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<TAsset>(path);
if (TryGetTypeObjectWithPrefab(asset, out TAsset component))
{
return component;
}
else
{
if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
Debug.LogError($"Failed to find asset '{assetName}' <{typeof(TAsset)}>");
return default(TAsset);
}
public static Object FindPrefabInProject(Type assetType, string assetName = "") {
#if UNITY_EDITOR
var filter = $"t:Prefab {assetName}";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (TryGetTypeObjectWithPrefab(asset, assetType, out Component component))
{
return component;
}
else
{
if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
return default(Object);
}
public static List<TAsset> FindAllPrefabsInProject<TAsset>(string assetName = "") where TAsset : Component {
var list = new List<TAsset>();
#if UNITY_EDITOR
var filter = $"t:Prefab {assetName}";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<TAsset>(path);
if (TryGetTypeObjectWithPrefab(asset, out TAsset component))
{
list.Add(component);
}
else
{
if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
return list;
}
public static List<Object> FindAllPrefabsInProject(Type assetType, string assetName = "") {
var list = new List<Object>();
#if UNITY_EDITOR
var filter = $"t:Prefab {assetName}";
var guids = AssetDatabase.FindAssets(filter);
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (TryGetTypeObjectWithPrefab(asset, assetType, out Component component))
{
list.Add(component);
}
else
{
if (ShouldUnloadAsset(asset))
{
Resources.UnloadAsset(asset);
}
}
}
#else
Debug.LogError("AssetUtility should not be called in non-Editor mode");
#endif
return list;
}
// Util
#if UNITY_EDITOR
public static bool ShouldUnloadAsset(Object asset) {
if (asset == null)
{
return false;
}
return !(asset is GameObject or Component or AssetBundle ||
PrefabUtility.GetPrefabAssetType(asset) != PrefabAssetType.NotAPrefab);
}
private static bool TryGetTypeObjectWithPrefab(Object prefab, Type type, out Component component) {
if (prefab != null && prefab is GameObject gameObject && gameObject.TryGetComponent(type, out component))
{
return true;
}
component = default;
return false;
}
private static bool TryGetTypeObjectWithPrefab<TComponent>(Object prefab, out TComponent component)
where TComponent : Component {
if (prefab != null && prefab is TComponent prefabAsComponent)
{
component = prefabAsComponent;
return true;
}
if (prefab != null && prefab is GameObject gameObject && gameObject.TryGetComponent(out component))
{
return true;
}
component = default;
return false;
}
#endif
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f53e63ec5b4b3744bda48fb1159ac40

View File

@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Jovian.Utilities {
/// <summary>
/// <b>Editor use-only.</b><br/>
/// Allows easy access between different instances. Supports 1 instance per type. <br/>
/// Add instances and retrieve elsewhere.<br/><br/>
/// Read more: https://en.wikipedia.org/wiki/Service_locator_pattern
/// </summary>
public static class EditorServiceLocator {
private sealed class ServiceNotFoundException : Exception {
public ServiceNotFoundException(Type type) : base($"ServiceNotFoundException. Type={type}") { }
}
private static Dictionary<Type, object> serviceContainer = new();
public static bool IsDirty { get; private set; }
#if UNITY_EDITOR
public static Dictionary<Type, object> ServiceContainer => serviceContainer;
#endif
[RuntimeInitializeOnLoadMethod]
public static void Init() {
serviceContainer = new();
}
#if UNITY_EDITOR
[InitializeOnEnterPlayMode]
private static void Reset(EnterPlayModeOptions options) {
serviceContainer?.Clear();
IsDirty = true;
}
#endif
/// <summary>
/// Add an instance to the locator. Will throw an exception if a type already exists in the locator.
/// <seealso cref="AddOrReplace{T}"/>
/// </summary>
[Conditional("UNITY_EDITOR")]
public static void Add<T>(T service) {
#if UNITY_EDITOR
if (service == null) {
throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
}
serviceContainer.Add(typeof(T), service);
IsDirty = true;
#else
UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
#endif
}
/// <summary>
/// Add an instance to the locator. Will replace any existing instance without an exception.
/// An alias for <see cref="Set{T}"/>
/// </summary>
[Conditional("UNITY_EDITOR")]
public static void AddOrReplace<T>(T service) {
Set(service);
}
/// <summary>
/// Add an instance to the locator. Will replace any existing instance without an exception.
/// An alias for <see cref="AddOrReplace{T}"/>
/// </summary>
[Conditional("UNITY_EDITOR")]
public static void Set<T>(T service) {
#if UNITY_EDITOR
if (service == null) {
throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
}
serviceContainer[typeof(T)] = service;
IsDirty = true;
#else
UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
#endif
}
/// <summary>
/// Removes any type matching the instance passed in from the locator. This is good practice.
/// </summary>
/// <param name="service"></param>
/// <typeparam name="T"></typeparam>
/// <exception cref="NullReferenceException"></exception>
[Conditional("UNITY_EDITOR")]
public static void Remove<T>(T service) {
#if UNITY_EDITOR
if (service == null) {
throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
}
serviceContainer.Remove(typeof(T));
IsDirty = true;
#else
UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
#endif
}
/// <summary>
/// Retrieves an instance from the locator matching the type. Will throw an exception if nothing is found.
/// <seealso cref="TryGet{T}(out T)"/>
/// </summary>
public static T Get<T>() {
#if UNITY_EDITOR
if (serviceContainer.TryGetValue(typeof(T), out object service)) {
return (T)service;
}
#endif
throw new ServiceNotFoundException(typeof(T));
}
[Obsolete("Use Get<T> or TryGet<T>(out T) since they follow the C# conventions for TryGet")]
public static T TryGet<T>() {
#if UNITY_EDITOR
if (serviceContainer.TryGetValue(typeof(T), out object service)) {
return (T)service;
}
#endif
return default;
}
/// <summary>
/// Retrieves an instance from the locator matching the type. Returns true/false based on success.
/// </summary>
public static bool TryGet<T>(out T instance) {
#if UNITY_EDITOR
if (serviceContainer.TryGetValue(typeof(T), out object service)) {
instance = (T)service;
return instance != null;
}
#endif
instance = default;
return false;
}
[Conditional("UNITY_EDITOR")]
public static void Clear() {
serviceContainer.Clear();
IsDirty = true;
}
[Conditional("UNITY_EDITOR")]
public static void Clean() {
IsDirty = false;
}
}
}

View File

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

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Jovian.Utilities.Utilities {
public static class GizmosUtility {
public static bool IsGameObjectOrChildSelected(GameObject gameObject) {
#if UNITY_EDITOR
return GameObjectUtilities.IsGameObjectAChildOfGameObject(Selection.activeGameObject, gameObject);
#else
return false;
#endif
}
public static void DrawColliders(IEnumerable<Collider> colliders) {
foreach(Collider collider in colliders) {
if(collider != null) {
DrawCollider(collider);
}
}
}
public static void DrawWireColliders(IEnumerable<Collider> colliders) {
foreach(Collider collider in colliders) {
if(collider != null) {
DrawWireCollider(collider);
}
}
}
public static void DrawCollider(Collider collider)
=> DrawColliderInternal(collider, Gizmos.DrawSphere, Gizmos.DrawCube, Gizmos.DrawMesh);
public static void DrawWireCollider(Collider collider)
=> DrawColliderInternal(collider, Gizmos.DrawWireSphere, Gizmos.DrawWireCube, Gizmos.DrawWireMesh);
private static void DrawColliderInternal(Collider collider, Action<Vector3, float> drawSphere, Action<Vector3, Vector3> drawCube,
Action<Mesh> drawMesh) {
Gizmos.matrix = collider.transform.localToWorldMatrix;
switch(collider) {
case BoxCollider boxCollider:
drawCube(boxCollider.center, boxCollider.size);
break;
case SphereCollider sphereCollider:
drawSphere(sphereCollider.center, sphereCollider.radius);
break;
case CapsuleCollider capsuleCollider: {
Vector3 direction = GetAxis(capsuleCollider.direction);
drawSphere(capsuleCollider.center + direction * capsuleCollider.height * 0.5f, capsuleCollider.radius);
drawSphere(capsuleCollider.center - direction * capsuleCollider.height * 0.5f, capsuleCollider.radius);
break;
}
case MeshCollider meshCollider:
drawMesh(meshCollider.sharedMesh);
break;
default:
throw new NotSupportedException($"Cannot draw collider of type '{typeof(Collider)}'");
}
}
private static Vector3 GetAxis(int direction) {
switch(direction) {
case 0:
return Vector3.right;
case 1:
return Vector3.up;
case 2:
return Vector3.forward;
default:
throw new NotSupportedException($"Direction '{direction}' does not map to an axis.");
}
}
}
}

View File

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

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using System;
using UnityEditor.SceneManagement;
#endif
namespace Jovian.Utilities {
public static class HierarchyUtility {
#if UNITY_EDITOR
public static List<TComponent> FindComponentsInHierarchy<TComponent>(bool includeInactive = false) where TComponent : Component {
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if(prefabStage == null) {
return SceneUtility.FindComponentsInActiveScene<TComponent>(includeInactive);
}
else {
return new List<TComponent>(prefabStage.prefabContentsRoot.GetComponentsInChildren<TComponent>(includeInactive));
}
}
public static List<Component> FindComponentsInHierarchy(Type componentType, bool includeInactive = false) {
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if(prefabStage == null) {
return SceneUtility.FindComponentsInActiveScene(componentType, includeInactive);
}
else {
return new List<Component>(prefabStage.prefabContentsRoot.GetComponentsInChildren(componentType, includeInactive));
}
}
#endif
}
}

View File

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

View File

@@ -0,0 +1,68 @@
#if UNITY_EDITOR
using System;
using UnityEngine;
using UnityEditor;
namespace Jovian.Utilities {
public static class InspectorGUIUtility {
public static object DrawField(string name, Type type, object value) {
GUIContent label = new(name, $"<{type.Name}> = {value}");
if(type == typeof(string)) {
return EditorGUILayout.TextField(label, (string)value);
}
if(type == typeof(bool)) {
return EditorGUILayout.Toggle(label, (bool)value);
}
if(type == typeof(float)) {
return EditorGUILayout.FloatField(label, (float)value);
}
if(type == typeof(int)) {
return EditorGUILayout.IntField(label, (int)value);
}
if(type == typeof(byte)) {
return (byte)EditorGUILayout.IntField(label, (byte)value);
}
if(type == typeof(Vector2)) {
return EditorGUILayout.Vector2Field(label, (Vector2)value);
}
if(type == typeof(Vector3)) {
return EditorGUILayout.Vector3Field(label, (Vector3)value);
}
if(type == typeof(Vector4)) {
return EditorGUILayout.Vector4Field(label, (Vector4)value);
}
if(type == typeof(Bounds)) {
return EditorGUILayout.BoundsField(label, (Bounds)value);
}
if(type == typeof(BoundsInt)) {
return EditorGUILayout.BoundsIntField(label, (BoundsInt)value);
}
if(type == typeof(Color)) {
return EditorGUILayout.ColorField(label, (Color)value);
}
if(type == typeof(AnimationCurve)) {
return EditorGUILayout.CurveField(label, (AnimationCurve)value);
}
if (type == typeof(double)) {
return EditorGUILayout.DoubleField(label, (double)value);
}
if (type == typeof(Gradient)) {
return EditorGUILayout.GradientField(label, (Gradient)value);
}
if (type == typeof(long)) {
return EditorGUILayout.LongField(label, (long)value);
}
if(type.IsEnum) {
return EditorGUILayout.EnumPopup(label, (Enum)value);
}
if(type.IsSubclassOf(typeof(UnityEngine.Object)) || value is UnityEngine.Object) {
return EditorGUILayout.ObjectField(label, (UnityEngine.Object)value, type, true);
}
string stringValue = value == null ? "<NULL>" : value.ToString();
EditorGUILayout.TextField(label, stringValue);
return value;
}
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8b62c711143ba3e4a8d4e941f00a8d2c

View File

@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace Jovian.Utilities {
public static class SceneUtility {
public static List<GameObject> FindGameObjectsInScene(this Scene scene, bool includeInactive = false, Func<GameObject, bool> predicate = null) {
if (scene.IsValid() == false)
{
Debug.LogError("Scene is invalid");
return null;
}
var list = new List<GameObject>();
GameObject[] sceneObjects = scene.GetRootGameObjects();
GameObject[] loadedObjects = FindDontDestroyOnLoadObjects();
GameObject[] combinedGameobjects = new GameObject[sceneObjects.Length + loadedObjects.Length];
sceneObjects.CopyTo(combinedGameobjects, 0);
loadedObjects.CopyTo(combinedGameobjects, sceneObjects.Length);
foreach (var rootObject in combinedGameobjects)
{
var allChildTransforms = rootObject.GetComponentsInChildren<Transform>(includeInactive);
var allGameObjects = allChildTransforms.Select(transform => transform.gameObject);
if (predicate != null)
{
allGameObjects = allGameObjects.Where(predicate);
}
list.AddRange(allGameObjects);
}
return list;
}
public static List<GameObject> FindGameObjectsInActiveScene(bool includeInactive = false, Func<GameObject, bool> predicate = null)
=> FindGameObjectsInScene(SceneManager.GetActiveScene(), includeInactive, predicate);
// Adding support for finding objects not in the main scenes but in DontDestroyOnLoad
// From https://forum.unity.com/threads/editor-script-how-to-access-objects-under-dontdestroyonload-while-in-play-mode.442014/#post-3570916
private static GameObject[] FindDontDestroyOnLoadObjects() {
if (!Application.isPlaying)
{
return new GameObject[] { }; // return an empty array as this method creates issues in edit mode
}
GameObject temp = null;
try
{
temp = new GameObject();
Object.DontDestroyOnLoad(temp);
UnityEngine.SceneManagement.Scene dontDestroyOnLoad = temp.scene;
Object.DestroyImmediate(temp);
temp = null;
return dontDestroyOnLoad.GetRootGameObjects();
}
finally
{
if (temp != null)
Object.DestroyImmediate(temp);
}
}
public static List<TComponent> FindComponentsInScene<TComponent>(this Scene scene, bool includeInactive = false) where TComponent : Component {
if (scene.IsValid() == false)
{
Debug.LogError("Scene is invalid");
return null;
}
var list = new List<TComponent>();
foreach (var rootObject in scene.GetRootGameObjects())
{
list.AddRange(rootObject.GetComponentsInChildren<TComponent>(includeInactive));
}
return list;
}
public static List<Component> FindComponentsInScene(this Scene scene, Type componentType, bool includeInactive = false) {
if (scene.IsValid() == false)
{
Debug.LogError("Scene is invalid");
return null;
}
var list = new List<Component>();
foreach (GameObject rootObject in scene.GetRootGameObjects())
{
list.AddRange(rootObject.GetComponentsInChildren(componentType, includeInactive));
}
return list;
}
public static List<TComponent> FindComponentsInActiveScene<TComponent>(bool includeInactive = false) where TComponent : Component
=> FindComponentsInScene<TComponent>(SceneManager.GetActiveScene(), includeInactive);
public static List<Component> FindComponentsInActiveScene(Type componentType, bool includeInactive = false)
=> FindComponentsInScene(SceneManager.GetActiveScene(), componentType, includeInactive);
public static List<TComponent> FindComponentsInAllScenes<TComponent>(bool includeInactive = false) where TComponent : Component {
List<TComponent> allComponents = new();
for (int i = 0, c = SceneManager.sceneCount; i < c; i++)
{
allComponents.AddRange(FindComponentsInScene<TComponent>(SceneManager.GetSceneAt(i), includeInactive));
}
return allComponents;
}
public static TComponent FindComponentInScene<TComponent>(this Scene scene, bool includeInactive = false) where TComponent : Component {
if (scene.IsValid() == false)
{
Debug.LogError("Scene is invalid");
return null;
}
var list = new List<TComponent>();
foreach (var rootObject in scene.GetRootGameObjects())
{
list.AddRange(rootObject.GetComponentsInChildren<TComponent>(includeInactive));
}
return list.FirstOrDefault();
}
public static Component FindComponentInScene(this Scene scene, Type componentType, bool includeInactive = false) {
if (scene.IsValid() == false)
{
Debug.LogError("Scene is invalid");
return null;
}
var list = new List<Component>();
foreach (GameObject rootObject in scene.GetRootGameObjects())
{
list.AddRange(rootObject.GetComponentsInChildren(componentType, includeInactive));
}
return list.FirstOrDefault();
}
public static TComponent FindComponentInActiveScene<TComponent>(bool includeInactive = false) where TComponent : Component
=> FindComponentInScene<TComponent>(SceneManager.GetActiveScene(), includeInactive);
public static Component FindComponentInActiveScene(Type componentType, bool includeInactive = false)
=> FindComponentInScene(SceneManager.GetActiveScene(), componentType, includeInactive);
public static TComponent FindComponentInAllScenes<TComponent>(bool includeInactive = false) where TComponent : Component
=> FindComponentsInAllScenes<TComponent>(includeInactive).FirstOrDefault();
public static GameObject FindGameObject(string nameQuery) {
char delimiter = '/';
string[] querySegments = nameQuery.Split(delimiter);
return FindGameObjectsInActiveScene(true, (gameObject) => {
int queryIndex = querySegments.Length - 1;
Transform compareTransform = gameObject.transform;
do
{
if (compareTransform != null && compareTransform.name.Equals(querySegments[queryIndex]))
{
compareTransform = compareTransform.parent;
queryIndex--;
}
// if compare is null and string is null that means the first query segment was /, so that's the root element - then our parent MUST be null
else if (compareTransform == null && string.IsNullOrEmpty(querySegments[queryIndex]))
{
return true;
}
else
{
return false;
}
} while (queryIndex >= 0);
return true;
}).FirstOrDefault();
}
private static Transform GetChildWithName(this Transform transform, string childName) {
foreach (Transform child in transform.transform)
{
if (child.name.Equals(childName))
{
return child;
}
}
return null;
}
}
}

View File

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

View File

@@ -0,0 +1,211 @@
using System;
using System.Collections;
using System.Diagnostics;
using System.Text.RegularExpressions;
using UnityEngine;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
using Type = System.Type;
#if UNITY_EDITOR
using UnityEditor;
using System.Reflection;
#endif
namespace Jovian.Utilities {
public static class SerializedObjectUtility {
[Conditional("UNITY_EDITOR")]
public static void SaveObjectProperties(Object targetObject, params object[] args) {
#if UNITY_EDITOR
EditorSerializedObjectUtility.SaveObjectProperties(targetObject, args);
#endif
}
}
#if UNITY_EDITOR
public static class EditorSerializedObjectUtility {
//https://answers.unity.com/questions/929293/get-field-type-of-serializedproperty.html
public static Type GetTypeFromProperty(SerializedProperty property) {
//gets parent type info
string[] slices = property.propertyPath.Split('.');
System.Type type = property.serializedObject.targetObject.GetType();
for(int i = 0; i < slices.Length; i++)
if (slices[i] == "Array")
{
i++; //skips "data[x]"
type = type.GetElementType(); //gets info on array elements
}
//gets info on field and its type
else type = type.GetField(slices[i], BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance).FieldType;
//type is now the type of the property
return type;
}
private static readonly Regex isArrayElementRegex = new Regex("Array.data\\[\\d+\\]$");
public static bool IsPropertyAnArrayElement(SerializedProperty property) {
return isArrayElementRegex.IsMatch(property.propertyPath);
}
public static SerializedProperty GetArrayPropertyWithElementProperty(SerializedProperty property) {
SerializedObject serializedObject = property.serializedObject;
string propertyPath = property.propertyPath;
int arrayDataIndex = propertyPath.LastIndexOf(".Array.data", StringComparison.Ordinal);
string propertyPathWithoutArray = propertyPath.Substring(0, arrayDataIndex);
int pathDividerIndex = propertyPathWithoutArray.LastIndexOf(".", StringComparison.Ordinal);
string parentPropertyName = propertyPathWithoutArray;
if(pathDividerIndex != -1) {
parentPropertyName = propertyPathWithoutArray.Substring(pathDividerIndex);
}
return serializedObject.FindProperty(parentPropertyName);;
}
public static void SaveObjectProperties(Object targetObject, params object[] args) {
SerializedObject serializedObject = new SerializedObject(targetObject);
for(int i = 0; i < args.Length; i += 2) {
var keyArg = args[i];
var keyType = keyArg.GetType();
if((keyType == typeof(string)) == false) {
throw new System.NotSupportedException(string.Format("Key must be string. {0} is {1}", args[i], keyType));
}
else {
var property = serializedObject.FindProperty((string)keyArg);
object argValue = args[i + 1];
if(property == null) {
throw new System.Exception(string.Format("No property found for key {0}", keyArg));
}
if(argValue == null) {
property.objectReferenceValue = null;
}
else {
if(property.isArray) {
property.arraySize = 0;
IEnumerable argArray = (IEnumerable)argValue;
IEnumerator enumerator = argArray.GetEnumerator();
int index = 0;
while(enumerator.MoveNext()) {
property.InsertArrayElementAtIndex(index);
var arrayProperty = property.GetArrayElementAtIndex(index);
SetSerializedPropertyValue(arrayProperty, enumerator.Current);
index++;
}
}
else {
SetSerializedPropertyValue(property, argValue);
}
}
}
}
serializedObject.ApplyModifiedProperties();
}
private static void SetSerializedPropertyValue(SerializedProperty property, object value) {
if(property == null) {
UnityEngine.Debug.LogError("SetSerializedPropertyValue failed, property is null");
return;
}
switch(property.propertyType) {
case SerializedPropertyType.AnimationCurve:
property.animationCurveValue = (AnimationCurve)value;
break;
case SerializedPropertyType.Boolean:
property.boolValue = (bool)value;
break;
case SerializedPropertyType.BoundsInt:
property.boundsIntValue = (BoundsInt)value;
break;
case SerializedPropertyType.Character:
property.intValue = (int)(char)value;
break;
case SerializedPropertyType.Color: {
if(value is Color32) {
property.colorValue = (Color)(Color32)value;
}
else {
property.colorValue = (Color)value;
}
break;
}
case SerializedPropertyType.ExposedReference:
case SerializedPropertyType.ObjectReference:
property.objectReferenceValue = (Object)value;
break;
case SerializedPropertyType.Float:
property.floatValue = (float)value;
break;
case SerializedPropertyType.Integer:
property.intValue = (int)value;
break;
case SerializedPropertyType.LayerMask:
property.intValue = ((LayerMask)value).value;
break;
case SerializedPropertyType.Quaternion:
property.quaternionValue = (Quaternion)value;
break;
case SerializedPropertyType.Rect:
property.rectValue = (Rect)value;
break;
case SerializedPropertyType.RectInt:
property.rectIntValue = (RectInt)value;
break;
case SerializedPropertyType.String:
property.stringValue = (string)value;
break;
case SerializedPropertyType.Vector2:
property.vector2Value = (Vector2)value;
break;
case SerializedPropertyType.Vector2Int:
property.vector2IntValue = (Vector2Int)value;
break;
case SerializedPropertyType.Vector3:
property.vector3Value = (Vector3)value;
break;
case SerializedPropertyType.Vector3Int:
property.vector3IntValue = (Vector3Int)value;
break;
case SerializedPropertyType.Vector4:
property.vector4Value = (Vector4)value;
break;
case SerializedPropertyType.Enum:
property.enumValueIndex = (int)value; // need to test this
// flags???
break;
case SerializedPropertyType.Generic:
SaveGenericProperty(property, value);
break;
default:
throw new System.NotSupportedException($"PropertyType {property.propertyType} is not supported - yet. Array? {property.isArray} Path: {property.propertyPath}");
}
}
private static void SaveGenericProperty(SerializedProperty property, object instance) {
Type type = instance.GetType();
var fields = type.GetRuntimeFields();
foreach(FieldInfo field in fields) {
if(field.IsNotSerialized || field.IsStatic) {
continue;
}
SerializedProperty fieldProperty = property.FindPropertyRelative(field.Name);
if(fieldProperty != null) {
SetSerializedPropertyValue(property.FindPropertyRelative(field.Name), field.GetValue(instance));
}
else {
UnityEngine.Debug.Log($"SaveGenericProperty cannot field serializedProperty named '{field.Name}'");
}
}
}
}
#endif
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 51917079be732084cbaae41d702e72ce

View File

@@ -0,0 +1,45 @@
using System;
using UnityEngine;
namespace Jovian.Utilities {
public abstract class NumberRange<T> {
public T min;
public T max;
public abstract float Lerp(float t);
public abstract float LerpUnclamped(float t);
// returns 0 to 1
public abstract float InverseLerp(float t);
// returns values -1 to 1
public abstract float InverseLerpSigned(float t);
public abstract T Random();
}
[Serializable]
public class FloatRange : NumberRange<float> {
public FloatRange(float min, float max) {
this.min = min;
this.max = max;
}
public override float Lerp(float t) => Mathf.Lerp(min, max, t);
public override float LerpUnclamped(float t) => Mathf.LerpUnclamped(min, max, t);
public override float InverseLerp(float t) => Mathf.InverseLerp(min, max, t);
public override float InverseLerpSigned(float t) => Mathf.InverseLerp(min, max, t) * 2f - 1f;
public override float Random() => UnityEngine.Random.Range(min, max);
}
[Serializable]
public class IntRange : NumberRange<int> {
public IntRange(int min, int max) {
this.min = min;
this.max = max;
}
public override float Lerp(float t) => Mathf.Lerp(min, max, t);
public override float LerpUnclamped(float t) => Mathf.LerpUnclamped(min, max, t);
public override float InverseLerp(float t) => Mathf.InverseLerp(min, max, t);
public override float InverseLerpSigned(float t) => Mathf.InverseLerp(min, max, t) * 2f - 1f;
public override int Random() => UnityEngine.Random.Range(min, max);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 01901fe75ec22104b866b8a243adf3c3

View File

@@ -0,0 +1,59 @@
using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Jovian.Utilities {
public static class GameObjectUtilities {
public static void DestroyGameObjectsOfType<T>() where T : MonoBehaviour {
T[] objects = Object.FindObjectsOfType<T>();
for (int i = 0; i < objects.Length; ++i) {
Object.Destroy(objects[i].gameObject);
}
}
public static bool IsGameObjectAChildOfGameObject(this GameObject targetGameObject, GameObject potentialParentGameObject) {
if (targetGameObject == null) {
return false;
}
Transform potentialParentTransform = potentialParentGameObject.transform;
Transform checkTransform = targetGameObject.transform;
do {
if (checkTransform == potentialParentTransform) {
return true;
}
checkTransform = checkTransform.parent;
} while (checkTransform != null);
return false;
}
public static bool TryFindChildByName(this GameObject gameObject,
string name,
out GameObject childGameObject,
bool includeInactive = false,
bool allowPartialMatch = false,
StringComparison stringComparison = StringComparison.Ordinal) {
childGameObject = null;
if (gameObject == null) {
return false;
}
Transform[] children = gameObject.GetComponentsInChildren<Transform>(includeInactive);
foreach (Transform child in children) {
if (allowPartialMatch && child.name.Contains(name, stringComparison)) {
childGameObject = child.gameObject;
return true;
}
if (!allowPartialMatch && child.name.Equals(name, stringComparison)) {
childGameObject = child.gameObject;
return true;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 42dbc9bd96270ef4fb84e4df2590e3ad

View File

@@ -0,0 +1,14 @@
{
"name": "JovianUtilities",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

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

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.Utilities
{
public interface ILoadingProcess {
bool HasLoaded { get; }
}
public class LoadingProcessHandler {
private readonly HashSet<ILoadingProcess> allProcesses = new();
private readonly HashSet<ILoadingProcess> anyProcesses = new();
public event Action OnLoadComplete;
public bool IsLoadingComplete() {
foreach(ILoadingProcess loadingProcess in anyProcesses) {
if(loadingProcess.HasLoaded) {
return true;
}
}
bool areAllProcessesComplete = true;
foreach(ILoadingProcess loadingProcess in allProcesses) {
if(loadingProcess.HasLoaded == false) {
areAllProcessesComplete = false;
break;
}
}
return areAllProcessesComplete;
}
public void AddAllProcess(ILoadingProcess loadingProcess) {
allProcesses.Add(loadingProcess);
}
public void AddAnyProcess(ILoadingProcess loadingProcess) {
anyProcesses.Add(loadingProcess);
}
public void Complete() {
OnLoadComplete?.Invoke();
}
}
public class TimeElapsedLoadingProcess : ILoadingProcess {
private readonly float endTime;
public bool HasLoaded => Time.realtimeSinceStartup > endTime;
public TimeElapsedLoadingProcess(float duration) {
endTime = Time.realtimeSinceStartup + duration;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 65bcc9fd235ddb54e991d2799a5e2e2d

View File

@@ -0,0 +1,7 @@
{
"name": "com.jovian.utilities",
"displayName": "Jovian Utilities",
"version": "0.1.18",
"description": "General purpose utility scripts",
"hideInEditor": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1067449040171e9439c6e3aca37af40f
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -6,6 +6,20 @@
"source": "embedded",
"dependencies": {}
},
"com.jovian.assets-history": {
"version": "file:com.jovian.recentassets",
"depth": 0,
"source": "embedded",
"dependencies": {}
},
"com.jovian.logger": {
"version": "file:com.jovian.logger",
"depth": 0,
"source": "embedded",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.1"
}
},
"com.jovian.savesystem": {
"version": "file:com.jovian.savesystem",
"depth": 0,
@@ -20,6 +34,12 @@
"source": "embedded",
"dependencies": {}
},
"com.jovian.utilities": {
"version": "file:com.jovian.utilties",
"depth": 0,
"source": "embedded",
"dependencies": {}
},
"com.jovian.zonesystem": {
"version": "file:com.jovian.zonesystem",
"depth": 0,