com.wallstop-studios.dxmessaging 2.2.0 → 3.1.0

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.
Files changed (270) hide show
  1. package/CHANGELOG.md +315 -67
  2. package/CHANGELOG.md.meta +7 -7
  3. package/Editor/Analyzers/BaseCallIlInspector.cs +277 -0
  4. package/Editor/Analyzers/BaseCallIlInspector.cs.meta +11 -0
  5. package/Editor/Analyzers/BaseCallLogMessageParser.cs +295 -0
  6. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +11 -0
  7. package/Editor/Analyzers/BaseCallReportAggregator.cs +308 -0
  8. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +11 -0
  9. package/Editor/Analyzers/BaseCallTypeScanner.cs +110 -0
  10. package/Editor/Analyzers/BaseCallTypeScanner.cs.meta +11 -0
  11. package/Editor/Analyzers/BaseCallTypeScannerCore.cs +562 -0
  12. package/Editor/Analyzers/BaseCallTypeScannerCore.cs.meta +11 -0
  13. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +1129 -0
  14. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +11 -0
  15. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +44 -44
  16. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +44 -44
  17. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +44 -44
  18. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +44 -44
  19. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +44 -44
  20. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  21. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +46 -0
  22. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  23. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +72 -72
  24. package/Editor/Analyzers.meta +8 -8
  25. package/Editor/AssemblyInfo.cs.meta +11 -3
  26. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +81 -0
  27. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +11 -0
  28. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +429 -0
  29. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +11 -0
  30. package/Editor/CustomEditors/MessagingComponentEditor.cs +1 -1
  31. package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +11 -3
  32. package/Editor/CustomEditors.meta +2 -2
  33. package/Editor/DxMessagingEditorIdle.cs +62 -0
  34. package/Editor/DxMessagingEditorIdle.cs.meta +11 -0
  35. package/Editor/DxMessagingEditorInitializer.cs +112 -15
  36. package/Editor/DxMessagingEditorInitializer.cs.meta +11 -3
  37. package/Editor/DxMessagingEditorLog.cs +32 -0
  38. package/Editor/DxMessagingEditorLog.cs.meta +11 -0
  39. package/Editor/DxMessagingMenu.cs.meta +11 -11
  40. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +11 -11
  41. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +313 -0
  42. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +11 -0
  43. package/Editor/Settings/DxMessagingSettings.cs +261 -11
  44. package/Editor/Settings/DxMessagingSettings.cs.meta +11 -3
  45. package/Editor/Settings/DxMessagingSettingsProvider.cs +50 -33
  46. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +11 -3
  47. package/Editor/Settings.meta +2 -2
  48. package/Editor/SetupCscRsp.cs +406 -39
  49. package/Editor/SetupCscRsp.cs.meta +11 -3
  50. package/Editor/Testing/MessagingComponentEditorHarness.cs +2 -2
  51. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +11 -3
  52. package/Editor/Testing.meta +3 -3
  53. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef +14 -14
  54. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef.meta +7 -7
  55. package/Editor.meta +8 -8
  56. package/LICENSE.md +9 -9
  57. package/LICENSE.md.meta +7 -7
  58. package/README.md +940 -900
  59. package/README.md.meta +7 -7
  60. package/Runtime/AssemblyInfo.cs +4 -0
  61. package/Runtime/AssemblyInfo.cs.meta +11 -3
  62. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +11 -3
  63. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +11 -3
  64. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs +26 -0
  65. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +11 -0
  66. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs +2 -4
  67. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +11 -3
  68. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +11 -3
  69. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +11 -3
  70. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs +56 -0
  71. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs.meta +11 -0
  72. package/Runtime/Core/Attributes.meta +2 -2
  73. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs +195 -0
  74. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs.meta +11 -0
  75. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs +179 -0
  76. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs.meta +11 -0
  77. package/Runtime/Core/Configuration.meta +9 -0
  78. package/Runtime/Core/DataStructure/CyclicBuffer.cs +46 -28
  79. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +11 -3
  80. package/Runtime/Core/DataStructure.meta +2 -2
  81. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +11 -3
  82. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +11 -3
  83. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +11 -3
  84. package/Runtime/Core/Diagnostics.meta +2 -2
  85. package/Runtime/Core/DxMessagingStaticState.cs +19 -0
  86. package/Runtime/Core/DxMessagingStaticState.cs.meta +11 -11
  87. package/Runtime/Core/Extensions/EnumExtensions.cs +6 -5
  88. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +11 -3
  89. package/Runtime/Core/Extensions/IListExtensions.cs.meta +11 -3
  90. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +12 -12
  91. package/Runtime/Core/Extensions/MessageExtensions.cs +0 -60
  92. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +11 -11
  93. package/Runtime/Core/Extensions.meta +8 -8
  94. package/Runtime/Core/Helper/MessageCache.cs +32 -0
  95. package/Runtime/Core/Helper/MessageCache.cs.meta +11 -3
  96. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +11 -3
  97. package/Runtime/Core/Helper.meta +2 -2
  98. package/Runtime/Core/IMessage.cs +3 -3
  99. package/Runtime/Core/IMessage.cs.meta +11 -11
  100. package/Runtime/Core/InstanceId.cs +25 -1
  101. package/Runtime/Core/InstanceId.cs.meta +11 -11
  102. package/Runtime/Core/Internal/DxUnsafe.cs +60 -0
  103. package/Runtime/Core/Internal/DxUnsafe.cs.meta +11 -0
  104. package/Runtime/Core/Internal/FlatDispatch.cs +198 -0
  105. package/Runtime/Core/Internal/FlatDispatch.cs.meta +11 -0
  106. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs +38 -0
  107. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs.meta +11 -0
  108. package/Runtime/Core/Internal/TypedSlotIndex.cs +81 -0
  109. package/Runtime/Core/Internal/TypedSlotIndex.cs.meta +11 -0
  110. package/Runtime/Core/Internal/TypedSlots.cs +597 -0
  111. package/Runtime/Core/Internal/TypedSlots.cs.meta +11 -0
  112. package/Runtime/Core/Internal.meta +9 -0
  113. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +11 -11
  114. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +11 -11
  115. package/Runtime/Core/MessageBus/IMessageBus.cs +189 -15
  116. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +11 -11
  117. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +11 -11
  118. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs +1 -0
  119. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +11 -11
  120. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs +16 -0
  121. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs.meta +11 -0
  122. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs +40 -0
  123. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs.meta +11 -0
  124. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +719 -0
  125. package/Runtime/Core/MessageBus/Internal/BusSlots.cs.meta +11 -0
  126. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs +38 -0
  127. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs.meta +11 -0
  128. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs +20 -0
  129. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs.meta +11 -0
  130. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs +28 -0
  131. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs.meta +11 -0
  132. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs +48 -0
  133. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs.meta +11 -0
  134. package/Runtime/Core/MessageBus/Internal/ISweepable.cs +15 -0
  135. package/Runtime/Core/MessageBus/Internal/ISweepable.cs.meta +11 -0
  136. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs +222 -0
  137. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs.meta +11 -0
  138. package/Runtime/Core/MessageBus/Internal/SlotKey.cs +192 -0
  139. package/Runtime/Core/MessageBus/Internal/SlotKey.cs.meta +11 -0
  140. package/Runtime/Core/MessageBus/Internal.meta +9 -0
  141. package/Runtime/Core/MessageBus/MessageBus.cs +5366 -3838
  142. package/Runtime/Core/MessageBus/MessageBus.cs.meta +11 -11
  143. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +11 -11
  144. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs +187 -14
  145. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +11 -11
  146. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +11 -11
  147. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +11 -11
  148. package/Runtime/Core/MessageBus.meta +8 -8
  149. package/Runtime/Core/MessageHandler.cs +2399 -1042
  150. package/Runtime/Core/MessageHandler.cs.meta +11 -11
  151. package/Runtime/Core/MessageRegistrationHandle.cs.meta +11 -11
  152. package/Runtime/Core/MessageRegistrationToken.cs +429 -44
  153. package/Runtime/Core/MessageRegistrationToken.cs.meta +11 -11
  154. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +11 -3
  155. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +11 -11
  156. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +11 -11
  157. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +11 -11
  158. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +11 -3
  159. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +11 -11
  160. package/Runtime/Core/Messages/StringMessage.cs.meta +11 -3
  161. package/Runtime/Core/Messages.meta +8 -8
  162. package/Runtime/Core/MessagingDebug.cs.meta +11 -11
  163. package/Runtime/Core/Pooling/CollectionPool.cs +266 -0
  164. package/Runtime/Core/Pooling/CollectionPool.cs.meta +11 -0
  165. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs +30 -0
  166. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs.meta +11 -0
  167. package/Runtime/Core/Pooling/DxPools.cs +157 -0
  168. package/Runtime/Core/Pooling/DxPools.cs.meta +11 -0
  169. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs +106 -0
  170. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs.meta +11 -0
  171. package/Runtime/Core/Pooling/IDxMessagingClock.cs +18 -0
  172. package/Runtime/Core/Pooling/IDxMessagingClock.cs.meta +11 -0
  173. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs +55 -0
  174. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs.meta +11 -0
  175. package/Runtime/Core/Pooling/StopwatchClock.cs +27 -0
  176. package/Runtime/Core/Pooling/StopwatchClock.cs.meta +11 -0
  177. package/Runtime/Core/Pooling/UnityRealtimeClock.cs +31 -0
  178. package/Runtime/Core/Pooling/UnityRealtimeClock.cs.meta +11 -0
  179. package/Runtime/Core/Pooling.meta +9 -0
  180. package/Runtime/Core.meta +8 -8
  181. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +12 -12
  182. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +11 -11
  183. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +12 -12
  184. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +11 -3
  185. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs +73 -0
  186. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +11 -11
  187. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef +20 -20
  188. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef.meta +7 -7
  189. package/Runtime/Unity/Integrations/Reflex.meta +8 -8
  190. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +11 -3
  191. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs +109 -1
  192. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +11 -11
  193. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef +30 -30
  194. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef.meta +7 -7
  195. package/Runtime/Unity/Integrations/VContainer.meta +8 -8
  196. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +11 -3
  197. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef +30 -30
  198. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef.meta +7 -7
  199. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs +79 -1
  200. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +11 -11
  201. package/Runtime/Unity/Integrations/Zenject.meta +8 -8
  202. package/Runtime/Unity/Integrations.meta +8 -8
  203. package/Runtime/Unity/MessageAwareComponent.cs +74 -0
  204. package/Runtime/Unity/MessageAwareComponent.cs.meta +11 -11
  205. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +12 -12
  206. package/Runtime/Unity/MessagingComponent.cs +43 -10
  207. package/Runtime/Unity/MessagingComponent.cs.meta +11 -11
  208. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +12 -12
  209. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +12 -12
  210. package/Runtime/Unity.meta +8 -8
  211. package/Runtime/WallstopStudios.DxMessaging.asmdef +14 -14
  212. package/Runtime/WallstopStudios.DxMessaging.asmdef.meta +7 -7
  213. package/Runtime.meta +8 -8
  214. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab +98 -98
  215. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab.meta +7 -7
  216. package/Samples~/DI/Prefabs.meta +8 -8
  217. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset +14 -14
  218. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset.meta +8 -8
  219. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset +14 -14
  220. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset.meta +8 -8
  221. package/Samples~/DI/Providers.meta +8 -8
  222. package/Samples~/DI/README.md +51 -51
  223. package/Samples~/DI/README.md.meta +7 -7
  224. package/Samples~/DI/Reflex/SampleInstaller.cs +7 -0
  225. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +11 -11
  226. package/Samples~/DI/Reflex.meta +8 -8
  227. package/Samples~/DI/VContainer/SampleLifetimeScope.cs +6 -1
  228. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +11 -11
  229. package/Samples~/DI/VContainer.meta +8 -8
  230. package/Samples~/DI/Zenject/SampleInstaller.cs +8 -0
  231. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +11 -11
  232. package/Samples~/DI/Zenject.meta +8 -8
  233. package/Samples~/DI.meta +8 -8
  234. package/Samples~/Mini Combat/Boot.cs.meta +11 -11
  235. package/Samples~/Mini Combat/Enemy.cs.meta +11 -11
  236. package/Samples~/Mini Combat/Messages.cs.meta +11 -11
  237. package/Samples~/Mini Combat/Player.cs.meta +11 -11
  238. package/Samples~/Mini Combat/README.md +324 -323
  239. package/Samples~/Mini Combat/README.md.meta +7 -7
  240. package/Samples~/Mini Combat/UIOverlay.cs.meta +11 -11
  241. package/Samples~/Mini Combat/Walkthrough.md +430 -430
  242. package/Samples~/Mini Combat/Walkthrough.md.meta +7 -7
  243. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef +13 -13
  244. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef.meta +7 -7
  245. package/Samples~/Mini Combat.meta +8 -8
  246. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +11 -11
  247. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +11 -11
  248. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +11 -11
  249. package/Samples~/UI Buttons + Inspector/README.md +210 -209
  250. package/Samples~/UI Buttons + Inspector/README.md.meta +7 -7
  251. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +11 -11
  252. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef +13 -13
  253. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef.meta +7 -7
  254. package/Samples~/UI Buttons + Inspector.meta +8 -8
  255. package/SourceGenerators/Directory.Build.props +50 -3
  256. package/SourceGenerators/Directory.Build.props.meta +7 -7
  257. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs +96 -63
  258. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +11 -11
  259. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +745 -87
  260. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +11 -3
  261. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +39 -46
  262. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj.meta +7 -7
  263. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.meta +8 -8
  264. package/SourceGenerators/global.json +7 -0
  265. package/SourceGenerators/global.json.meta +7 -0
  266. package/SourceGenerators.meta +8 -8
  267. package/Third Party Notices.md +3 -3
  268. package/Third Party Notices.md.meta +7 -7
  269. package/package.json +102 -92
  270. package/package.json.meta +7 -7
