com.wallstop-studios.dxmessaging 3.1.0 → 3.2.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 (85) hide show
  1. package/CHANGELOG.md +286 -0
  2. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +3 -3
  3. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +3 -3
  4. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +3 -3
  5. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +3 -3
  6. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +3 -3
  7. package/Editor/DxMessagingMenu.cs.meta +3 -3
  8. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +3 -3
  9. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +2 -3
  10. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +3 -3
  11. package/Editor/SetupCscRsp.cs +232 -163
  12. package/README.md +1 -1
  13. package/Runtime/Analyzers.meta +8 -0
  14. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +3 -3
  15. package/Runtime/Core/DxMessagingStaticState.cs.meta +3 -3
  16. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +3 -3
  17. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +3 -3
  18. package/Runtime/Core/IMessage.cs.meta +3 -3
  19. package/Runtime/Core/InstanceId.cs.meta +3 -3
  20. package/Runtime/Core/Internal/TypedSlots.cs +42 -0
  21. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +3 -3
  22. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +3 -3
  23. package/Runtime/Core/MessageBus/IMessageBus.cs +63 -28
  24. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +3 -3
  25. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +3 -3
  26. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +3 -3
  27. package/Runtime/Core/MessageBus/MessageBus.cs +548 -474
  28. package/Runtime/Core/MessageBus/MessageBus.cs.meta +3 -3
  29. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +3 -3
  30. package/Runtime/Core/MessageBus/MessageBusRegistration.cs +207 -0
  31. package/Runtime/Core/MessageBus/MessageBusRegistration.cs.meta +11 -0
  32. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +3 -3
  33. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +3 -3
  34. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +3 -3
  35. package/Runtime/Core/MessageHandler.cs +1210 -394
  36. package/Runtime/Core/MessageHandler.cs.meta +3 -3
  37. package/Runtime/Core/MessageRegistrationHandle.cs.meta +3 -3
  38. package/Runtime/Core/MessageRegistrationToken.cs +1296 -819
  39. package/Runtime/Core/MessageRegistrationToken.cs.meta +3 -3
  40. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +3 -3
  41. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +3 -3
  42. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +3 -3
  43. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +3 -3
  44. package/Runtime/Core/MessagingDebug.cs.meta +3 -3
  45. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +3 -3
  46. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +3 -3
  47. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +3 -3
  48. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +3 -3
  49. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +3 -3
  50. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +3 -3
  51. package/Runtime/Unity/MessageAwareComponent.cs.meta +3 -3
  52. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +3 -3
  53. package/Runtime/Unity/MessagingComponent.cs.meta +3 -3
  54. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +3 -3
  55. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +3 -3
  56. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +3 -3
  57. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +3 -3
  58. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +3 -3
  59. package/Samples~/Mini Combat/Boot.cs.meta +3 -3
  60. package/Samples~/Mini Combat/Enemy.cs.meta +3 -3
  61. package/Samples~/Mini Combat/Messages.cs.meta +3 -3
  62. package/Samples~/Mini Combat/Player.cs.meta +3 -3
  63. package/Samples~/Mini Combat/UIOverlay.cs.meta +3 -3
  64. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +3 -3
  65. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +3 -3
  66. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +3 -3
  67. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +3 -3
  68. package/SourceGenerators/Directory.Build.props +5 -5
  69. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +3 -3
  70. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +12 -11
  71. package/package.json +6 -3
  72. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  73. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +0 -0
  74. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
  75. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll.meta +0 -0
  76. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll +0 -0
  77. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll.meta +0 -0
  78. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll +0 -0
  79. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll.meta +0 -0
  80. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  81. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +0 -0
  82. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  83. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +0 -0
  84. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  85. /package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +0 -0
@@ -5,12 +5,9 @@ namespace DxMessaging.Editor
5
5
  using System;
6
6
  using System.Collections.Generic;
7
7
  using System.IO;
8
- using System.Linq;
9
- using System.Security.Cryptography;
10
8
  using DxMessaging.Editor.Settings;
11
9
  using UnityEditor;
12
10
  using UnityEngine;
13
- using Object = UnityEngine.Object;
14
11
 
15
12
  [InitializeOnLoad]
16
13
  public static class SetupCscRsp
@@ -22,57 +19,55 @@ namespace DxMessaging.Editor
22
19
  )
23
20
  .Replace("\\", "/");
24
21
 
