@mflrevan/ucp 0.5.1 → 0.5.2

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.
@@ -1,7 +1,10 @@
1
1
  using System;
2
2
  using System.Collections.Generic;
3
+ using System.IO;
3
4
  using System.Linq;
5
+ using System.Reflection;
4
6
  using System.Text.RegularExpressions;
7
+ using UnityEditor;
5
8
  using UnityEngine;
6
9
 
7
10
  namespace UCP.Bridge
@@ -14,7 +17,10 @@ namespace UCP.Bridge
14
17
 
15
18
  private static readonly object s_historyLock = new object();
16
19
  private static readonly List<LogRecord> s_history = new List<LogRecord>();
20
+ private static Func<List<ConsoleBackfillEntry>> s_consoleBackfillProvider = CaptureRecentConsoleEntries;
17
21
  private static long s_nextId = 1;
22
+ private static StreamWriter s_captureWriter;
23
+ private static string s_capturePath;
18
24
 
19
25
  public static void Register(CommandRouter router)
20
26
  {
@@ -24,6 +30,8 @@ namespace UCP.Bridge
24
30
  router.Register("logs/search", HandleSearch);
25
31
  router.Register("logs/get", HandleGet);
26
32
  router.Register("logs/status", HandleStatus);
33
+ router.Register("logs/capture/start", HandleCaptureStart);
34
+ router.Register("logs/capture/stop", HandleCaptureStop);
27
35
  }
28
36
 
29
37
  public static Dictionary<string, object> RecordLog(string message, string stackTrace, LogType type)
@@ -37,6 +45,7 @@ namespace UCP.Bridge
37
45
  {
38
46
  s_history.Clear();
39
47
  s_nextId = 1;
48
+ s_consoleBackfillProvider = CaptureRecentConsoleEntries;
40
49
  }
41
50
  }
42
51
 
@@ -45,8 +54,64 @@ namespace UCP.Bridge
45
54
  return RecordLog(level, message, stackTrace);
46
55
  }
47
56
 
57
+ public static void SetConsoleBackfillProviderForTests(Func<List<ConsoleBackfillEntry>> provider)
58
+ {
59
+ lock (s_historyLock)
60
+ {
61
+ s_consoleBackfillProvider = provider ?? CaptureRecentConsoleEntries;
62
+ }
63
+ }
64
+
65
+ public static void SeedHistoryFromConsole()
66
+ {
67
+ lock (s_historyLock)
68
+ {
69
+ SeedHistoryFromConsoleLocked();
70
+ }
71
+ }
72
+
73
+ public static Dictionary<string, object> StartFileCapture(string path)
74
+ {
75
+ lock (s_historyLock)
76
+ {
77
+ StopFileCaptureLocked();
78
+
79
+ var resolvedPath = ResolveCapturePath(path);
80
+ var directory = Path.GetDirectoryName(resolvedPath);
81
+ if (!string.IsNullOrEmpty(directory))
82
+ Directory.CreateDirectory(directory);
83
+
84
+ s_captureWriter = new StreamWriter(resolvedPath, false);
85
+ s_captureWriter.AutoFlush = true;
86
+ s_capturePath = resolvedPath;
87
+ s_captureWriter.WriteLine($"# UCP play-mode log capture started {DateTime.UtcNow:o}");
88
+
89
+ return new Dictionary<string, object>
90
+ {
91
+ ["status"] = "ok",
92
+ ["path"] = resolvedPath
93
+ };
94
+ }
95
+ }
96
+
97
+ public static Dictionary<string, object> StopFileCapture()
98
+ {
99
+ lock (s_historyLock)
100
+ {
101
+ var path = s_capturePath;
102
+ StopFileCaptureLocked();
103
+ return new Dictionary<string, object>
104
+ {
105
+ ["status"] = "ok",
106
+ ["path"] = path ?? string.Empty,
107
+ ["active"] = false
108
+ };
109
+ }
110
+ }
111
+
48
112
  public static long GetLatestId()