@@ -0,0 +1,1129 @@
1
+ namespace DxMessaging.Editor.Analyzers
2
+ {
3
+ #if UNITY_EDITOR
4
+ using System;
5
+ using System.Collections.Generic;
6
+ using System.Globalization;
7
+ using System.IO;
8
+ using System.Linq;
9
+ using System.Reflection;
10
+ using DxMessaging.Editor.Settings;
11
+ using UnityEditor;
12
+ using UnityEditor.Compilation;
13
+ using UnityEngine;
14
+
15
+ /// <summary>
16
+ /// Per-type entry recorded by the inspector overlay's data feed.
17
+ /// </summary>
18
+ /// <remarks>
19
+ /// This shape is the public contract that <c>MessageAwareComponentInspectorOverlay</c>
20
+ /// consumes; the field names are kept short and lower-cased so Unity's
21
+ /// <see cref="JsonUtility"/> serializer round-trips them cleanly through the JSON cache.
22
+ /// </remarks>
23
+ [Serializable]
24
+ public sealed class BaseCallReportEntry
25
+ {
26
+ /// <summary>Fully-qualified name of the offending type.</summary>
27
+ public string typeName;
28
+
29
+ /// <summary>Method names whose overrides are missing the corresponding <c>base.*()</c> call.</summary>
30
+ public List<string> missingBaseFor = new();
31
+
32
+ /// <summary>Diagnostic IDs that contributed to this entry (e.g., DXMSG006, DXMSG007, DXMSG009, DXMSG010).</summary>
33
+ /// <remarks>
34
+ /// Note: the IL-reflection scanner classifies DXMSG009 as DXMSG007 because the two are
35
+ /// indistinguishable at the IL level. The compile-time analyzer remains authoritative for
36
+ /// the precise ID classification -- see the analyzer reference docs and the inspector
37
+ /// integration section of <c>docs/reference/analyzers.md</c>. DXMSG008 (audit-marker for
38
+ /// opted-out types) is intentionally NOT included here: opted-out types are excluded from
39
+ /// the snapshot so the overlay's "Stop ignoring" path can reason about them via the
40
+ /// project ignore list directly.
41
+ /// </remarks>
42
+ public List<string> diagnosticIds = new();
43
+
44
+ /// <summary>Source file path (best-effort) for "Open Script" actions in the inspector overlay.</summary>
45
+ public string filePath;
46
+
47
+ /// <summary>1-based line number of the first relevant diagnostic, when known.</summary>
48
+ public int line;
49
+ }
50
+
51
+ [Serializable]
52
+ internal sealed class BaseCallReportFile
53
+ {
54
+ public int version = 1;
55
+ public string generatedAt;
56
+ public List<BaseCallReportEntry> types = new();
57
+ }
58
+
59
+ /// <summary>
60
+ /// Builds the per-FQN snapshot consumed by the inspector overlay from a deterministic IL
61
+ /// reflection scanner (<see cref="BaseCallTypeScanner"/>) -- and, optionally, a legacy
62
+ /// console-scrape bridge for users who want the union of both data sources.
63
+ /// </summary>
64
+ /// <remarks>
65
+ /// <para>
66
+ /// <b>Primary source (always-on): <see cref="BaseCallTypeScanner"/>.</b> Walks loaded
67
+ /// <c>MessageAwareComponent</c> subclasses via Unity's <c>TypeCache</c> and inspects each
68
+ /// override's IL body for the base-call shape. Deterministic across Unity 2021 cache hits,
69
+ /// incremental compiles, and arbitrary domain-reload sequences -- the only inputs are the
70
+ /// loaded assemblies in the AppDomain, which do not depend on Unity's compile-pipeline
71
+ /// state. Runs on every <see cref="AssemblyReloadEvents.afterAssemblyReload"/> and on every
72
+ /// <c>CompilationPipeline.assemblyCompilationFinished</c> burst (debounced via
73
+ /// <see cref="EditorApplication.delayCall"/>).
74
+ /// </para>
75
+ /// <para>
76
+ /// <b>Secondary source (opt-in): legacy console-scrape bridge.</b> When
77
+ /// <see cref="DxMessagingSettings.UseConsoleBridge"/> is <c>true</c>, the harvester ALSO
78
+ /// reads warnings from <c>UnityEditor.LogEntries</c> via reflection and from
79
+ /// <c>CompilationPipeline.assemblyCompilationFinished</c>'s per-assembly
80
+ /// <c>CompilerMessage[]</c> payloads. This path is non-deterministic on Unity 2021 (Bee/csc
81
+ /// cache hits cause Unity to skip surfacing analyzer warnings to either store) and is the
82
+ /// reason the IL-reflection scanner exists. Default off; available for users who want the
83
+ /// union of both data sources.
84
+ /// </para>
85
+ /// <para>
86
+ /// The inspector overlay reads its snapshot from the unified per-FQN map populated here on
87
+ /// every rescan. Use the menu <c>Tools > DxMessaging > Rescan Base-Call Warnings</c> for a
88
+ /// manual force-rescan.
89
+ /// </para>
90
+ /// <para>
91
+ /// <see cref="IsAvailable"/> stays <c>true</c> as long as the static constructor itself does
92
+ /// not throw -- the IL scanner is always wired, so the overlay never falls back to its
93
+ /// degraded "harvester unavailable" HelpBox in normal operation. <see cref="LogEntriesAvailable"/>
94
+ /// continues to report whether the legacy reflection layer is bindable, for diagnostics only.
95
+ /// </para>
96
+ /// </remarks>
97
+ [InitializeOnLoad]
98
+ public static class DxMessagingConsoleHarvester
99
+ {
100
+ private const string ReportFileName = "baseCallReport.json";
101
+ private const string ReportDirectoryName = "DxMessaging";
102
+ private const double PollIntervalSeconds = 0.25;
103
+
104
+ // N1: cap the per-rescan list capacity so a 100k-warning console doesn't allocate a
105
+ // pathologically large initial backing array. The list still grows freely if the console
106
+ // really does hold more entries than this, but the OOM-edge becomes a non-issue.
107
+ private const int MaxLineListInitialCapacity = 1024;
108
+
109
+ private static readonly Dictionary<string, BaseCallReportEntry> SnapshotInternal = new(
110
+ StringComparer.Ordinal
111
+ );
112
+
113
+ // Per-assembly attribution for the LEGACY CompilationPipeline.assemblyCompilationFinished
114
+ // feed (only consulted when DxMessagingSettings.UseConsoleBridge is true). When a
115
+ // recompile no longer reports a previously-seen type (because the user fixed the missing
116
+ // base call), we drop it from the bridge merged view. Without per-assembly tracking we'd
117
+ // never know which entries to retire.
118
+ //
119
+ // Lifecycle: writes happen inside _compilationFeedLock from
120
+ // OnAssemblyCompilationFinished. Reads happen on the editor main thread inside RescanNow,
121
+ // also under the lock to flush + clear the channel atomically.
122
+ //
123
+ // The merge + retirement bookkeeping for these maps lives in
124
+ // <see cref="BaseCallReportAggregator"/> as a pure helper so it can be tested via
125
+ // dotnet-test (the harvester itself is Unity-only and cannot be loaded outside the
126
+ // editor). Mutations to _typesByAssembly and _compilationMerged go through that helper
127
+ // exclusively to keep the test surface and runtime behaviour identical.
128
+ //
129
+ // Note: starting in v2.3, the IL-reflection scanner (BaseCallTypeScanner) is the primary
130
+ // source of truth; it runs unconditionally on every rescan, regardless of bridge state.
131
+ // The bridge only contributes ADDITIONAL data, never overrides the scanner.
132
+ private static readonly Dictionary<string, HashSet<string>> _typesByAssembly = new(
133
+ StringComparer.OrdinalIgnoreCase
134
+ );
135
+
136
+ // Per-FQN merged view of every assembly's latest reports, kept in sync with
137
+ // _typesByAssembly by BaseCallReportAggregator.ApplyAssemblyReports. The final inspector
138
+ // snapshot is built by unioning this with the LogEntries-derived report.
139
+ private static readonly Dictionary<string, ParsedTypeReport> _compilationMerged = new(
140
+ StringComparer.Ordinal
141
+ );
142
+
143
+ private static readonly HashSet<string> AlreadyWarned = new(StringComparer.Ordinal);
144
+
145
+ // Lock guarding all reads/writes to _typesByAssembly and the parsed-message buffer that
146
+ // flows from OnAssemblyCompilationFinished (worker thread for the parse) into
147
+ // DrainScheduledRescan (editor main thread for snapshot integration). Unity can fire
148
+ // assemblyCompilationFinished from a non-main thread on some Editor versions; the rest
149
+ // of the harvester (LogEntries reflection, AssetDatabase, persistence) is main-thread
150
+ // only and uses simple read order, so the lock is scoped to the cross-thread channel.
151
+ private static readonly object _compilationFeedLock = new();
152
+
153
+ // Drained by DrainScheduledRescan on the next editor tick. Holds the union of all
154
+ // CompilerMessage payloads captured since the last drain, attributed to their source
155
+ // assembly so we can retire entries that the user has fixed.
156
+ private static readonly Dictionary<
157
+ string,
158
+ Dictionary<string, ParsedTypeReport>
159
+ > _pendingByAssembly = new(StringComparer.OrdinalIgnoreCase);
160
+
161
+ // True when the LogEntries reflection layer failed to bind. The harvester remains
162
+ // available via the CompilerMessage path; this flag just gates the LogEntries-specific
163
+ // code paths (Tick polling, RescanNow's reflection call). Renamed from `_disabled` so
164
+ // the name reflects what it actually means.
165
+ private static readonly bool _logEntriesDisabled;
166
+
167
+ // Reflection handles. Resolved once in the static ctor; null when the running Unity version
168
+ // does not expose the expected LogEntries shape.
169
+ private static readonly Type _logEntryType;
170
+ private static readonly MethodInfo _startGettingEntries;
171
+ private static readonly MethodInfo _endGettingEntries;
172
+ private static readonly MethodInfo _getEntryInternal;
173
+ private static readonly MethodInfo _getCount;
174
+ private static readonly FieldInfo _messageField;
175
+
176
+ private static double _lastTickTime;
177
+ private static int _lastSeenCount;
178
+
179
+ // Latch flipped on by `OnAssemblyCompilationFinished` to coalesce the burst of one-event-
180
+ // per-assembly callbacks Unity fires during a build. We schedule a single deferred
181
+ // RescanNow via `EditorApplication.delayCall` (DrainScheduledRescan) and clear the latch
182
+ // when that callback runs. Without this debounce, a 30-assembly project would queue 30
183
+ // RescanNow invocations during the very window when the editor is most fragile.
184
+ private static volatile bool _rescanScheduled;
185
+
186
+ // Tracks whether the current snapshot has been refreshed by a scan in THIS Editor session,
187
+ // or whether it was loaded eagerly from `Library/DxMessaging/baseCallReport.json` in the
188
+ // static ctor and has not yet been overwritten. The inspector overlay reads this to
189
+ // distinguish "fresh-this-session" warnings from cached-from-previous-session warnings;
190
+ // when the cache is showing, we annotate the HelpBox with a small suffix so the user
191
+ // understands the data may be stale until the first post-reload scan completes.
192
+ //
193
+ // Default `false`: the static ctor's `LoadFromDisk` runs first, so by the time anything
194
+ // observes the snapshot, either (a) the cache populated entries that pre-date this session,
195
+ // or (b) the cache was empty (truly fresh). In case (b) the overlay renders no warning
196
+ // anyway; there are no entries to annotate; so the false default is correct for both.
197
+ // Flipped to `true` after the first successful `RescanNow` post-startup; never flipped
198
+ // back to `false`. Volatile so the editor-loop reader sees the write without a memory
199
+ // barrier on Unity's pre-2022 mono runtime.
200
+ private static volatile bool _isFreshThisSession;
201
+
202
+ /// <summary>
203
+ /// Direct read of the latest console-derived report by FQN. Returns <c>true</c> if an
204
+ /// entry exists for the given fully-qualified type name. The <paramref name="entry"/>
205
+ /// reference points at the live snapshot row -- callers must not mutate it.
206
+ /// </summary>
207
+ /// <remarks>
208
+ /// All mutation happens on the main thread inside <see cref="RescanNow"/>; the inspector
209
+ /// overlay (also main thread) reads via this method one-call-per-frame-per-component, so
210
+ /// there is no race that would justify the per-access defensive copy that
211
+ /// <see cref="Snapshot"/> performs. Prefer this method in hot paths.
212
+ /// </remarks>
213
+ public static bool TryGetEntry(string fullyQualifiedTypeName, out BaseCallReportEntry entry)
214
+ {
215
+ if (string.IsNullOrEmpty(fullyQualifiedTypeName))
216
+ {
217
+ entry = null;
218
+ return false;
219
+ }
220
+ return SnapshotInternal.TryGetValue(fullyQualifiedTypeName, out entry);
221
+ }
222
+
223
+ /// <summary>
224
+ /// Read-only snapshot of the latest console-derived report, keyed by FQN.
225
+ /// </summary>
226
+ /// <remarks>
227
+ /// Each access returns a fresh dictionary copy. Prefer <see cref="TryGetEntry"/> in hot
228
+ /// paths (the inspector overlay) -- this property exists for callers that need to enumerate
229
+ /// the full snapshot.
230
+ /// </remarks>
231
+ public static IReadOnlyDictionary<string, BaseCallReportEntry> Snapshot =>
232
+ new Dictionary<string, BaseCallReportEntry>(SnapshotInternal, StringComparer.Ordinal);
233
+
234
+ /// <summary>
235
+ /// <c>true</c> as long as the harvester has at least one functioning data source.
236
+ /// </summary>
237
+ /// <remarks>
238
+ /// The <c>CompilationPipeline.assemblyCompilationFinished</c> feed is wired
239
+ /// unconditionally on every supported Unity version, so this property is effectively
240
+ /// always <c>true</c> in normal operation; it only flips to <c>false</c> when the static
241
+ /// constructor itself throws (a hard initialization failure). The LogEntries reflection
242
+ /// layer is the optional source -- see <see cref="LogEntriesAvailable"/> for that flag.
243
+ /// The inspector overlay reads this property to decide whether to render its degraded
244
+ /// HelpBox, so the contract here is "should the overlay attempt to render at all".
245
+ /// </remarks>
246
+ public static bool IsAvailable { get; private set; } = true;
247
+
248
+ /// <summary>
249
+ /// <c>true</c> when the legacy <c>UnityEditor.LogEntries</c> reflection layer resolved
250
+ /// successfully on this Unity version. The harvester does not require this to be true to
251
+ /// function -- Unity 2021's analyzer warnings flow through the CompilerMessage feed
252
+ /// instead. Exposed primarily for diagnostics / tests.
253
+ /// </summary>
254
+ public static bool LogEntriesAvailable => !_logEntriesDisabled;
255
+
256
+ /// <summary>
257
+ /// <c>true</c> once the first <see cref="RescanNow"/> of this Editor session has produced
258
+ /// a fresh snapshot; <c>false</c> while the inspector is still showing the on-disk cache
259
+ /// loaded eagerly by the static constructor.
260
+ /// </summary>
261
+ /// <remarks>
262
+ /// The inspector overlay reads this to annotate its HelpBox: when <c>false</c> AND a
263
+ /// warning is being shown, the overlay appends a "(cached from previous session --
264
+ /// refreshing...)" suffix so the user knows the data is from yesterday's scan and a fresh
265
+ /// one is in flight. The flag is set inside <see cref="RescanNow"/> and never reset, so
266
+ /// the suffix disappears as soon as the first post-reload scan lands and stays gone for
267
+ /// the rest of the session.
268
+ /// </remarks>
269
+ public static bool IsFreshThisSession => _isFreshThisSession;
270
+
271
+ /// <summary>Raised whenever the snapshot changes (post-compile, post-domain-reload, or polled console-count change).</summary>
272
+ public static event Action ReportUpdated;
273
+
274
+ static DxMessagingConsoleHarvester()
275
+ {
276
+ try
277
+ {
278
+ Type logEntriesType =
279
+ Type.GetType("UnityEditor.LogEntries,UnityEditor.dll")
280
+ // S9: legacy / future-Unity probe. UnityEditorInternal.LogEntries doesn't
281
+ // exist today, but documenting the fallback as a one-liner keeps us forward-
282
+ // compatible at zero cost.
283
+ ?? Type.GetType("UnityEditorInternal.LogEntries,UnityEditor.dll");
284
+ _logEntryType =
285
+ Type.GetType("UnityEditor.LogEntry,UnityEditor.dll")
286
+ ?? Type.GetType("UnityEditorInternal.LogEntry,UnityEditor.dll");
287
+
288
+ bool logEntriesBound = false;
289
+ if (logEntriesType is not null && _logEntryType is not null)
290
+ {
291
+ if (_logEntryType.IsValueType)
292
+ {
293
+ // S8: defensive value-type guard. If a future Unity version makes LogEntry
294
+ // a struct, Activator.CreateInstance would hand us a boxed copy and the
295
+ // GetEntry call would mutate that copy in-place; harvest would silently
296
+ // report empty. Disable the LogEntries path rather than silently producing
297
+ // a wrong result; the CompilerMessage feed still runs.
298
+ LogOnce(
299
+ "logentry-is-struct",
300
+ "LogEntry is a value type on this Unity version; LogEntries scanning disabled. Falling back to the CompilerMessage feed."
301
+ );
302
+ }
303
+ else
304
+ {
305
+ _startGettingEntries = SafeGetStaticMethod(
306
+ logEntriesType,
307
+ "StartGettingEntries"
308
+ );
309
+ _endGettingEntries = SafeGetStaticMethod(
310
+ logEntriesType,
311
+ "EndGettingEntries"
312
+ );
313
+ _getEntryInternal = SafeGetStaticMethod(logEntriesType, "GetEntryInternal");
314
+ _getCount = SafeGetStaticMethod(logEntriesType, "GetCount");
315
+ _messageField = SafeGetInstanceField(_logEntryType, "message");
316
+
317
+ logEntriesBound =
318
+ _startGettingEntries is not null
319
+ && _endGettingEntries is not null
320
+ && _getEntryInternal is not null
321
+ && _getCount is not null
322
+ && _messageField is not null;
323
+ }
324
+ }
325
+
326
+ if (!logEntriesBound)
327
+ {
328
+ // The LogEntries reflection layer is unavailable. The IL-reflection scanner
329
+ // is the primary data source so this is no longer a critical path; the
330
+ // log-once is kept for diagnostic purposes (and only matters when the user
331
+ // has enabled the legacy bridge via DxMessagingSettings.UseConsoleBridge).
332
+ LogOnce(
333
+ "reflection-fallback",
334
+ "LogEntries reflection unavailable on this Unity version. The IL-reflection "
335
+ + "scanner remains the primary data source; the legacy console-scrape bridge "
336
+ + "(opt-in via DxMessagingSettings.UseConsoleBridge) cannot read LogEntries on this version."
337
+ );
338
+ _logEntriesDisabled = true;
339
+ }
340
+
341
+ LoadFromDisk();
342
+
343
+ // AssetDatabase isn't fully ready inside the static ctor; defer the first scan
344
+ // until Unity is idle so settings load doesn't fight a transitional asset-import
345
+ // state.
346
+ ScheduleRescanWhenIdle();
347
+ AssemblyReloadEvents.afterAssemblyReload += ScheduleRescanWhenIdle;
348
+ CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
349
+ if (!_logEntriesDisabled)
350
+ {
351
+ EditorApplication.update += Tick;
352
+ }
353
+ }
354
+ catch (Exception ex)
355
+ {
356
+ DxMessagingEditorLog.LogWarning(
357
+ "DxMessagingConsoleHarvester failed to initialize.",
358
+ ex
359
+ );
360
+ _logEntriesDisabled = true;
361
+ IsAvailable = false;
362
+ }
363
+ }
364
+
365
+ /// <summary>
366
+ /// Force a re-read of the editor console and drain any pending CompilerMessage payloads.
367
+ /// Called automatically on domain reload and compilation events; the menu entry exposes it
368
+ /// for manual invocation. Settings setters (e.g.
369
+ /// <see cref="DxMessagingSettings.BaseCallCheckEnabled"/>) call this via
370
+ /// <see cref="EditorApplication.delayCall"/> so a re-enable repopulates the snapshot
371
+ /// without waiting for the next polled tick.
372
+ /// </summary>
373
+ [MenuItem("Tools/Wallstop Studios/DxMessaging/Rescan Base-Call Warnings")]
374
+ public static void RescanNow()
375
+ {
376
+ if (!IsAvailable)
377
+ {
378
+ return;
379
+ }
380
+
381
+ // Critical: NEVER touch LogEntries reflection or AssetDatabase while Unity is mid-
382
+ // compile or mid-asset-update. Reading LogEntries during compilation contends with the
383
+ // compiler's own log-buffer lock and can deadlock the editor. Touching AssetDatabase
384
+ // (via TryLoadSettings → GetOrCreateSettings → CreateAsset) during compilation
385
+ // schedules an import that re-triggers compilation; an infinite-loop trap that
386
+ // permanently freezes script-compilation startup. Defer to the post-compile state
387
+ // and let the polled tick (or the explicit afterAssemblyReload hook) pick it up.
388
+ if (EditorApplication.isCompiling || EditorApplication.isUpdating)
389
+ {
390
+ return;
391
+ }
392
+
393
+ DxMessagingSettings settings = TryLoadSettings();
394
+ if (settings != null && !settings._baseCallCheckEnabled)
395
+ {
396
+ bool wasNonEmpty = SnapshotInternal.Count > 0;
397
+ SnapshotInternal.Clear();
398
+ // S3: keep the per-assembly bookkeeping (_typesByAssembly + _compilationMerged)
399
+ // in lock-step. Clearing only one half leaves stale rows that the next
400
+ // ApplyAssemblyReports call would silently re-promote into the snapshot when the
401
+ // user toggles the master switch back on without an intervening recompile.
402
+ _typesByAssembly.Clear();
403
+ _compilationMerged.Clear();
404
+ lock (_compilationFeedLock)
405
+ {
406
+ _pendingByAssembly.Clear();
407
+ }
408
+ _lastSeenCount = 0;
409
+ PersistToDisk();
410
+ // The "check disabled" path still represents a successful session-time decision
411
+ // about the snapshot; flip the freshness flag so the overlay never lingers in
412
+ // "cached from previous session" mode after the user has explicitly silenced the
413
+ // check. Doing this BEFORE RaiseReportUpdated mirrors the main path's ordering.
414
+ _isFreshThisSession = true;
415
+ if (wasNonEmpty)
416
+ {
417
+ RaiseReportUpdated();
418
+ }
419
+ return;
420
+ }
421
+
422
+ // -- Primary source (always-on): IL-reflection scanner over loaded
423
+ // MessageAwareComponent subclasses. Deterministic across Unity 2021 cache hits and
424
+ // incremental compiles; replaces the lossy console-scrape harvester as the
425
+ // inspector overlay's source of truth.
426
+ Dictionary<string, BaseCallReportEntry> scannerEntries;
427
+ try
428
+ {
429
+ scannerEntries = BaseCallTypeScanner.Scan(settings);
430
+ }
431
+ catch (Exception ex)
432
+ {
433
+ LogExceptionOnce("scanner", "BaseCallTypeScanner.Scan threw.", ex);
434
+ scannerEntries = new Dictionary<string, BaseCallReportEntry>(
435
+ StringComparer.Ordinal
436
+ );
437
+ }
438
+
439
+ // The scanner produces a complete view of all loaded subclasses on every call, so it
440
+ // fully replaces the snapshot. Build the new map up-front from the scanner's output;
441
+ // we'll union the legacy-bridge entries into it below if the user opted in.
442
+ Dictionary<string, BaseCallReportEntry> nextSnapshot = new(
443
+ scannerEntries,
444
+ StringComparer.Ordinal
445
+ );
446
+
447
+ bool useBridge = settings != null && settings._useConsoleBridge;
448
+
449
+ int currentCount = 0;
450
+ bool logEntriesHarvested = false;
451
+ if (useBridge)
452
+ {
453
+ // -- Secondary source (opt-in): LogEntries reflection (Unity 2022+ reliable path).
454
+ Dictionary<string, ParsedTypeReport> logEntriesAggregate = HarvestFromLogEntries(
455
+ out currentCount,
456
+ out logEntriesHarvested
457
+ );
458
+
459
+ // -- Secondary source (opt-in): pending CompilerMessage payloads (Unity 2021's
460
+ // primary path under the legacy bridge). Drain the cross-thread channel
461
+ // atomically.
462
+ Dictionary<string, Dictionary<string, ParsedTypeReport>> drained;
463
+ lock (_compilationFeedLock)
464
+ {
465
+ if (_pendingByAssembly.Count == 0)
466
+ {
467
+ drained = null;
468
+ }
469
+ else
470
+ {
471
+ drained = new Dictionary<string, Dictionary<string, ParsedTypeReport>>(
472
+ _pendingByAssembly,
473
+ StringComparer.OrdinalIgnoreCase
474
+ );
475
+ _pendingByAssembly.Clear();
476
+ }
477
+ }
478
+
479
+ ApplyCompilerMessageDrain(drained);
480
+
481
+ // Merge the bridge view (LogEntries + CompilerMessage) and union it INTO the
482
+ // scanner-produced snapshot. The scanner is authoritative; the bridge can only
483
+ // ADD methods/diagnostic ids it sees that the scanner missed (e.g. exotic IL
484
+ // shapes the byte walker stepped past). Bridge entries never override the
485
+ // scanner's classification.
486
+ try
487
+ {
488
+ Dictionary<string, BaseCallReportEntryDto> bridgeSnapshot =
489
+ BaseCallReportAggregator.BuildSnapshot(
490
+ logEntriesHarvested ? logEntriesAggregate : null,
491
+ _compilationMerged
492
+ );
493
+ UnionBridgeIntoSnapshot(bridgeSnapshot, nextSnapshot);
494
+ }
495
+ catch (Exception ex)
496
+ {
497
+ LogExceptionOnce("aggregate", "Snapshot merge failed.", ex);
498
+ // Fall through with the scanner-only snapshot; partial data is better than
499
+ // wiping the snapshot when the bridge half misbehaves.
500
+ }
501
+ }
502
+ else
503
+ {
504
+ // Bridge is disabled: drop any pending CompilerMessage entries the harvester may
505
+ // have buffered (they would otherwise leak into the snapshot the next time the
506
+ // user toggles the bridge on). The bridge bookkeeping is reset below as well.
507
+ lock (_compilationFeedLock)
508
+ {
509
+ _pendingByAssembly.Clear();
510
+ }
511
+ _typesByAssembly.Clear();
512
+ _compilationMerged.Clear();
513
+ }
514
+
515
+ // Replace the live snapshot with the new view in one swap. The scanner runs over ALL
516
+ // loaded types every time, so this is a full-replace; types the user has fixed since
517
+ // the last scan disappear, types newly broken appear.
518
+ SnapshotInternal.Clear();
519
+ foreach (KeyValuePair<string, BaseCallReportEntry> kvp in nextSnapshot)
520
+ {
521
+ SnapshotInternal[kvp.Key] = kvp.Value;
522
+ }
523
+
524
+ if (useBridge && logEntriesHarvested)
525
+ {
526
+ _lastSeenCount = currentCount;
527
+ }
528
+ PersistToDisk();
529
+ // Mark the snapshot as session-fresh AFTER the persist + before the event fires, so
530
+ // that any subscriber repainting the inspector observes the same "fresh" state the
531
+ // overlay will see on its next read. Subsequent scans are no-ops on this flag.
532
+ _isFreshThisSession = true;
533
+ RaiseReportUpdated();
534
+ }
535
+
536
+ // Unions the bridge-produced DTOs into the scanner-produced snapshot. The scanner is the
537
+ // authoritative source; the bridge can only contribute methods / diagnostic ids the
538
+ // scanner missed for a type, OR a brand-new type entry the scanner did not produce (e.g.
539
+ // a subclass the scanner couldn't classify because its IL was stripped). The first non-
540
+ // empty file path / line wins, matching the bridge's pre-existing semantics.
541
+ private static void UnionBridgeIntoSnapshot(
542
+ Dictionary<string, BaseCallReportEntryDto> bridgeSnapshot,
543
+ Dictionary<string, BaseCallReportEntry> scannerSnapshot
544
+ )
545
+ {
546
+ if (bridgeSnapshot is null || bridgeSnapshot.Count == 0)
547
+ {
548
+ return;
549
+ }
550
+ foreach (KeyValuePair<string, BaseCallReportEntryDto> kvp in bridgeSnapshot)
551
+ {
552
+ BaseCallReportEntryDto dto = kvp.Value;
553
+ if (dto is null || string.IsNullOrEmpty(dto.TypeName))
554
+ {
555
+ continue;
556
+ }
557
+ if (!scannerSnapshot.TryGetValue(dto.TypeName, out BaseCallReportEntry existing))
558
+ {
559
+ existing = new BaseCallReportEntry
560
+ {
561
+ typeName = dto.TypeName,
562
+ missingBaseFor = new List<string>(dto.MissingBaseFor),
563
+ diagnosticIds = dto.DiagnosticIds.ToList(),
564
+ filePath = dto.FilePath ?? string.Empty,
565
+ line = dto.Line,
566
+ };
567
+ scannerSnapshot[dto.TypeName] = existing;
568
+ continue;
569
+ }
570
+ foreach (string method in dto.MissingBaseFor)
571
+ {
572
+ if (
573
+ !string.IsNullOrEmpty(method)
574
+ && !existing.missingBaseFor.Contains(method, StringComparer.Ordinal)
575
+ )
576
+ {
577
+ existing.missingBaseFor.Add(method);
578
+ }
579
+ }
580
+ foreach (string id in dto.DiagnosticIds)
581
+ {
582
+ if (
583
+ !string.IsNullOrEmpty(id)
584
+ && !existing.diagnosticIds.Contains(id, StringComparer.Ordinal)
585
+ )
586
+ {
587
+ existing.diagnosticIds.Add(id);
588
+ }
589
+ }
590
+ if (string.IsNullOrEmpty(existing.filePath) && !string.IsNullOrEmpty(dto.FilePath))
591
+ {
592
+ existing.filePath = dto.FilePath;
593
+ existing.line = dto.Line;
594
+ }
595
+ }
596
+ }
597
+
598
+ // Reads the editor console via LogEntries reflection. Returns the aggregated per-type
599
+ // report, the current console count, and whether the harvest actually ran (false when
600
+ // the LogEntries reflection layer is unavailable or threw). On Unity 2021 this returns
601
+ // an empty aggregate every time; the analyzer warnings flow through the CompilerMessage
602
+ // feed instead and arrive via ApplyCompilerMessageDrain.
603
+ private static Dictionary<string, ParsedTypeReport> HarvestFromLogEntries(
604
+ out int currentCount,
605
+ out bool harvested
606
+ )
607
+ {
608
+ currentCount = 0;
609
+ harvested = false;
610
+ if (_logEntriesDisabled)
611
+ {
612
+ return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
613
+ }
614
+
615
+ try
616
+ {
617
+ currentCount = (int)_getCount.Invoke(null, null);
618
+ }
619
+ catch (Exception ex)
620
+ {
621
+ LogExceptionOnce("getcount", "GetCount invocation failed.", ex);
622
+ return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
623
+ }
624
+
625
+ // S4: console-clear handling. We always overwrite _lastSeenCount near the bottom of
626
+ // RescanNow, so the only point of acting on a shrunken count here is to be explicit
627
+ // about the semantic. The accumulator is rebuilt from scratch every rescan, so the
628
+ // clear case is naturally consistent; even an empty log produces an empty aggregate
629
+ // and a ReportUpdated fire that drops stale rows.
630
+
631
+ // B2 + S6: enter the get/end pair only AFTER StartGettingEntries actually succeeded.
632
+ try
633
+ {
634
+ _startGettingEntries.Invoke(null, null);
635
+ }
636
+ catch (Exception ex)
637
+ {
638
+ LogExceptionOnce("start", "StartGettingEntries invocation failed.", ex);
639
+ return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
640
+ }
641
+
642
+ // N1: clamp the initial capacity to a sane ceiling. The list is allowed to grow past
643
+ // this if the console really does hold more entries; we just don't blow up the heap on
644
+ // first allocation.
645
+ List<string> lines = new(Math.Min(currentCount, MaxLineListInitialCapacity));
646
+ int harvestedCount = currentCount;
647
+ try
648
+ {
649
+ if (_startGettingEntries.ReturnType == typeof(int))
650
+ {
651
+ // The Invoke return value is intentionally discarded; we re-pull via GetCount
652
+ // because the polled count is authoritative.
653
+ try
654
+ {
655
+ harvestedCount = (int)_getCount.Invoke(null, null);
656
+ }
657
+ catch (Exception ex)
658
+ {
659
+ LogExceptionOnce(
660
+ "getcount-after-start",
661
+ "GetCount after StartGettingEntries failed.",
662
+ ex
663
+ );
664
+ // Retain the previous count.
665
+ }
666
+ }
667
+
668
+ object entryInstance = Activator.CreateInstance(_logEntryType);
669
+ object[] invokeArgs = new object[2];
670
+ invokeArgs[1] = entryInstance;
671
+ for (int j = 0; j < harvestedCount; j++)
672
+ {
673
+ invokeArgs[0] = j;
674
+ try
675
+ {
676
+ _getEntryInternal.Invoke(null, invokeArgs);
677
+ }
678
+ catch (Exception ex)
679
+ {
680
+ LogExceptionOnce(
681
+ "getentry",
682
+ $"GetEntryInternal invocation failed at index {j}.",
683
+ ex
684
+ );
685
+ continue;
686
+ }
687
+
688
+ string message;
689
+ try
690
+ {
691
+ message = _messageField.GetValue(entryInstance) as string;
692
+ }
693
+ catch (Exception ex)
694
+ {
695
+ LogExceptionOnce(
696
+ "getmessage",
697
+ $"LogEntry.message read failed at index {j}.",
698
+ ex
699
+ );
700
+ continue;
701
+ }
702
+
703
+ if (!string.IsNullOrEmpty(message))
704
+ {
705
+ lines.Add(message);
706
+ }
707
+ }
708
+ }
709
+ catch (Exception ex)
710
+ {
711
+ LogExceptionOnce("harvest", "Harvest loop failed.", ex);
712
+ }
713
+ finally
714
+ {
715
+ try
716
+ {
717
+ _endGettingEntries.Invoke(null, null);
718
+ }
719
+ catch (Exception ex)
720
+ {
721
+ LogExceptionOnce("end", "EndGettingEntries invocation failed.", ex);
722
+ }
723
+ }
724
+
725
+ harvested = true;
726
+ try
727
+ {
728
+ return BaseCallLogMessageParser.Aggregate(lines);
729
+ }
730
+ catch (Exception ex)
731
+ {
732
+ LogExceptionOnce(
733
+ "aggregate-logentries",
734
+ "Aggregating LogEntries lines failed.",
735
+ ex
736
+ );
737
+ return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
738
+ }
739
+ }
740
+
741
+ // Folds a freshly-drained per-assembly batch into the long-lived per-assembly bookkeeping
742
+ // via <see cref="BaseCallReportAggregator.ApplyAssemblyReports"/>. The aggregator owns the
743
+ // retirement logic (a type the user fixed disappears as soon as the assembly recompiles
744
+ // without re-reporting it) and the cross-assembly survival rule (a type stays in the
745
+ // merged view as long as ANY assembly still reports it).
746
+ private static void ApplyCompilerMessageDrain(
747
+ Dictionary<string, Dictionary<string, ParsedTypeReport>> drained
748
+ )
749
+ {
750
+ if (drained is null || drained.Count == 0)
751
+ {
752
+ return;
753
+ }
754
+
755
+ foreach (KeyValuePair<string, Dictionary<string, ParsedTypeReport>> kvp in drained)
756
+ {
757
+ BaseCallReportAggregator.ApplyAssemblyReports(
758
+ kvp.Key,
759
+ kvp.Value ?? new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal),
760
+ _typesByAssembly,
761
+ _compilationMerged
762
+ );
763
+ }
764
+ }
765
+
766
+ /// <summary>
767
+ /// Hint the harvester that something external changed (e.g., a settings toggle) and the
768
+ /// next polled tick should treat the console as fresh. Cheaper than a synchronous
769
+ /// <see cref="RescanNow"/> when the caller is on a thread / context that may not be safe
770
+ /// to do reflection from.
771
+ /// </summary>
772
+ public static void RequestRescan()
773
+ {
774
+ if (!IsAvailable)
775
+ {
776
+ return;
777
+ }
778
+ // Setting _lastSeenCount to a sentinel forces the next Tick to see a count delta and
779
+ // call RescanNow on the editor's update thread (when LogEntries is wired). When
780
+ // LogEntries is unavailable, Tick is not registered, so we fall back to delayCall.
781
+ if (_logEntriesDisabled)
782
+ {
783
+ ScheduleRescanWhenIdle();
784
+ return;
785
+ }
786
+ _lastSeenCount = -1;
787
+ }
788
+
789
+ private static void Tick()
790
+ {
791
+ // Tick is only registered when the LogEntries reflection layer is available, so we
792
+ // do NOT need to re-check _logEntriesDisabled here; but the IsAvailable guard
793
+ // protects against a future failure mode where IsAvailable is flipped to false at
794
+ // runtime.
795
+ if (!IsAvailable)
796
+ {
797
+ return;
798
+ }
799
+
800
+ // Defensive belt: never reflect into LogEntries while a compile or asset-import is
801
+ // running. Even though RescanNow() itself bails on this state, we don't want to even
802
+ // call GetCount(); the lock contention is the source of the freeze, and GetCount
803
+ // touches the same buffer.
804
+ if (EditorApplication.isCompiling || EditorApplication.isUpdating)
805
+ {
806
+ return;
807
+ }
808
+
809
+ try
810
+ {
811
+ double now = EditorApplication.timeSinceStartup;
812
+ if (now - _lastTickTime < PollIntervalSeconds)
813
+ {
814
+ return;
815
+ }
816
+ _lastTickTime = now;
817
+
818
+ int currentCount;
819
+ try
820
+ {
821
+ currentCount = (int)_getCount.Invoke(null, null);
822
+ }
823
+ catch (Exception ex)
824
+ {
825
+ LogExceptionOnce("tick-count", "GetCount during Tick failed.", ex);
826
+ return;
827
+ }
828
+
829
+ if (currentCount != _lastSeenCount)
830
+ {
831
+ RescanNow();
832
+ }
833
+ }
834
+ catch (Exception ex)
835
+ {
836
+ LogExceptionOnce("tick", "Tick failed.", ex);
837
+ }
838
+ }
839
+
840
+ private static void OnAssemblyCompilationFinished(
841
+ string assemblyPath,
842
+ CompilerMessage[] messages
843
+ )
844
+ {
845
+ // CRITICAL: this fires for EVERY assembly compiled (10s of times per build). Running
846
+ // RescanNow synchronously here invokes LogEntries reflection while OTHER assemblies
847
+ // are still compiling; the compiler holds its log-buffer lock and our reflection
848
+ // call blocks waiting for it. Combined with AssetDatabase touches inside RescanNow,
849
+ // this caused permanent script-compilation freezes on Unity startup.
850
+ // We DO parse the per-assembly CompilerMessage payload here (cheap, pure-CPU work,
851
+ // no AssetDatabase / LogEntries contact) and stash it in the cross-thread channel
852
+ // without checking settings here. The settings gate requires AssetDatabase access, so
853
+ // RescanNow applies it later after DrainScheduledRescan has requeued until Unity is
854
+ // idle; if the bridge is disabled, RescanNow clears this pending buffer. This is the
855
+ // primary bridge data path on Unity 2021, where Roslyn-analyzer warnings DO arrive in
856
+ // CompilerMessage[] but do NOT reliably appear in the LogEntries store.
857
+ try
858
+ {
859
+ if (!string.IsNullOrEmpty(assemblyPath) && messages != null)
860
+ {
861
+ List<string> lines = null;
862
+ foreach (CompilerMessage compilerMessage in messages)
863
+ {
864
+ string body = compilerMessage.message;
865
+ if (string.IsNullOrEmpty(body))
866
+ {
867
+ continue;
868
+ }
869
+ // Quick prefilter so we don't parse every CS0123 in the build. The
870
+ // analyzer always emits "DXMSG00" inside the diagnostic id.
871
+ if (body.IndexOf("DXMSG00", StringComparison.Ordinal) < 0)
872
+ {
873
+ continue;
874
+ }
875
+ lines ??= new List<string>();
876
+ lines.Add(body);
877
+ }
878
+
879
+ Dictionary<string, ParsedTypeReport> aggregated = lines is null
880
+ ? new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal)
881
+ : BaseCallLogMessageParser.Aggregate(lines);
882
+
883
+ lock (_compilationFeedLock)
884
+ {
885
+ // Even when this assembly produced zero matching messages, we still want
886
+ // an empty entry so DrainScheduledRescan can RETIRE the assembly's prior
887
+ // attribution (the user fixed every offending type in this assembly).
888
+ _pendingByAssembly[assemblyPath] = aggregated;
889
+ }
890
+ }
891
+ }
892
+ catch (Exception ex)
893
+ {
894
+ LogExceptionOnce(
895
+ "compilation-parse",
896
+ $"Failed to parse CompilerMessage payload for {assemblyPath}.",
897
+ ex
898
+ );
899
+ }
900
+
901
+ // Schedule a single delayed drain. The callback rechecks editor state and requeues if
902
+ // Unity is still compiling/updating, so this never drops the final rescan.
903
+ ScheduleRescanWhenIdle();
904
+ }
905
+
906
+ internal static void ScheduleRescanWhenIdle()
907
+ {
908
+ if (!IsAvailable || _rescanScheduled)
909
+ {
910
+ return;
911
+ }
912
+ _rescanScheduled = true;
913
+ EditorApplication.delayCall += DrainScheduledRescan;
914
+ }
915
+
916
+ private static void DrainScheduledRescan()
917
+ {
918
+ _rescanScheduled = false;
919
+ // delayCall can fire while still mid-compile if the editor is in a weird state.
920
+ // RescanNow has its own isCompiling/isUpdating guard; re-defer if needed.
921
+ if (EditorApplication.isCompiling || EditorApplication.isUpdating)
922
+ {
923
+ if (!_rescanScheduled)
924
+ {
925
+ _rescanScheduled = true;
926
+ EditorApplication.delayCall += DrainScheduledRescan;
927
+ }
928
+ return;
929
+ }
930
+ SafeRescanFromCallback();
931
+ }
932
+
933
+ private static void SafeRescanFromCallback()
934
+ {
935
+ try
936
+ {
937
+ RescanNow();
938
+ }
939
+ catch (Exception ex)
940
+ {
941
+ LogExceptionOnce("rescan-callback", "RescanNow callback threw.", ex);
942
+ }
943
+ }
944
+
945
+ private static DxMessagingSettings TryLoadSettings()
946
+ {
947
+ // CRITICAL: passive load only. We must NOT call GetOrCreateSettings here; that path
948
+ // can call AssetDatabase.CreateAsset, which during script compilation schedules an
949
+ // import → re-triggers compilation → permanent freeze. The Project Settings page and
950
+ // the inspector overlay both call GetOrCreateSettings on demand (outside compilation),
951
+ // so the asset is materialised through normal user interaction. If the asset doesn't
952
+ // exist yet (fresh project, first compile), the harvester treats the snapshot as
953
+ // unconfigured and behaves as if the master toggle is enabled (default behaviour).
954
+ try
955
+ {
956
+ string[] guids = AssetDatabase.FindAssets($"t:{nameof(DxMessagingSettings)}");
957
+ if (guids == null || guids.Length == 0)
958
+ {
959
+ return null;
960
+ }
961
+ string assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
962
+ if (string.IsNullOrEmpty(assetPath))
963
+ {
964
+ return null;
965
+ }
966
+ return AssetDatabase.LoadAssetAtPath<DxMessagingSettings>(assetPath);
967
+ }
968
+ catch (Exception ex)
969
+ {
970
+ LogExceptionOnce("settings", "Could not load DxMessagingSettings.", ex);
971
+ return null;
972
+ }
973
+ }
974
+
975
+ private static MethodInfo SafeGetStaticMethod(Type type, string name)
976
+ {
977
+ try
978
+ {
979
+ return type.GetMethod(name, BindingFlags.Public | BindingFlags.Static);
980
+ }
981
+ catch (Exception ex)
982
+ {
983
+ LogExceptionOnce(
984
+ $"resolve-{name}",
985
+ $"Failed to resolve static method '{name}' on {type.FullName}.",
986
+ ex
987
+ );
988
+ return null;
989
+ }
990
+ }
991
+
992
+ private static FieldInfo SafeGetInstanceField(Type type, string name)
993
+ {
994
+ try
995
+ {
996
+ return type.GetField(name, BindingFlags.Public | BindingFlags.Instance);
997
+ }
998
+ catch (Exception ex)
999
+ {
1000
+ LogExceptionOnce(
1001
+ $"resolve-field-{name}",
1002
+ $"Failed to resolve instance field '{name}' on {type.FullName}.",
1003
+ ex
1004
+ );
1005
+ return null;
1006
+ }
1007
+ }
1008
+
1009
+ private static void LogOnce(string key, string message)
1010
+ {
1011
+ if (!AlreadyWarned.Add(key))
1012
+ {
1013
+ return;
1014
+ }
1015
+ Debug.LogWarning($"[DxMessaging] {message}");
1016
+ }
1017
+
1018
+ private static void LogExceptionOnce(string key, string message, Exception exception)
1019
+ {
1020
+ if (!AlreadyWarned.Add(key))
1021
+ {
1022
+ return;
1023
+ }
1024
+ DxMessagingEditorLog.LogWarning(message, exception);
1025
+ }
1026
+
1027
+ private static void RaiseReportUpdated()
1028
+ {
1029
+ try
1030
+ {
1031
+ ReportUpdated?.Invoke();
1032
+ }
1033
+ catch (Exception ex)
1034
+ {
1035
+ LogExceptionOnce("report-updated", "ReportUpdated subscriber threw.", ex);
1036
+ }
1037
+ }
1038
+
1039
+ // -- JSON persistence -----------------------------------------------------------------
1040
+ // The cache survives editor restarts so the overlay has data to render before the first
1041
+ // post-launch rescan completes; it is rewritten on every successful rescan.
1042
+
1043
+ internal static void PersistToDisk()
1044
+ {
1045
+ try
1046
+ {
1047
+ string absolutePath = GetReportFilePath();
1048
+ EnsureDirectoryExists(absolutePath);
1049
+
1050
+ BaseCallReportFile file = new()
1051
+ {
1052
+ version = 1,
1053
+ generatedAt = DateTime.UtcNow.ToString(
1054
+ "yyyy-MM-ddTHH:mm:ssZ",
1055
+ CultureInfo.InvariantCulture
1056
+ ),
1057
+ types = SnapshotInternal
1058
+ .Values.OrderBy(e => e.typeName, StringComparer.Ordinal)
1059
+ .ToList(),
1060
+ };
1061
+
1062
+ string json = JsonUtility.ToJson(file, prettyPrint: true);
1063
+ File.WriteAllText(absolutePath, json);
1064
+ }
1065
+ catch (Exception ex)
1066
+ {
1067
+ LogExceptionOnce("persist", "Failed to persist analyzer diagnostics report.", ex);
1068
+ }
1069
+ }
1070
+
1071
+ internal static void LoadFromDisk()
1072
+ {
1073
+ try
1074
+ {
1075
+ string absolutePath = GetReportFilePath();
1076
+ if (!File.Exists(absolutePath))
1077
+ {
1078
+ return;
1079
+ }
1080
+
1081
+ string json = File.ReadAllText(absolutePath);
1082
+ if (string.IsNullOrWhiteSpace(json))
1083
+ {
1084
+ return;
1085
+ }
1086
+
1087
+ BaseCallReportFile file = JsonUtility.FromJson<BaseCallReportFile>(json);
1088
+ if (file?.types == null)
1089
+ {
1090
+ return;
1091
+ }
1092
+
1093
+ SnapshotInternal.Clear();
1094
+ foreach (BaseCallReportEntry entry in file.types)
1095
+ {
1096
+ if (entry == null || string.IsNullOrEmpty(entry.typeName))
1097
+ {
1098
+ continue;
1099
+ }
1100
+ entry.missingBaseFor ??= new List<string>();
1101
+ entry.diagnosticIds ??= new List<string>();
1102
+ SnapshotInternal[entry.typeName] = entry;
1103
+ }
1104
+ }
1105
+ catch (Exception ex)
1106
+ {
1107
+ LogExceptionOnce("load", "Failed to load analyzer diagnostics report.", ex);
1108
+ }
1109
+ }
1110
+
1111
+ internal static string GetReportFilePath()
1112
+ {
1113
+ string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."))
1114
+ .Replace("\\", "/");
1115
+ return Path.Combine(projectRoot, "Library", ReportDirectoryName, ReportFileName)
1116
+ .Replace("\\", "/");
1117
+ }
1118
+
1119
+ private static void EnsureDirectoryExists(string absolutePath)
1120
+ {
1121
+ string directory = Path.GetDirectoryName(absolutePath);
1122
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
1123
+ {
1124
+ Directory.CreateDirectory(directory);
1125
+ }
1126
+ }
1127
+ }
1128
+ #endif
1129
+ }