25
- private static readonly string[] AnalyzerDirectories =
26
- {
27
- "Packages/com.wallstop-studios.dxmessaging/Editor/Analyzers/",
28
- "Library/PackageCache/com.wallstop-studios.dxmessaging/Editor/Analyzers/",
29
- };
22
+ // Older package versions copied the analyzer + Roslyn runtime DLLs into the consumer
23
+ // project here so the source generator applied project-wide. The generator now ships
24
+ // under the package's Runtime/Analyzers folder (Unity scopes it natively to the runtime
25
+ // assembly and everything that references it, including the predefined Assembly-CSharp),
26
+ // so this in-project copy is redundant and is removed on upgrade.
27
+ internal const string LegacyAnalyzerCopyFolder =
28
+ "Assets/Plugins/Editor/WallstopStudios.DxMessaging";
30
29
 
31
- private static readonly string SourceGeneratorDllName =
30
+ private const string LegacySourceGeneratorDllName =
32
31
  "WallstopStudios.DxMessaging.SourceGenerators.dll";
33
32
 
34
- // The analyzer DLL is a SEPARATE assembly, but both compiler-host DLLs are pinned to
35
- // Unity 2021-compatible Roslyn 3.8.0. They ship side-by-side and both need the
36
- // RoslynAnalyzer label.
37
- private static readonly string AnalyzerDllName = "WallstopStudios.DxMessaging.Analyzer.dll";
33
+ // Released 2.x legacy folders predate the companion analyzer DLL, so the source generator
34
+ // is the required package-owned marker. Unknown DLLs still make the folder unsafe.
35
+ private static readonly HashSet<string> RequiredLegacyAnalyzerCopyDlls = new(
36
+ StringComparer.OrdinalIgnoreCase
37
+ )
38
+ {
39
+ LegacySourceGeneratorDllName,
40
+ };
38
41
 
39
- // The analyzer DLLs and shared Roslyn surface ship unconditionally; they're light enough
40
- // and required for DXMSG002–DXMSG009 to function at all. The list intentionally references
41
- // a few transitive Roslyn deps that may or may not physically ship with the package; the
42
- // copy loop below silently skips any name that isn't on disk.
43
- private static readonly string[] RequiredDllNames =
42
+ private static readonly HashSet<string> KnownLegacyAnalyzerCopyDlls = new(
43
+ StringComparer.OrdinalIgnoreCase
44
+ )
44
45
  {
45
- SourceGeneratorDllName,
46
- AnalyzerDllName,
46
+ LegacySourceGeneratorDllName,
47
+ "WallstopStudios.DxMessaging.Analyzer.dll",
47
48
  "Microsoft.CodeAnalysis.dll",
48
49
  "Microsoft.CodeAnalysis.CSharp.dll",
49
- "System.Text.Encodings.Web.dll",
50
- "System.Reflection.Metadata.dll",
51
- "System.Runtime.CompilerServices.Unsafe.dll",
50
+ "System.Buffers.dll",
52
51
  "System.Collections.Immutable.dll",
53
52
  "System.Memory.dll",
54
- "System.Buffers.dll",
55
- "System.Threading.Tasks.Extensions.dll",
56
53
  "System.Numerics.Vectors.dll",
54
+ "System.Reflection.Metadata.dll",
55
+ "System.Runtime.CompilerServices.Unsafe.dll",
57
56
  "System.Text.Encoding.CodePages.dll",
57
+ "System.Text.Encodings.Web.dll",
58
+ "System.Threading.Tasks.Extensions.dll",
58
59
  };
59
60
 
60
- // DLLs that must be tagged with Unity's "RoslynAnalyzer" asset label so Unity's compiler
61
- // pipeline picks them up as analyzer/source-generator hosts. Other DLLs in the same folder
62
- // (Roslyn runtime, immutable collections) are plain Editor-only plugin DLLs.
63
- private static readonly HashSet<string> AnalyzerLabeledDllNames = new(
64
- StringComparer.OrdinalIgnoreCase
65
- )
66
- {
67
- SourceGeneratorDllName,
68
- AnalyzerDllName,
69
- };
70
-
71
- private static readonly HashSet<string> DllNames = new(StringComparer.OrdinalIgnoreCase);
61
+ private static bool loggedSkippedLegacyAnalyzerCopyCleanup;
72
62
 
73
63
  static SetupCscRsp()