49
113
  {
114
+ SeedHistoryFromConsole();
50
115
  lock (s_historyLock)
51
116
  {
52
117
  return s_history.Count == 0 ? 0 : s_history[s_history.Count - 1].Id;
@@ -55,6 +120,7 @@ namespace UCP.Bridge
55
120
 
56
121
  public static Dictionary<string, object> BuildStatusSummary(long afterId = 0)
57
122
  {
123
+ SeedHistoryFromConsole();
58
124
  lock (s_historyLock)
59
125
  {
60
126
  var ordered = s_history
@@ -86,6 +152,7 @@ namespace UCP.Bridge
86
152
 
87
153
  long id = Convert.ToInt64(idObj);
88
154
 
155
+ SeedHistoryFromConsole();
89
156
  lock (s_historyLock)
90
157
  {
91
158
  var entry = s_history.FirstOrDefault(record => record.Id == id);
@@ -106,28 +173,74 @@ namespace UCP.Bridge
106
173
  return BuildStatusSummary(afterId);
107
174
  }
108
175
 
176
+ private static object HandleCaptureStart(string paramsJson)
177
+ {
178
+ var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
179
+ if (p == null || !p.TryGetValue("path", out var pathObj) || pathObj == null)
180
+ throw new ArgumentException("Missing 'path' parameter");
181
+
182
+ return StartFileCapture(pathObj.ToString());
183
+ }
184
+
185
+ private static object HandleCaptureStop(string paramsJson)
186
+ {
187
+ return StopFileCapture();
188
+ }
189
+
109
190
  private static Dictionary<string, object> RecordLog(string level, string message, string stackTrace)
110
191
  {
111
192
  lock (s_historyLock)
112
193
  {
113
- var entry = new LogRecord
114
- {
115
- Id = s_nextId++,
116
- Level = NormalizeLevel(level),
117
- Message = message ?? string.Empty,
118
- StackTrace = stackTrace ?? string.Empty,
119
- TimestampUtc = DateTime.UtcNow
120
- };
121
- entry.Timestamp = entry.TimestampUtc.ToString("o");
122
-
123
- s_history.Add(entry);
124
- if (s_history.Count > MaxHistoryEntries)
125
- s_history.RemoveAt(0);
194
+ var entry = AppendRecordLocked(
195
+ NormalizeLevel(level),
196
+ message ?? string.Empty,
197
+ stackTrace ?? string.Empty,
198
+ DateTime.UtcNow
199
+ );
200
+ WriteCaptureLineLocked(entry);
126
201
 
127
202
  return SerializeFull(entry);
128
203
  }
129
204
  }
130
205
 
206
+ private static void WriteCaptureLineLocked(LogRecord entry)
207
+ {
208
+ if (s_captureWriter == null)
209
+ return;
210
+
211
+ var message = (entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n");
212
+ s_captureWriter.WriteLine($"{entry.Timestamp} [{entry.Level}] {message}");
213
+ if (!string.IsNullOrEmpty(entry.StackTrace))
214
+ {
215
+ var stack = entry.StackTrace.Replace("\r", "\\r").Replace("\n", "\\n");
216
+ s_captureWriter.WriteLine($"{entry.Timestamp} [stack] {stack}");
217
+ }
218
+ }
219
+
220
+ private static void StopFileCaptureLocked()
221
+ {
222
+ if (s_captureWriter != null)
223
+ {
224
+ s_captureWriter.WriteLine($"# UCP log capture stopped {DateTime.UtcNow:o}");
225
+ s_captureWriter.Dispose();
226
+ s_captureWriter = null;
227
+ }
228
+
229
+ s_capturePath = null;
230
+ }
231
+
232
+ private static string ResolveCapturePath(string path)
233
+ {
234
+ if (string.IsNullOrEmpty(path))
235
+ throw new ArgumentException("Log capture path cannot be empty");
236
+
237
+ if (Path.IsPathRooted(path))
238
+ return Path.GetFullPath(path);
239
+
240
+ var projectRoot = Path.GetDirectoryName(Application.dataPath);
241
+ return Path.GetFullPath(Path.Combine(projectRoot, path));
242
+ }
243
+
131
244
  private static LogQuery ParseQuery(string paramsJson, bool includePattern)
132
245
  {
133
246
  var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
@@ -137,6 +250,8 @@ namespace UCP.Bridge
137
250
  {
138
251
  if (p.TryGetValue("level", out var levelObj) && levelObj != null)
139
252
  query.Level = NormalizeLevel(levelObj.ToString());
253
+ if (p.TryGetValue("channel", out var channelObj) && channelObj != null)
254
+ query.Channel = channelObj.ToString();
140
255
  if (p.TryGetValue("count", out var countObj) && countObj != null)
141
256
  query.Count = Math.Max(1, Convert.ToInt32(countObj));
142
257
  if (p.TryGetValue("beforeId", out var beforeObj) && beforeObj != null)
@@ -166,6 +281,7 @@ namespace UCP.Bridge
166
281
 
167
282
  private static LogQueryResult QueryHistory(LogQuery query)
168
283
  {
284
+ SeedHistoryFromConsole();
169
285
  lock (s_historyLock)
170
286
  {
171
287
  IEnumerable<LogRecord> candidates = s_history;
@@ -176,6 +292,10 @@ namespace UCP.Bridge
176
292
  candidates = candidates.Where(entry => entry.Id > query.AfterId.Value);
177
293
  if (!string.IsNullOrEmpty(query.Level))
178
294
  candidates = candidates.Where(entry => PassesLevel(entry.Level, query.Level));
295
+ if (!string.IsNullOrEmpty(query.Channel))
296
+ candidates = candidates.Where(entry =>
297
+ (entry.Message ?? string.Empty).IndexOf(query.Channel, StringComparison.OrdinalIgnoreCase) >= 0
298
+ || (entry.StackTrace ?? string.Empty).IndexOf(query.Channel, StringComparison.OrdinalIgnoreCase) >= 0);
179
299
 
180
300
  if (query.Regex != null)
181
301
  {
@@ -361,6 +481,184 @@ namespace UCP.Bridge
361
481
  }
362
482
  }
363
483
 
484
+ private static void SeedHistoryFromConsoleLocked()
485
+ {
486
+ if (s_history.Count > 0)
487
+ return;
488
+
489
+ List<ConsoleBackfillEntry> consoleEntries;
490
+ try
491
+ {
492
+ consoleEntries = s_consoleBackfillProvider != null
493
+ ? s_consoleBackfillProvider.Invoke()
494
+ : new List<ConsoleBackfillEntry>();
495
+ }
496
+ catch
497
+ {
498
+ return;
499
+ }
500
+
501
+ if (consoleEntries == null || consoleEntries.Count == 0)
502
+ return;
503
+
504
+ var baseTimestamp = DateTime.UtcNow;
505
+ for (var index = 0; index < consoleEntries.Count; index++)
506
+ {
507
+ var entry = consoleEntries[index];
508
+ if (entry == null || string.IsNullOrEmpty(entry.Message) || entry.Message.StartsWith("[UCP]", StringComparison.Ordinal))
509
+ continue;
510
+
511
+ AppendRecordLocked(
512
+ entry.Level,
513
+ entry.Message,
514
+ entry.StackTrace,
515
+ baseTimestamp.AddMilliseconds(index)
516
+ );
517
+ }
518
+ }
519
+
520
+ private static LogRecord AppendRecordLocked(string level, string message, string stackTrace, DateTime timestampUtc)
521
+ {
522
+ var entry = new LogRecord
523
+ {
524
+ Id = s_nextId++,
525
+ Level = NormalizeLevel(level),
526
+ Message = message ?? string.Empty,
527
+ StackTrace = stackTrace ?? string.Empty,
528
+ TimestampUtc = timestampUtc
529
+ };
530
+ entry.Timestamp = entry.TimestampUtc.ToString("o");
531
+
532
+ s_history.Add(entry);
533
+ if (s_history.Count > MaxHistoryEntries)
534
+ s_history.RemoveAt(0);
535
+
536
+ return entry;
537
+ }
538
+
539
+ private static List<ConsoleBackfillEntry> CaptureRecentConsoleEntries()
540
+ {
541
+ try
542
+ {
543
+ var assembly = typeof(Editor).Assembly;
544
+ var logEntriesType = assembly.GetType("UnityEditor.LogEntries");
545
+ var logEntryType = assembly.GetType("UnityEditor.LogEntry");
546
+ if (logEntriesType == null || logEntryType == null)
547
+ return new List<ConsoleBackfillEntry>();
548
+
549
+ var getCount = logEntriesType.GetMethod("GetCount", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
550
+ var getEntry = logEntriesType
551
+ .GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
552
+ .FirstOrDefault(method => method.Name == "GetEntryInternal");
553
+ var messageField = logEntryType.GetField("message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
554
+ var modeField = logEntryType.GetField("mode", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
555
+ if (getCount == null || getEntry == null || messageField == null || modeField == null)
556
+ return new List<ConsoleBackfillEntry>();
557
+
558
+ var count = Convert.ToInt32(getCount.Invoke(null, null));
559
+ if (count <= 0)
560
+ return new List<ConsoleBackfillEntry>();
561
+
562
+ var captured = new List<ConsoleBackfillEntry>();
563
+ var start = Math.Max(0, count - MaxHistoryEntries);
564
+ for (var index = start; index < count; index++)
565
+ {
566
+ var args = new object[] { index, Activator.CreateInstance(logEntryType) };
567
+ if (!Convert.ToBoolean(getEntry.Invoke(null, args)))
568
+ continue;
569
+
570
+ var entry = CreateConsoleBackfillEntry(
571
+ messageField.GetValue(args[1]) as string,
572
+ Convert.ToInt32(modeField.GetValue(args[1]))
573
+ );
574
+ if (entry != null)
575
+ captured.Add(entry);
576
+ }
577
+
578
+ return captured;
579
+ }
580
+ catch
581
+ {
582
+ return new List<ConsoleBackfillEntry>();
583
+ }
584
+ }
585
+
586
+ private static ConsoleBackfillEntry CreateConsoleBackfillEntry(string rawMessage, int mode)
587
+ {
588
+ var normalized = (rawMessage ?? string.Empty).Replace("\r\n", "\n").TrimEnd();
589
+ if (string.IsNullOrEmpty(normalized) || normalized.StartsWith("[UCP]", StringComparison.Ordinal))
590
+ return null;
591
+
592
+ var split = SplitConsoleMessage(normalized);
593
+ return new ConsoleBackfillEntry
594
+ {
595
+ Level = InferConsoleLevel(mode, split.Message, split.StackTrace),
596
+ Message = split.Message,
597
+ StackTrace = split.StackTrace
598
+ };
599
+ }
600
+
601
+ private static SplitLogMessage SplitConsoleMessage(string rawMessage)
602
+ {
603
+ var firstNewline = rawMessage.IndexOf('\n');
604
+ if (firstNewline < 0)
605
+ {
606
+ return new SplitLogMessage
607
+ {
608
+ Message = rawMessage,
609
+ StackTrace = string.Empty
610
+ };
611
+ }
612
+
613
+ var firstLine = rawMessage.Substring(0, firstNewline).TrimEnd();
614
+ var remainder = rawMessage.Substring(firstNewline + 1).Trim();
615
+ if (LooksLikeStackTrace(remainder))
616
+ {
617
+ return new SplitLogMessage
618
+ {
619
+ Message = firstLine,
620
+ StackTrace = remainder
621
+ };
622
+ }
623
+
624
+ return new SplitLogMessage
625
+ {
626
+ Message = rawMessage,
627
+ StackTrace = string.Empty
628
+ };
629
+ }
630
+
631
+ private static bool LooksLikeStackTrace(string value)
632
+ {
633
+ if (string.IsNullOrEmpty(value))
634
+ return false;
635
+
636
+ return value.StartsWith("UnityEngine.", StringComparison.Ordinal)
637
+ || value.StartsWith("UnityEditor.", StringComparison.Ordinal)
638
+ || value.StartsWith("System.", StringComparison.Ordinal)
639
+ || value.StartsWith("---", StringComparison.Ordinal)
640
+ || value.Contains(" (at ")
641
+ || value.IndexOf(":Log", StringComparison.Ordinal) >= 0
642
+ || value.IndexOf(":LogWarning", StringComparison.Ordinal) >= 0
643
+ || value.IndexOf(":LogError", StringComparison.Ordinal) >= 0
644
+ || value.IndexOf(":LogException", StringComparison.Ordinal) >= 0;
645
+ }
646
+
647
+ private static string InferConsoleLevel(int mode, string message, string stackTrace)
648
+ {
649
+ var combined = (message ?? string.Empty) + "\n" + (stackTrace ?? string.Empty);
650
+ if ((mode & 0x100) != 0 || combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0)
651
+ return combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0 ? "exception" : "error";
652
+ if ((mode & 0x200) != 0)
653
+ return "warning";
654
+ if (Regex.IsMatch(combined, @"\bwarning\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
655
+ return "warning";
656
+ if (Regex.IsMatch(combined, @"\berror\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
657
+ return combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0 ? "exception" : "error";
658
+
659
+ return "info";
660
+ }
661
+
364
662
  private static string Preview(string value, int maxChars)
365
663
  {
366
664
  if (string.IsNullOrEmpty(value) || value.Length <= maxChars)
@@ -417,9 +715,23 @@ namespace UCP.Bridge
417
715
  public DateTime TimestampUtc;
418
716
  }
419
717
 
718
+ public sealed class ConsoleBackfillEntry
719
+ {
720
+ public string Level;
721
+ public string Message;
722
+ public string StackTrace;
723
+ }
724
+
725
+ private sealed class SplitLogMessage
726
+ {
727
+ public string Message;
728
+ public string StackTrace;
729
+ }
730
+
420
731
  private sealed class LogQuery
421
732
  {
422
733
  public string Level;
734
+ public string Channel;
423
735
  public string Pattern;
424
736
  public Regex Regex;
425
737
  public int Count;
@@ -64,7 +64,10 @@ namespace UCP.Bridge
64
64
 
65
65
  var saveDirtyScenes = GetBoolParam(paramsJson, "saveDirtyScenes", true);
66
66
  var discardUntitled = GetBoolParam(paramsJson, "discardUntitled", true);
67
+ var logFile = GetStringParam(paramsJson, "logFile");
67
68
  SaveDirtyScenesIfRequested(saveDirtyScenes, discardUntitled);
69
+ if (!string.IsNullOrEmpty(logFile))
70
+ LogsController.StartFileCapture(logFile);
68
71
 
69
72
  lock (s_sessionLock)
70
73
  {
@@ -151,11 +154,21 @@ namespace UCP.Bridge
151
154
  break;
152
155
  case PlayModeStateChange.EnteredEditMode:
153
156
  s_lastExitedPlayAtUtc = DateTime.UtcNow;
157
+ LogsController.StopFileCapture();
154
158
  break;
155
159
  }
156
160
  }
157
161
  }
158
162
 
163
+ private static string GetStringParam(string paramsJson, string key)
164
+ {
165
+ var parameters = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
166
+ if (parameters != null && parameters.TryGetValue(key, out var valueObj) && valueObj != null)
167
+ return valueObj.ToString();
168
+
169
+ return null;
170
+ }
171
+
159
172
  private static object SerializeSessionSnapshot(SessionSnapshot snapshot)
160
173
  {
161
174
  var now = DateTime.UtcNow;
@@ -0,0 +1,151 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Reflection;
4
+ using UnityEditor;
5
+ using UnityEngine;
6
+
7
+ namespace UCP.Bridge
8
+ {
9
+ public static class ShaderController
10
+ {
11
+ public static void Register(CommandRouter router)
12
+ {
13
+ router.Register("shader/errors", HandleErrors);
14
+ }
15
+
16
+ private static object HandleErrors(string paramsJson)
17
+ {
18
+ var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
19
+ var errorsOnly = p != null && p.TryGetValue("errorsOnly", out var errorsOnlyObj) && errorsOnlyObj != null && Convert.ToBoolean(errorsOnlyObj);
20
+ var filter = p != null && p.TryGetValue("filter", out var filterObj) && filterObj != null ? filterObj.ToString() : null;
21
+
22
+ AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
23
+
24
+ var diagnostics = new List<object>();
25
+ var scanned = 0;
26
+ foreach (var guid in AssetDatabase.FindAssets("t:Shader"))
27
+ {
28
+ var path = AssetDatabase.GUIDToAssetPath(guid);
29
+ var shader = AssetDatabase.LoadAssetAtPath<Shader>(path);
30
+ if (shader == null)
31
+ continue;
32
+ if (!MatchesFilter(shader.name, path, filter))
33
+ continue;
34
+
35
+ scanned++;
36
+ foreach (var diagnostic in ReadShaderDiagnostics(shader, path, errorsOnly))
37
+ diagnostics.Add(diagnostic);
38
+ }
39
+
40
+ return new Dictionary<string, object>
41
+ {
42
+ ["status"] = "ok",
43
+ ["capability"] = FindShaderMessageMethod() != null ? "shader-messages" : "fallback",
44
+ ["scanned"] = scanned,
45
+ ["count"] = diagnostics.Count,
46
+ ["diagnostics"] = diagnostics
47
+ };
48
+ }
49
+
50
+ private static IEnumerable<object> ReadShaderDiagnostics(Shader shader, string path, bool errorsOnly)
51
+ {
52
+ var method = FindShaderMessageMethod();
53
+ if (method == null)
54
+ yield break;
55
+
56
+ var messages = method.Invoke(null, new object[] { shader }) as Array;
57
+ if (messages == null)
58
+ yield break;
59
+
60
+ foreach (var message in messages)
61
+ {
62
+ var isWarning = ReadBoolMember(message, "warning", "isWarning");
63
+ if (errorsOnly && isWarning)
64
+ continue;
65
+
66
+ yield return new Dictionary<string, object>
67
+ {
68
+ ["shader"] = shader.name,
69
+ ["path"] = path,
70
+ ["severity"] = isWarning ? "warning" : "error",
71
+ ["message"] = ReadStringMember(message, "message", "messageDetails"),
72
+ ["line"] = ReadIntMember(message, "line"),
73
+ ["platform"] = ReadStringMember(message, "platform"),
74
+ ["file"] = ReadStringMember(message, "file")
75
+ };
76
+ }
77
+ }
78
+
79
+ private static MethodInfo FindShaderMessageMethod()
80
+ {
81
+ var shaderUtil = typeof(Editor).Assembly.GetType("UnityEditor.ShaderUtil");
82
+ return FindShaderMethod(shaderUtil, "GetShaderMessages")
83
+ ?? FindShaderMethod(shaderUtil, "GetShaderErrors");
84
+ }
85
+
86
+ private static MethodInfo FindShaderMethod(Type shaderUtil, string name)
87
+ {
88
+ if (shaderUtil == null)
89
+ return null;
90
+
91
+ foreach (var method in shaderUtil.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
92
+ {
93
+ if (method.Name != name)
94
+ continue;
95
+ var parameters = method.GetParameters();
96
+ if (parameters.Length == 1 && parameters[0].ParameterType == typeof(Shader))
97
+ return method;
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ private static bool MatchesFilter(string shaderName, string path, string filter)
104
+ {
105
+ if (string.IsNullOrEmpty(filter))
106
+ return true;
107
+ return shaderName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0
108
+ || path.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
109
+ }
110
+
111
+ private static string ReadStringMember(object target, params string[] names)
112
+ {
113
+ foreach (var name in names)
114
+ {
115
+ var value = ReadMember(target, name);
116
+ if (value != null)
117
+ return value.ToString();
118
+ }
119
+
120
+ return string.Empty;
121
+ }
122
+
123
+ private static int ReadIntMember(object target, string name)
124
+ {
125
+ var value = ReadMember(target, name);
126
+ return value != null ? Convert.ToInt32(value) : 0;
127
+ }
128
+
129
+ private static bool ReadBoolMember(object target, params string[] names)
130
+ {
131
+ foreach (var name in names)
132
+ {
133
+ var value = ReadMember(target, name);
134
+ if (value != null)
135
+ return Convert.ToBoolean(value);
136
+ }
137
+
138
+ return false;
139
+ }
140
+
141
+ private static object ReadMember(object target, string name)
142
+ {
143
+ var type = target.GetType();
144
+ var field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
145
+ if (field != null)
146
+ return field.GetValue(target);
147
+ var property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
148
+ return property?.GetValue(target);
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 4cfbd1702c2741b48996b2de530bcb96