com.wallstop-studios.unity-helpers 2.0.0-rc61 → 2.0.0-rc62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,489 @@
1
+ namespace UnityHelpers.Core.Helper.Logging
2
+ {
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using System.Linq;
6
+ using System.Text;
7
+ using Extension;
8
+ using UnityEngine;
9
+ using Debug = UnityEngine.Debug;
10
+ using Object = UnityEngine.Object;
11
+
12
+ public sealed class UnityLogTagFormatter : IFormatProvider, ICustomFormatter
13
+ {
14
+ public const char Separator = ',';
15
+
16
+ private static readonly string NewLine = Environment.NewLine;
17
+
18
+ private static readonly Dictionary<string, string> ColorNamesToHex = ReflectionHelpers
19
+ .LoadStaticPropertiesForType<Color>()
20
+ .Where(kvp => kvp.Value.PropertyType == typeof(Color))
21
+ .Select(kvp => (kvp.Key, ((Color)kvp.Value.GetValue(null)).ToHex()))
22
+ .ToDictionary(StringComparer.OrdinalIgnoreCase);
23
+
24
+ /// <summary>
25
+ /// All currently registered decorations by tag.
26
+ /// </summary>
27
+ public IEnumerable<string> Decorations =>
28
+ _matchingDecorations.Values.SelectMany(x => x).Select(value => value.tag);
29
+
30
+ public IReadOnlyCollection<
31
+ IReadOnlyList<(
32
+ string tag,
33
+ bool editorOnly,
34
+ Func<string, bool> predicate,
35
+ Func<string, object, string> formatter
36
+ )>
37
+ > MatchingDecorations => _matchingDecorations.Values;
38
+
39
+ private readonly SortedDictionary<
40
+ int,
41
+ List<(
42
+ string tag,
43
+ bool editorOnly,
44
+ Func<string, bool> predicate,
45
+ Func<string, object, string> formatter
46
+ )>
47
+ > _matchingDecorations = new();
48
+ private readonly StringBuilder _cachedStringBuilder = new();
49
+ private readonly List<string> _cachedDecorators = new();
50
+ private readonly HashSet<string> _appliedTags = new(StringComparer.OrdinalIgnoreCase);
51
+
52
+ public UnityLogTagFormatter()
53
+ : this(true) { }
54
+
55
+ /// <summary>
56
+ /// Creates a new UnityLogTagFormatter.
57
+ /// </summary>
58
+ /// <param name="createDefaultDecorators">If true, applies default decorators (bold, italic, color, size, and json).</param>
59
+ public UnityLogTagFormatter(bool createDefaultDecorators)
60
+ {
61
+ if (!createDefaultDecorators)
62
+ {
63
+ return;
64
+ }
65
+
66
+ AddDecoration(
67
+ format =>
68
+ string.Equals(format, "b", StringComparison.OrdinalIgnoreCase)
69
+ || string.Equals(format, "bold", StringComparison.OrdinalIgnoreCase)
70
+ || string.Equals(format, "!", StringComparison.OrdinalIgnoreCase),
71
+ format: (_, value) => $"<b>{value}</b>",
72
+ tag: "Bold",
73
+ editorOnly: true,
74
+ force: true
75
+ );
76
+
77
+ AddDecoration(
78
+ format =>
79
+ string.Equals(format, "i", StringComparison.OrdinalIgnoreCase)
80
+ || string.Equals(format, "italic", StringComparison.OrdinalIgnoreCase)
81
+ || string.Equals(format, "_", StringComparison.OrdinalIgnoreCase),
82
+ format: (_, value) => $"<i>{value}</i>",
83
+ tag: "Italic",
84
+ editorOnly: true,
85
+ force: true
86
+ );
87
+
88
+ AddDecoration(
89
+ match: "json",
90
+ format: value => value?.ToJson() ?? "{}",
91
+ tag: "JSON",
92
+ editorOnly: false,
93
+ force: true
94
+ );
95
+
96
+ const char colorCharCheck = '#';
97
+ const string colorStringCheck = "color=";
98
+ AddDecoration(
99
+ format =>
100
+ format.StartsWith(colorCharCheck)
101
+ || format.StartsWith(colorStringCheck, StringComparison.OrdinalIgnoreCase),
102
+ format: (format, value) =>
103
+ {
104
+ string baseColor = format.StartsWith(
105
+ colorStringCheck,
106
+ StringComparison.OrdinalIgnoreCase
107
+ )
108
+ ? format.Substring(colorStringCheck.Length)
109
+ : format;
110
+
111
+ string hexCode = ColorNamesToHex.GetValueOrDefault(
112
+ format.StartsWith(colorCharCheck) ? format.Substring(1) : baseColor,
113
+ baseColor
114
+ );
115
+ return $"<color={hexCode}>{value}</color>";
116
+ },
117
+ tag: "Color",
118
+ editorOnly: true,
119
+ force: true
120
+ );
121
+
122
+ const string sizeCheck = "size=";
123
+ AddDecoration(
124
+ format =>
125
+ (
126
+ format.StartsWith(sizeCheck, StringComparison.OrdinalIgnoreCase)
127
+ && int.TryParse(format.Substring(sizeCheck.Length), out _)
128
+ || int.TryParse(format, out _)
129
+ ),
130
+ format: (format, value) =>
131
+ {
132
+ if (!int.TryParse(format, out int size))
133
+ {
134
+ size = int.Parse(format.Substring(sizeCheck.Length));
135
+ }
136
+ return $"<size={size}>{value}</size>";
137
+ },
138
+ tag: "Size",
139
+ editorOnly: true,
140
+ force: true
141
+ );
142
+ }
143
+
144
+ [HideInCallstack]
145
+ public object GetFormat(Type formatType)
146
+ {
147
+ return formatType.IsAssignableFrom(typeof(ICustomFormatter)) ? this : null;
148
+ }
149
+
150
+ [HideInCallstack]
151
+ public string Format(string format, object arg, IFormatProvider formatProvider)
152
+ {
153
+ if (string.IsNullOrWhiteSpace(format))
154
+ {
155
+ return arg?.ToString() ?? string.Empty;
156
+ }
157
+
158
+ _cachedDecorators.Clear();
159
+ if (!format.Contains(Separator))
160
+ {
161
+ _cachedDecorators.Add(format);
162
+ }
163
+ else
164
+ {
165
+ _cachedStringBuilder.Clear();
166
+ foreach (char element in format)
167
+ {
168
+ if (element == Separator)
169
+ {
170
+ if (0 < _cachedStringBuilder.Length)
171
+ {
172
+ _cachedDecorators.Add(_cachedStringBuilder.ToString());
173
+ _cachedStringBuilder.Clear();
174
+ }
175
+ }
176
+ else
177
+ {
178
+ _cachedStringBuilder.Append(element);
179
+ }
180
+ }
181
+ if (0 < _cachedStringBuilder.Length)
182
+ {
183
+ _cachedDecorators.Add(_cachedStringBuilder.ToString());
184
+ _cachedStringBuilder.Clear();
185
+ }
186
+ }
187
+
188
+ _appliedTags.Clear();
189
+ object formatted = arg;
190
+ foreach (string key in _cachedDecorators)
191
+ {
192
+ foreach (
193
+ List<(
194
+ string tag,
195
+ bool editorOnly,
196
+ Func<string, bool> predicate,
197
+ Func<string, object, string> formatter
198
+ )> matchingDecoration in _matchingDecorations.Values
199
+ )
200
+ {
201
+ foreach (
202
+ (
203
+ string tag,
204
+ bool editorOnly,
205
+ Func<string, bool> predicate,
206
+ Func<string, object, string> matchingFormatter
207
+ ) in matchingDecoration
208
+ )
209
+ {
210
+ if (
211
+ (Application.isEditor || !editorOnly)
212
+ && predicate(key)
213
+ && _appliedTags.Add(tag)
214
+ )
215
+ {
216
+ formatted = matchingFormatter(key, formatted);
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ if (0 < _appliedTags.Count)
223
+ {
224
+ return formatted.ToString();
225
+ }
226
+
227
+ if (arg is not string && arg is IFormattable formattable)
228
+ {
229
+ return formattable.ToString(format, this);
230
+ }
231
+
232
+ return arg?.ToString() ?? string.Empty;
233
+ }
234
+
235
+ [HideInCallstack]
236
+ public string Log(
237
+ FormattableString message,
238
+ Object context = null,
239
+ Exception e = null,
240
+ bool pretty = true
241
+ )
242
+ {
243
+ string rendered = Render(message, context, e, pretty);
244
+ if (context != null)
245
+ {
246
+ Debug.Log(rendered, context);
247
+ }
248
+ else
249
+ {
250
+ Debug.Log(rendered);
251
+ }
252
+
253
+ return rendered;
254
+ }
255
+
256
+ [HideInCallstack]
257
+ public string LogWarn(
258
+ FormattableString message,
259
+ Object context = null,
260
+ Exception e = null,
261
+ bool pretty = true
262
+ )
263
+ {
264
+ string rendered = Render(message, context, e, pretty);
265
+ if (context != null)
266
+ {
267
+ Debug.LogWarning(rendered, context);
268
+ }
269
+ else
270
+ {
271
+ Debug.LogWarning(rendered);
272
+ }
273
+
274
+ return rendered;
275
+ }
276
+
277
+ [HideInCallstack]
278
+ public string LogError(
279
+ FormattableString message,
280
+ Object context = null,
281
+ Exception e = null,
282
+ bool pretty = true
283
+ )
284
+ {
285
+ string rendered = Render(message, context, e, pretty);
286
+ if (context != null)
287
+ {
288
+ Debug.LogError(rendered, context);
289
+ }
290
+ else
291
+ {
292
+ Debug.LogError(rendered);
293
+ }
294
+
295
+ return rendered;
296
+ }
297
+
298
+ /// <summary>
299
+ /// Attempts to add a decoration.
300
+ /// </summary>
301
+ /// <param name="match">An exact match for tag ("a" would correspond to ${value:a})</param>
302
+ /// <param name="format">A formatter to apply to the matched object (typically something like value => $"<newFormat>{value}</newFormat>"){</param>
303
+ /// <param name="tag">A descriptive, unique identifier for the decoration (for example, "Bold", or "Color")</param>
304
+ /// <param name="priority">The priority to register the decoration at. Lower values will be evaluated first.</param>
305
+ /// <param name="editorOnly">If true, will only be applied when the game is running in the Unity Editor.</param>
306
+ /// <param name="force">
307
+ /// If true, will override any existing decorations for the same tag, regardless of priority.
308
+ /// If false, decorations with the same tag (compared OrdinalIgnoreCase) will cause the registration to fail.
309
+ /// </param>
310
+ /// <returns>True if the decoration was added, false if the decoration was not added.</returns>
311
+ public bool AddDecoration(
312
+ string match,
313
+ Func<object, string> format,
314
+ string tag = null,
315
+ int priority = 0,
316
+ bool editorOnly = false,
317
+ bool force = false
318
+ )
319
+ {
320
+ return AddDecoration(
321
+ check => string.Equals(check, match, StringComparison.OrdinalIgnoreCase),
322
+ format: (_, value) => format(value),
323
+ tag: tag ?? match,
324
+ priority: priority,
325
+ editorOnly: editorOnly,
326
+ force: force
327
+ );
328
+ }
329
+
330
+ /// <summary>
331
+ /// Attempts to add a decoration.
332
+ /// </summary>
333
+ /// <param name="predicate">
334
+ /// Tag matcher. Can be as complex as you want. For example, the default color matcher
335
+ /// is implemented something like tag => tag.StartsWith('#') || tag.StartsWith("color=")
336
+ /// </param>
337
+ /// <param name="format">
338
+ /// Custom formatting function. Takes in both the matched tag as well the current object to format. In
339
+ /// the same case of color matching, the implementation needs to be smart enough to handle the case where
340
+ /// the tag is "#red" or "color=red" or "color=#FF0000".
341
+ /// </param>
342
+ /// <param name="tag">A descriptive, unique identifier for the decoration (for example, "Bold", or "Color")</param>
343
+ /// <param name="priority">The priority to register the decoration at. Lower values will be evaluated first.</param>
344
+ /// <param name="editorOnly">If true, will only be applied when the game is running in the Unity Editor.</param>
345
+ /// <param name="force">
346
+ /// If true, will override any existing decorations for the same tag, regardless of priority.
347
+ /// If false, decorations with the same tag (compared OrdinalIgnoreCase) will cause the registration to fail.
348
+ /// </param>
349
+ /// <returns>True if the decoration was added, false if the decoration was not added.</returns>
350
+
351
+ public bool AddDecoration(
352
+ Func<string, bool> predicate,
353
+ Func<string, object, string> format,
354
+ string tag,
355
+ int priority = 0,
356
+ bool editorOnly = false,
357
+ bool force = false
358
+ )
359
+ {
360
+ foreach (var entry in _matchingDecorations)
361
+ {
362
+ for (int i = 0; i < entry.Value.Count; i++)
363
+ {
364
+ var existingDecoration = entry.Value[i];
365
+ if (
366
+ !string.Equals(
367
+ existingDecoration.tag,
368
+ tag,
369
+ StringComparison.OrdinalIgnoreCase
370
+ )
371
+ )
372
+ {
373
+ continue;
374
+ }
375
+
376
+ if (force)
377
+ {
378
+ if (priority != entry.Key)
379
+ {
380
+ entry.Value.RemoveAt(i);
381
+ break;
382
+ }
383
+ entry.Value[i] = (tag, editorOnly, predicate, format);
384
+ return true;
385
+ }
386
+ return false;
387
+ }
388
+ }
389
+
390
+ if (
391
+ !_matchingDecorations.TryGetValue(
392
+ priority,
393
+ out List<(
394
+ string tag,
395
+ bool editorOnly,
396
+ Func<string, bool> predicate,
397
+ Func<string, object, string> formatter
398
+ )> matchingDecorations
399
+ )
400
+ )
401
+ {
402
+ _matchingDecorations[priority] = new List<(
403
+ string tag,
404
+ bool editorOnly,
405
+ Func<string, bool> predicate,
406
+ Func<string, object, string> formatter
407
+ )>
408
+ {
409
+ (tag, editorOnly, predicate, format),
410
+ };
411
+ return true;
412
+ }
413
+
414
+ matchingDecorations.Add((tag, editorOnly, predicate, format));
415
+ return true;
416
+ }
417
+
418
+ /// <summary>
419
+ /// Attempts to remove a decoration by its tag.
420
+ /// </summary>
421
+ /// <param name="tag">Tag for the decoration ("Bold", "Color", etc.)</param>
422
+ /// <param name="decoration">The removed decoration, if one was found.</param>
423
+ /// <returns>True if a decoration was found for that tag and removed, false otherwise.</returns>
424
+ public bool RemoveDecoration(
425
+ string tag,
426
+ out (
427
+ string tag,
428
+ bool editorOnly,
429
+ Func<string, bool> predicate,
430
+ Func<string, object, string> formatter
431
+ ) decoration
432
+ )
433
+ {
434
+ foreach (var entry in _matchingDecorations)
435
+ {
436
+ for (int i = 0; i < entry.Value.Count; ++i)
437
+ {
438
+ decoration = entry.Value[i];
439
+ if (string.Equals(tag, decoration.tag, StringComparison.OrdinalIgnoreCase))
440
+ {
441
+ entry.Value.RemoveAt(i);
442
+ if (entry.Value.Count == 0)
443
+ {
444
+ _matchingDecorations.Remove(entry.Key);
445
+ }
446
+ return true;
447
+ }
448
+ }
449
+ }
450
+
451
+ decoration = default;
452
+ return false;
453
+ }
454
+
455
+ [HideInCallstack]
456
+ private string Render(
457
+ FormattableString message,
458
+ Object unityObject,
459
+ Exception e,
460
+ bool pretty
461
+ )
462
+ {
463
+ if (!pretty)
464
+ {
465
+ return e != null
466
+ ? $"{message.ToString(this)}{NewLine} {e}"
467
+ : message.ToString(this);
468
+ }
469
+
470
+ float now = Time.time;
471
+ string componentType;
472
+ string gameObjectName;
473
+ if (unityObject != null)
474
+ {
475
+ componentType = unityObject.GetType().Name;
476
+ gameObjectName = unityObject.name;
477
+ }
478
+ else
479
+ {
480
+ componentType = "NO_TYPE";
481
+ gameObjectName = "NO_NAME";
482
+ }
483
+
484
+ return e != null
485
+ ? $"{now}|{gameObjectName}[{componentType}]|{message.ToString(this)}{NewLine} {e}"
486
+ : $"{now}|{gameObjectName}[{componentType}]|{message.ToString(this)}";
487
+ }
488
+ }
489
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 4209667fb72340e4a133b76b2a074b5e
3
+ timeCreated: 1745446454
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: abd8f6a44318484083dc400b307d82d5
3
+ timeCreated: 1745446447
@@ -7,7 +7,7 @@
7
7
  {
8
8
  public static void LogNotAssigned(this Object component, string name)
9
9
  {
10
- component.LogWarn("{0} not found.", name);
10
+ component.LogWarn($"{name} not found.");
11
11
  }
12
12
  }
13
13
  }
@@ -30,7 +30,7 @@
30
30
  {
31
31
  if (log)
32
32
  {
33
- component.LogWarn("Could not find {0}.", tag);
33
+ component.LogWarn($"Could not find {tag}.");
34
34
  }
35
35
 
36
36
  return default;
@@ -45,11 +45,7 @@
45
45
  if (log)
46
46
  {
47
47
  component.LogWarn(
48
- "Failed to find {0} on {1} (name: {2}), id [{3}].",
49
- typeof(T).Name,
50
- tag,
51
- gameObject.name,
52
- gameObject.GetInstanceID()
48
+ $"Failed to find {typeof(T).Name} on {tag} (name: {gameObject.name}), id [{gameObject.GetInstanceID()}]."
53
49
  );
54
50
  }
55
51