74
64
  {
75
- ScheduleSetupStep(EnsureDLLsExistInAssets, "copy analyzer DLLs into Assets");
65
+ // Backstop only: the primary removal happens pre-compile in LegacyAnalyzerCopyCleanup.
66
+ // This catches projects whose legacy copy predates the upgrade and triggers no import.
67
+ ScheduleSetupStep(
68
+ () => TryRemoveLegacyAnalyzerCopy(),
69
+ "remove redundant in-project analyzer copy"
70
+ );
76
71
  ScheduleSetupStep(EnsureCscRsp, "clean csc.rsp analyzer entries");
77
72
  ScheduleAdditionalFileForIgnoreListSync();
78
73
  }
@@ -104,147 +99,186 @@ namespace DxMessaging.Editor
104
99
  }
105
100
  }
106
101
 
107
- private static void EnsureDLLsExistInAssets()
102
+ /// <summary>
103
+ /// Deletes the redundant in-project analyzer copy that older package versions deployed to
104
+ /// <see cref="LegacyAnalyzerCopyFolder"/>. The source generator now ships under the
105
+ /// package's <c>Runtime/Analyzers</c> folder and applies automatically, so the copy is no
106
+ /// longer needed -- and, critically, leaving it in place makes BOTH copies generate into
107
+ /// every DxMessaging-referencing assembly, emitting each member twice (CS0102). Removing it
108
+ /// is therefore an upgrade requirement, not just cleanup.
109
+ /// </summary>
110
+ /// <remarks>
111
+ /// Called from <see cref="LegacyAnalyzerCopyCleanup"/> (an <c>AssetPostprocessor</c>) so the
112
+ /// removal lands during the asset import that PRECEDES script compilation -- before the two
113
+ /// copies can both feed the compiler -- and again from the <c>[InitializeOnLoad]</c> static
114
+ /// constructor as a post-compile backstop. Idempotent (a no-op once the folder is gone) and
115
+ /// conservative: it leaves the folder untouched if it holds anything other than the analyzer
116
+ /// DLLs this package deployed there. Returns <c>true</c> only when it actually deleted the
117
+ /// folder.
118
+ /// </remarks>
119
+ internal static bool TryRemoveLegacyAnalyzerCopy()
120
+ {
121
+ if (!AssetDatabase.IsValidFolder(LegacyAnalyzerCopyFolder))
122
+ {
123
+ return false;
124
+ }
125
+
126
+ // The only shape this package ever created here is a flat set of analyzer / Roslyn
127
+ // DLLs plus their auto-generated .meta sidecars. Inspect the on-disk folder and bail
128
+ // out if a consumer repurposed it for anything else.
129
+ string absoluteFolder = Path.GetFullPath(
130
+ Path.Combine(Application.dataPath, "..", LegacyAnalyzerCopyFolder)
131
+ );
132
+ if (!Directory.Exists(absoluteFolder))
133
+ {
134
+ return false;
135
+ }
136
+
137
+ // A real subdirectory means a consumer repurposed this folder for their own content;
138
+ // preserve it. The package only ever wrote a flat set of analyzer DLLs here, so the
139
+ // safe-to-remove check below sees only files (a subfolder named "x.dll" can never be
140
+ // mistaken for a DLL).
141
+ string[] files = Directory.GetFiles(absoluteFolder);
142
+ if (Directory.GetDirectories(absoluteFolder).Length > 0)
143
+ {
144
+ LogSkippedLegacyAnalyzerCopyCleanupIfNeeded(files);
145
+ return false;
146
+ }
147
+
148
+ if (!IsLegacyAnalyzerCopySafeToRemove(files))
149
+ {
150
+ LogSkippedLegacyAnalyzerCopyCleanupIfNeeded(files);
151
+ return false;
152
+ }
153
+
154
+ if (AssetDatabase.DeleteAsset(LegacyAnalyzerCopyFolder))
155
+ {
156
+ Debug.Log(
157
+ "DxMessaging: removed the redundant in-project analyzer copy at "
158
+ + LegacyAnalyzerCopyFolder
159
+ + ". The source generator now ships under the package's Runtime/Analyzers "
160
+ + "folder and applies automatically; nothing needs to live under Assets."
161
+ );
162
+ return true;
163
+ }
164
+
165
+ return false;
166
+ }
167
+
168
+ private static void LogSkippedLegacyAnalyzerCopyCleanupIfNeeded(IEnumerable<string> files)
108
169
  {
109
- DllNames.Clear();
110
- foreach (
111
- string dllGuid in AssetDatabase.FindAssets("t:DefaultAsset", new[] { "Assets" })
170
+ if (
171
+ loggedSkippedLegacyAnalyzerCopyCleanup
172
+ || !ContainsKnownLegacyAnalyzerCopyEntry(files)
112
173
  )
113
174
  {
114
- string dllPath = AssetDatabase.GUIDToAssetPath(dllGuid);
115
- if (!dllPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
175
+ return;
176
+ }
177
+
178
+ loggedSkippedLegacyAnalyzerCopyCleanup = true;
179
+ Debug.LogWarning(
180
+ "DxMessaging: found a legacy in-project analyzer copy at "
181
+ + LegacyAnalyzerCopyFolder
182
+ + " but did not remove it because the folder contains content outside "
183
+ + "the exact known package payload or is missing the required "
184
+ + "DxMessaging source-generator DLL. Move consumer-owned files out of that folder, "
185
+ + "then delete the stale DxMessaging analyzer DLLs manually to avoid "
186
+ + "double-loading analyzers."
187
+ );
188
+ }
189
+
190
+ /// <summary>
191
+ /// True when the legacy analyzer-copy folder contains the first-party source generator and
192
+ /// every entry is one of the exact analyzer/dependency DLLs this package deployed there or
193
+ /// a matching <c>.dll.meta</c> sidecar. A subfolder, a foreign DLL, or any other file makes
194
+ /// this false so the folder is preserved rather than deleted.
195
+ /// </summary>
196
+ internal static bool IsLegacyAnalyzerCopySafeToRemove(IEnumerable<string> folderEntries)
197
+ {
198
+ HashSet<string> presentRequiredDlls = new(StringComparer.OrdinalIgnoreCase);
199
+ HashSet<string> seenEntryNames = new(StringComparer.OrdinalIgnoreCase);
200
+ foreach (string entry in folderEntries ?? Array.Empty<string>())
201
+ {
202
+ if (string.IsNullOrEmpty(entry))
116
203
  {
117
204
  continue;
118
205
  }
119
206
 
120
- if (!dllPath.Contains("Assets/Plugins", StringComparison.OrdinalIgnoreCase))
207
+ string name = GetLegacyAnalyzerCopyEntryName(entry);
208
+ if (string.IsNullOrEmpty(name) || !seenEntryNames.Add(name))
121
209
  {
122
- string dllName = Path.GetFileName(dllPath);
123
- DllNames.Add(dllName);
210
+ return false;
124
211
  }
125
- }
126
212
 
127
- foreach (string requiredDllName in RequiredDllNames)
128
- {
129
- if (DllNames.Contains(requiredDllName))
213
+ if (name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
130
214
  {
215
+ if (!KnownLegacyAnalyzerCopyDlls.Contains(name))
216
+ {
217
+ return false;
218
+ }
219
+
220
+ if (RequiredLegacyAnalyzerCopyDlls.Contains(name))
221
+ {
222
+ presentRequiredDlls.Add(name);
223
+ }
224
+
131
225
  continue;
132
226
  }
133
227
 
134
- foreach (string relativeDirectory in AnalyzerDirectories)
228
+ if (name.EndsWith(".dll.meta", StringComparison.OrdinalIgnoreCase))
135
229
  {
136
- try
230
+ string dllName = name.Substring(0, name.Length - ".meta".Length);
231
+ if (!KnownLegacyAnalyzerCopyDlls.Contains(dllName))
137
232
  {
138
- string sourceFile = $"{relativeDirectory}{requiredDllName}";
139
- if (!File.Exists(sourceFile))
140
- {
141
- continue;
142
- }
143
-
144
- const string pluginsDirectory =
145
- "Assets/Plugins/Editor/WallstopStudios.DxMessaging/";
146
- string outputAsset = $"{pluginsDirectory}{requiredDllName}";
147
- if (!Directory.Exists(pluginsDirectory))
148
- {
149
- Directory.CreateDirectory(pluginsDirectory);
150
- AssetDatabase.Refresh();
151
- }
152
- bool needsCopy = FilesDiffer(sourceFile, outputAsset);
153
- if (needsCopy)
154
- {
155
- File.Copy(sourceFile, outputAsset, true);
156
- AssetDatabase.ImportAsset(outputAsset);
157
- }
158
-
159
- if (AnalyzerLabeledDllNames.Contains(requiredDllName))
160
- {
161
- Object loadedDll = AssetDatabase.LoadMainAssetAtPath(outputAsset);
162
- if (loadedDll != null)
163
- {
164
- string[] existingLabels = AssetDatabase.GetLabels(loadedDll);
165
- if (!existingLabels.Contains("RoslynAnalyzer"))
166
- {
167
- List<string> newLabels = existingLabels.ToList();
168
- newLabels.Add("RoslynAnalyzer");
169
- AssetDatabase.SetLabels(loadedDll, newLabels.ToArray());
170
- }
171
- }
172
- }
173
-
174
- if (AssetImporter.GetAtPath(outputAsset) is PluginImporter importer)
175
- {
176
- bool importerDirty = false;
177
-
178
- // A RoslynAnalyzer-labeled DLL must be EXCLUDED from every
179
- // build platform, including the Editor, so Unity treats it as a
180
- // C# compiler analyzer rather than a managed precompiled
181
- // assembly. The same-named DLL is importable from two locations
182
- // (the package's own Editor/Analyzers copy and this Assets
183
- // copy); if either is an Editor-enabled precompiled assembly,
184
- // Unity 2021 aborts with "Multiple precompiled assemblies with
185
- // the same name". The Roslyn runtime dependencies in the same
186
- // folder already ship Editor-disabled and never collide -- this
187
- // converges the analyzer DLLs onto that proven-safe shape.
188
- // NOTE: SetExcludeFromAnyPlatform is a no-op once
189
- // CompatibleWithAnyPlatform is false, so the Editor platform is
190
- // disabled through the effective SetCompatibleWithEditor API.
191
- if (importer.GetCompatibleWithAnyPlatform())
192
- {
193
- importer.SetCompatibleWithAnyPlatform(false);
194
- importerDirty = true;
195
- }
196
-
197
- if (importer.GetCompatibleWithEditor())
198
- {
199
- importer.SetCompatibleWithEditor(false);
200
- importerDirty = true;
201
- }
202
-
203
- if (importerDirty || needsCopy)
204
- {
205
- importer.SaveAndReimport();
206
- }
207
- }
208
-
209
- DllNames.Add(requiredDllName);
210
- break;
211
- }
212
- catch (Exception ex)
213
- {
214
- DxMessagingEditorLog.LogError(
215
- $"Failed to copy {requiredDllName} to Assets.",
216
- ex
217
- );
233
+ return false;
218
234
  }
235
+
236
+ continue;
219
237
  }
220
- }
221
238
 
222
- if (DllNames.Count > 0)
223
- {
224
- AssetDatabase.Refresh();
239
+ return false;
225
240
  }
241
+
242
+ return presentRequiredDlls.Count == RequiredLegacyAnalyzerCopyDlls.Count;
226
243
  }
227
244
 
228
- private static bool FilesDiffer(string sourcePath, string destinationPath)
245
+ private static string GetLegacyAnalyzerCopyEntryName(string entry)
229
246
  {
230
- if (!File.Exists(destinationPath))
231
- {
232
- return true;
233
- }
247
+ string normalizedEntry = entry.Replace("\\", "/");
248
+ int lastSeparator = normalizedEntry.LastIndexOf('/');
249
+ return lastSeparator >= 0
250
+ ? normalizedEntry.Substring(lastSeparator + 1)
251
+ : normalizedEntry;
252
+ }
234
253
 
235
- FileInfo sourceInfo = new(sourcePath);
236
- FileInfo destinationInfo = new(destinationPath);
237
- if (sourceInfo.Length != destinationInfo.Length)
254
+ private static bool ContainsKnownLegacyAnalyzerCopyEntry(IEnumerable<string> folderEntries)
255
+ {
256
+ foreach (string entry in folderEntries ?? Array.Empty<string>())
238
257
  {
239
- return true;
258
+ if (string.IsNullOrEmpty(entry))
259
+ {
260
+ continue;
261
+ }
262
+
263
+ string name = GetLegacyAnalyzerCopyEntryName(entry);
264
+ if (KnownLegacyAnalyzerCopyDlls.Contains(name))
265
+ {
266
+ return true;
267
+ }
268
+
269
+ if (!name.EndsWith(".dll.meta", StringComparison.OrdinalIgnoreCase))
270
+ {
271
+ continue;
272
+ }
273
+
274
+ string dllName = name.Substring(0, name.Length - ".meta".Length);
275
+ if (KnownLegacyAnalyzerCopyDlls.Contains(dllName))
276
+ {
277
+ return true;
278
+ }
240
279
  }
241
280
 
242
- using FileStream sourceStream = File.OpenRead(sourcePath);
243
- using FileStream destinationStream = File.OpenRead(destinationPath);
244
- using SHA256 sha256 = SHA256.Create();
245
- byte[] sourceHash = sha256.ComputeHash(sourceStream);
246
- byte[] destinationHash = sha256.ComputeHash(destinationStream);
247
- return !sourceHash.AsSpan().SequenceEqual(destinationHash);
281
+ return false;
248
282
  }
249
283
 
250
284
  /// <summary>
@@ -252,9 +286,12 @@ namespace DxMessaging.Editor
252
286
  /// </summary>
253
287
  /// <remarks>
254
288
  /// DxMessaging analyzers are activated solely through the RoslynAnalyzer-labeled
255
- /// <c>Assets/Plugins/Editor/WallstopStudios.DxMessaging</c> copy. A second
256
- /// <c>-a:</c> registration here double-loads the analyzer/source-generator, and
257
- /// registering dependency DLLs as analyzers makes Unity's compiler path fragile.
289
+ /// DLLs shipped under the package's <c>Runtime/Analyzers</c> folder, which Unity
290
+ /// scopes to the runtime assembly and every assembly that references it (including
291
+ /// the predefined Assembly-CSharp). A stray <c>-a:</c> registration here (for
292
+ /// example, one left behind by an older package version that copied analyzers into
293
+ /// the project) double-loads the source generator, and registering dependency DLLs
294
+ /// as analyzers makes Unity's compiler path fragile, so this method strips them.
258
295
  /// </remarks>
259
296
  private static void EnsureCscRsp()
260
297
  {
@@ -262,8 +299,7 @@ namespace DxMessaging.Editor
262
299
  {
263
300
  if (!File.Exists(RspFilePath))
264
301
  {
265
- File.WriteAllText(RspFilePath, string.Empty);
266
- AssetDatabase.ImportAsset("csc.rsp");
302
+ return;
267
303
  }
268
304
 
269
305
  string rspContent = File.ReadAllText(RspFilePath);
@@ -375,12 +411,6 @@ namespace DxMessaging.Editor
375
411
  {
376
412
  try
377
413
  {
378
- if (!File.Exists(RspFilePath))
379
- {
380
- File.WriteAllText(RspFilePath, string.Empty);
381
- AssetDatabase.ImportAsset("csc.rsp");
382
- }
383
-
384
414
  string sidecarRelativePath = DxMessagingBaseCallIgnoreSync.SidecarAssetPath;
385
415
  string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."))
386
416
  .Replace("\\", "/");
@@ -388,6 +418,22 @@ namespace DxMessaging.Editor
388
418
  .Replace("\\", "/");
389
419
 
390
420
  bool sidecarExists = File.Exists(sidecarAbsolutePath);
421
+ if (!File.Exists(RspFilePath))
422
+ {
423
+ if (!sidecarExists)
424
+ {
425
+ return;
426
+ }
427
+
428
+ File.WriteAllText(
429
+ RspFilePath,
430
+ FormatAdditionalFileArgument(sidecarRelativePath) + Environment.NewLine
431
+ );
432
+ AssetDatabase.ImportAsset("csc.rsp");
433
+ Debug.Log("Updated csc.rsp additionalfile entries.");
434
+ return;
435
+ }
436
+
391
437
  string[] newLines = SynchronizeAdditionalFileForIgnoreListLines(
392
438
  File.ReadAllLines(RspFilePath),
393
439
  sidecarRelativePath,
@@ -630,5 +676,28 @@ namespace DxMessaging.Editor
630
676
  return arguments;
631
677
  }
632
678
  }
679
+
680
+ /// <summary>
681
+ /// Removes the legacy in-project analyzer copy (via
682
+ /// <see cref="SetupCscRsp.TryRemoveLegacyAnalyzerCopy"/>) during the asset import that PRECEDES
683
+ /// script compilation. This is the primary upgrade path: deleting the redundant copy before the
684
+ /// compiler runs prevents it and the package's Runtime/Analyzers copy from both generating into
685
+ /// the same assembly (which would emit each member twice -- CS0102). It is a cheap no-op in the
686
+ /// steady state: once the folder is gone, <c>AssetDatabase.IsValidFolder</c> short-circuits on
687
+ /// every subsequent import.
688
+ /// </summary>
689
+ internal sealed class LegacyAnalyzerCopyCleanup : AssetPostprocessor
690
+ {
691
+ // Classic four-argument signature so this compiles on every supported Unity (2021.3+).
692
+ private static void OnPostprocessAllAssets(
693
+ string[] importedAssets,
694
+ string[] deletedAssets,
695
+ string[] movedAssets,
696
+ string[] movedFromAssetPaths
697
+ )
698
+ {
699
+ SetupCscRsp.TryRemoveLegacyAnalyzerCopy();
700
+ }
701
+ }
633
702
  }
634
703
  #endif
package/README.md CHANGED
@@ -908,7 +908,7 @@ DxMessaging publishes machine-readable context for AI agents through [llms.txt](
908
908
 
909
909
  - **[llms.txt](llms.txt)** -- Complete project overview, API reference, and context in a single file
910
910
  - **[Repository Guidelines](.llm/context.md)** -- Coding standards and development workflows
911
- - **[AI Agent Skills](.llm/skills/)** -- 155+ specialized skill documents covering documentation, testing, GitHub Actions, and more
911
+ - **[AI Agent Skills](.llm/skills/)** -- 157+ specialized skill documents covering documentation, testing, GitHub Actions, and more
912
912
 
913
913
  The `llms.txt` file is automatically updated via CI/CD to stay current with project changes. It includes:
914
914
 
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: 9b74c5a1c1a3530ec597b257c46ad4b0
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,7 +6,7 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
12
12
 
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -83,6 +83,48 @@ namespace DxMessaging.Core.Internal
83
83
  /// invalidation. Idempotent.
84
84
  /// </summary>
85
85
  void Reset();
86
+
87
+ /// <summary>
88
+ /// Non-generic membership probe over the cache's entries map. The
89
+ /// per-handle teardown object (which carries no delegate-shape type
90
+ /// argument) uses this instead of a typed
91
+ /// <c>entries.ContainsKey</c> so it can decide whether the
92
+ /// over-deregistration silent-no-op guard fires without down-casting
93
+ /// to the concrete <c>HandlerActionCache&lt;TDelegate&gt;</c>.
94
+ /// </summary>
95
+ /// <param name="originalHandler">
96
+ /// The dedup/identity key (boxed delegate) the teardown captured at
97
+ /// registration time; cast back to the cache's delegate shape by the
98
+ /// implementation.
99
+ /// </param>
100
+ /// <returns>True iff an entry exists for the handler.</returns>
101
+ bool ContainsEntry(object originalHandler);
102
+
103
+ /// <summary>
104
+ /// Bumps <see cref="Version"/> by one. Mirrors the explicit
105
+ /// <c>version++</c> the legacy deregistration closure performed at the
106
+ /// top of its body (before the bus deregister call) so any in-flight
107
+ /// dispatch snapshot detects invalidation at the same point.
108
+ /// </summary>
109
+ void BumpVersion();
110
+
111
+ /// <summary>
112
+ /// Non-generic decrement-or-remove for the captured handler entry.
113
+ /// Mirrors the EXACT refcount logic the legacy deregistration closure
114
+ /// used: if the entry's refcount is at or below one, removes it from
115
+ /// both the entries map and the insertion-order list and bumps
116
+ /// <see cref="Version"/> a second time (reporting
117
+ /// <paramref name="wasLastForEntry"/> = true so the caller can
118
+ /// decrement the owning slot's live count); otherwise decrements the
119
+ /// refcount in place, preserving the first registration's handler /
120
+ /// flat invoker.
121
+ /// </summary>
122
+ /// <param name="originalHandler">The dedup/identity key (boxed delegate).</param>
123
+ /// <param name="wasLastForEntry">
124
+ /// True iff this call removed the entry (the refcount reached zero).
125
+ /// </param>
126
+ /// <returns>True iff an entry existed for the handler.</returns>
127
+ bool DeregisterEntry(object originalHandler, out bool wasLastForEntry);
86
128
  }
87
129
 
88
130
  /// <summary>
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -6,6 +6,6 @@ MonoImporter:
6
6
  defaultReferences: []
7
7
  executionOrder: 0
8
8
  icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: