com.wallstop-studios.dxmessaging 2.0.0-rc16 → 2.0.0-rc17
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.
- package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
- package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.pdb.meta +7 -0
- package/Editor/SetupCscRsp.cs +24 -7
- package/README.md +7 -17
- package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs +5 -1
- package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs +5 -1
- package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs +5 -1
- package/Runtime/Core/IMessage.cs +13 -0
- package/Runtime/Core/InstanceId.cs +6 -16
- package/Runtime/Core/MessageBus/MessageBus.cs +1751 -828
- package/Runtime/Core/MessageHandler.cs +408 -292
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +445 -0
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +3 -0
- package/Tests/Runtime/Benchmarks/PerformanceTests.cs +41 -26
- package/Tests/Runtime/Scripts/Components/GenericMessageAwareComponent.cs +19 -0
- package/Tests/Runtime/Scripts/Components/GenericMessageAwareComponent.cs.meta +3 -0
- package/Tests/Runtime/Scripts/Messages/GenericUntargetedMessage.cs +7 -0
- package/Tests/Runtime/Scripts/Messages/GenericUntargetedMessage.cs.meta +3 -0
- package/package.json +2 -2
- package/Runtime/Core/Attributes/DxAutoMessageTypeAttribute.cs +0 -7
- package/Runtime/Core/Attributes/DxAutoMessageTypeAttribute.cs.meta +0 -3
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoMessageTypeGenerator.cs +0 -92
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoMessageTypeGenerator.cs.meta +0 -11
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxBroadcastMessageGenerator.cs +0 -94
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxBroadcastMessageGenerator.cs.meta +0 -3
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxTargetedMessageGenerator.cs +0 -94
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxTargetedMessageGenerator.cs.meta +0 -3
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxUntargetedMessageGenerator.cs +0 -94
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxUntargetedMessageGenerator.cs.meta +0 -3
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
namespace WallstopStudios.DxMessaging.SourceGenerators;
|
|
2
|
+
|
|
3
|
+
using System;
|
|
4
|
+
using System.Collections.Generic;
|
|
5
|
+
using System.Collections.Immutable;
|
|
6
|
+
using System.Linq;
|
|
7
|
+
using System.Text;
|
|
8
|
+
using System.Threading;
|
|
9
|
+
using Microsoft.CodeAnalysis;
|
|
10
|
+
using Microsoft.CodeAnalysis.CSharp;
|
|
11
|
+
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
12
|
+
using Microsoft.CodeAnalysis.Text;
|
|
13
|
+
|
|
14
|
+
[Generator(LanguageNames.CSharp)]
|
|
15
|
+
public sealed class DxMessageIdGenerator : IIncrementalGenerator
|
|
16
|
+
{
|
|
17
|
+
// --- Constants for Attributes and Interfaces ---
|
|
18
|
+
private const string BaseInterfaceFullName = "DxMessaging.Core.IMessage"; // Base interface
|
|
19
|
+
|
|
20
|
+
private const string BroadcastAttrFullName =
|
|
21
|
+
"DxMessaging.Core.Attributes.DxBroadcastMessageAttribute";
|
|
22
|
+
private const string TargetedAttrFullName =
|
|
23
|
+
"DxMessaging.Core.Attributes.DxTargetedMessageAttribute";
|
|
24
|
+
private const string UntargetedAttrFullName =
|
|
25
|
+
"DxMessaging.Core.Attributes.DxUntargetedMessageAttribute";
|
|
26
|
+
|
|
27
|
+
private const string BroadcastInterfaceFullName = "DxMessaging.Core.Messages.IBroadcastMessage";
|
|
28
|
+
private const string TargetedInterfaceFullName = "DxMessaging.Core.Messages.ITargetedMessage";
|
|
29
|
+
private const string UntargetedInterfaceFullName =
|
|
30
|
+
"DxMessaging.Core.Messages.IUntargetedMessage";
|
|
31
|
+
|
|
32
|
+
// --- Diagnostics ---
|
|
33
|
+
private static readonly DiagnosticDescriptor CollisionError = new DiagnosticDescriptor(
|
|
34
|
+
id: "DXMSG001",
|
|
35
|
+
title: "Message ID Collision",
|
|
36
|
+
messageFormat: "OptimizedMessageId collision detected across different message types. The generated ID '{0}' is shared by the following types: {1}. Please rename one or more types slightly to resolve the hash collision.",
|
|
37
|
+
category: "DxMessaging",
|
|
38
|
+
defaultSeverity: DiagnosticSeverity.Error,
|
|
39
|
+
isEnabledByDefault: true
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
private static readonly DiagnosticDescriptor CollisionWarning = new DiagnosticDescriptor(
|
|
43
|
+
id: "DXMSG003", // New ID
|
|
44
|
+
title: "Message ID Collision Fallback",
|
|
45
|
+
messageFormat: "OptimizedMessageId collision detected for ID '{0}'. The following types will use a fallback implementation (HasOptimizedId = false): {1}.",
|
|
46
|
+
category: "DxMessaging",
|
|
47
|
+
defaultSeverity: DiagnosticSeverity.Warning, // Set severity to Warning
|
|
48
|
+
isEnabledByDefault: true
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
private static readonly DiagnosticDescriptor MultipleAttributesError = new DiagnosticDescriptor(
|
|
52
|
+
id: "DXMSG002",
|
|
53
|
+
title: "Multiple Message Attributes",
|
|
54
|
+
messageFormat: "Type '{0}' cannot have more than one Dx message attribute ([DxBroadcastMessage], [DxTargetedMessage], [DxUntargetedMessage]).",
|
|
55
|
+
category: "DxMessaging",
|
|
56
|
+
defaultSeverity: DiagnosticSeverity.Error,
|
|
57
|
+
isEnabledByDefault: true
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Helper record to pass data through the pipeline
|
|
61
|
+
private record struct SemanticTargetInfo(
|
|
62
|
+
INamedTypeSymbol TypeSymbol,
|
|
63
|
+
TypeDeclarationSyntax DeclarationSyntax,
|
|
64
|
+
string TargetInterfaceFullName
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Helper record for final processing stage
|
|
68
|
+
private record struct MessageInfo(
|
|
69
|
+
INamedTypeSymbol TypeSymbol,
|
|
70
|
+
TypeDeclarationSyntax DeclarationSyntax,
|
|
71
|
+
string TargetInterfaceFullName,
|
|
72
|
+
int GeneratedId
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Helper record for final assignment stage
|
|
76
|
+
private record struct FinalMessageInfo(
|
|
77
|
+
INamedTypeSymbol TypeSymbol,
|
|
78
|
+
TypeDeclarationSyntax DeclarationSyntax,
|
|
79
|
+
string TargetInterfaceFullName,
|
|
80
|
+
int FinalId, // The ID actually assigned (hash or fallback)
|
|
81
|
+
bool WasCollision
|
|
82
|
+
); // Flag indicating if it was part of a collision
|
|
83
|
+
|
|
84
|
+
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
85
|
+
{
|
|
86
|
+
// --- Step 1: Find all classes/structs with attribute lists (Potential Candidates) ---
|
|
87
|
+
IncrementalValuesProvider<TypeDeclarationSyntax> potentialTypeDeclarations =
|
|
88
|
+
context.SyntaxProvider.CreateSyntaxProvider(
|
|
89
|
+
predicate: static (node, _) => IsSyntaxTargetForGeneration(node), // Quick syntax filter
|
|
90
|
+
transform: static (ctx, ct) => (TypeDeclarationSyntax)ctx.Node
|
|
91
|
+
); // Just pass the node
|
|
92
|
+
|
|
93
|
+
// --- Step 2: Get Semantic Info and Filter by Attributes ---
|
|
94
|
+
IncrementalValuesProvider<SemanticTargetInfo?> semanticTargets = potentialTypeDeclarations
|
|
95
|
+
.Select(static (typeDecl, ct) => new { typeDecl, ct }) // Combine with CancellationToken if needed implicitly by GetSemanticTargetForGeneration
|
|
96
|
+
.Combine(context.CompilationProvider)
|
|
97
|
+
.Select(
|
|
98
|
+
static (data, ct) =>
|
|
99
|
+
GetSemanticTargetForGeneration(data.Left.typeDecl, data.Right, ct)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// --- Step 3: Filter out invalid targets ---
|
|
103
|
+
IncrementalValuesProvider<SemanticTargetInfo> validSemanticTargets = semanticTargets
|
|
104
|
+
.Where(static target => target.HasValue)
|
|
105
|
+
.Select(static (target, _) => target!.Value); // Use non-null assertion or keep filtering
|
|
106
|
+
|
|
107
|
+
// --- Step 4: Calculate Hash IDs ---
|
|
108
|
+
IncrementalValuesProvider<MessageInfo> typesWithIds = validSemanticTargets.Select(
|
|
109
|
+
static (target, ct) =>
|
|
110
|
+
{
|
|
111
|
+
string fullyQualifiedName = target.TypeSymbol.ToDisplayString(
|
|
112
|
+
SymbolDisplayFormat.FullyQualifiedFormat
|
|
113
|
+
);
|
|
114
|
+
int generatedId = ComputeStableHashCode(fullyQualifiedName);
|
|
115
|
+
return new MessageInfo(
|
|
116
|
+
target.TypeSymbol,
|
|
117
|
+
target.DeclarationSyntax,
|
|
118
|
+
target.TargetInterfaceFullName,
|
|
119
|
+
generatedId
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// --- Step 5: Collect all valid types with IDs ---
|
|
125
|
+
IncrementalValueProvider<(Compilation, ImmutableArray<MessageInfo>)> compilationAndTypes =
|
|
126
|
+
context.CompilationProvider.Combine(typesWithIds.Collect());
|
|
127
|
+
|
|
128
|
+
// --- Step 6: Generate source or diagnostics ---
|
|
129
|
+
context.RegisterSourceOutput(
|
|
130
|
+
compilationAndTypes,
|
|
131
|
+
static (spc, source) => Execute(source.Item1, source.Item2, spc)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Quick syntax filter: Checks if the node is a class or struct with any attributes
|
|
136
|
+
private static bool IsSyntaxTargetForGeneration(SyntaxNode node) =>
|
|
137
|
+
node is TypeDeclarationSyntax { AttributeLists.Count: > 0 } typeDecl
|
|
138
|
+
&& (
|
|
139
|
+
typeDecl.IsKind(SyntaxKind.ClassDeclaration)
|
|
140
|
+
|| typeDecl.IsKind(SyntaxKind.StructDeclaration)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Semantic filter: Checks if the type has exactly one of the target attributes
|
|
144
|
+
private static SemanticTargetInfo? GetSemanticTargetForGeneration(
|
|
145
|
+
TypeDeclarationSyntax typeDeclarationSyntax,
|
|
146
|
+
Compilation compilation,
|
|
147
|
+
CancellationToken cancellationToken
|
|
148
|
+
)
|
|
149
|
+
{
|
|
150
|
+
SemanticModel semanticModel = compilation.GetSemanticModel(
|
|
151
|
+
typeDeclarationSyntax.SyntaxTree
|
|
152
|
+
);
|
|
153
|
+
if (
|
|
154
|
+
semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, cancellationToken)
|
|
155
|
+
is not INamedTypeSymbol typeSymbol
|
|
156
|
+
)
|
|
157
|
+
{
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
string? foundTargetInterface = null;
|
|
162
|
+
bool multipleAttributes = false;
|
|
163
|
+
|
|
164
|
+
foreach (AttributeData attributeData in typeSymbol.GetAttributes())
|
|
165
|
+
{
|
|
166
|
+
cancellationToken.ThrowIfCancellationRequested();
|
|
167
|
+
if (attributeData.AttributeClass == null)
|
|
168
|
+
continue;
|
|
169
|
+
|
|
170
|
+
string currentAttributeFullName = attributeData.AttributeClass.ToDisplayString();
|
|
171
|
+
string? targetInterfaceForThisAttribute = null;
|
|
172
|
+
|
|
173
|
+
switch (currentAttributeFullName)
|
|
174
|
+
{
|
|
175
|
+
case BroadcastAttrFullName:
|
|
176
|
+
targetInterfaceForThisAttribute = BroadcastInterfaceFullName;
|
|
177
|
+
break;
|
|
178
|
+
case TargetedAttrFullName:
|
|
179
|
+
targetInterfaceForThisAttribute = TargetedInterfaceFullName;
|
|
180
|
+
break;
|
|
181
|
+
case UntargetedAttrFullName:
|
|
182
|
+
targetInterfaceForThisAttribute = UntargetedInterfaceFullName;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (targetInterfaceForThisAttribute != null)
|
|
187
|
+
{
|
|
188
|
+
if (foundTargetInterface != null)
|
|
189
|
+
{
|
|
190
|
+
// Found more than one relevant attribute!
|
|
191
|
+
multipleAttributes = true;
|
|
192
|
+
break; // No need to check further attributes
|
|
193
|
+
}
|
|
194
|
+
foundTargetInterface = targetInterfaceForThisAttribute;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (multipleAttributes || foundTargetInterface == null)
|
|
199
|
+
{
|
|
200
|
+
// Report error for multiple attributes later in Execute if needed,
|
|
201
|
+
// but don't consider this a valid target for generation now.
|
|
202
|
+
// If foundTargetInterface is null, it didn't have any relevant attribute.
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Found exactly one relevant attribute
|
|
207
|
+
return new SemanticTargetInfo(typeSymbol, typeDeclarationSyntax, foundTargetInterface);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --- Main execution logic: collision check, warning, and source generation ---
|
|
211
|
+
private static void Execute(
|
|
212
|
+
Compilation compilation,
|
|
213
|
+
ImmutableArray<MessageInfo> typesToGenerate,
|
|
214
|
+
SourceProductionContext context
|
|
215
|
+
)
|
|
216
|
+
{
|
|
217
|
+
if (typesToGenerate.IsDefaultOrEmpty)
|
|
218
|
+
{
|
|
219
|
+
return; // Nothing to do
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var validTypes = new List<MessageInfo>(typesToGenerate.Length);
|
|
223
|
+
var typesWithMultipleAttributes = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
|
|
224
|
+
var generatedIds = new HashSet<int>();
|
|
225
|
+
|
|
226
|
+
// --- Step 1: Check for Multiple Attributes on the same type ---
|
|
227
|
+
var groupedByType = typesToGenerate.GroupBy(
|
|
228
|
+
m => m.TypeSymbol,
|
|
229
|
+
SymbolEqualityComparer.Default
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
foreach (var group in groupedByType)
|
|
233
|
+
{
|
|
234
|
+
if (group.Count() > 1)
|
|
235
|
+
{
|
|
236
|
+
ISymbol collidingSymbol = group.Key;
|
|
237
|
+
typesWithMultipleAttributes.Add(collidingSymbol);
|
|
238
|
+
context.ReportDiagnostic(
|
|
239
|
+
Diagnostic.Create(
|
|
240
|
+
MultipleAttributesError,
|
|
241
|
+
collidingSymbol.Locations.FirstOrDefault() ?? Location.None,
|
|
242
|
+
collidingSymbol.ToDisplayString()
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
else
|
|
247
|
+
{
|
|
248
|
+
// Add types with only one attribute to the list for further processing
|
|
249
|
+
validTypes.Add(group.First());
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// If no types remain after filtering multi-attribute ones, exit
|
|
254
|
+
if (validTypes.Count == 0)
|
|
255
|
+
{
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Step 2: Assign Final IDs (Handling Collisions) ---
|
|
260
|
+
var finalAssignments = new List<FinalMessageInfo>(validTypes.Count);
|
|
261
|
+
var collisionWarnings = new List<Diagnostic>(); // Collect warnings
|
|
262
|
+
int fallbackIdCounter = -1; // Start fallback IDs from -1 and go down
|
|
263
|
+
|
|
264
|
+
// Group by the initial hash ID to find collisions
|
|
265
|
+
var groupedByHash = validTypes.GroupBy(m => m.GeneratedId);
|
|
266
|
+
|
|
267
|
+
foreach (var group in groupedByHash)
|
|
268
|
+
{
|
|
269
|
+
if (group.Count() == 1)
|
|
270
|
+
{
|
|
271
|
+
// No collision for this hash ID
|
|
272
|
+
var info = group.First();
|
|
273
|
+
generatedIds.Add(info.GeneratedId);
|
|
274
|
+
finalAssignments.Add(
|
|
275
|
+
new FinalMessageInfo(
|
|
276
|
+
info.TypeSymbol,
|
|
277
|
+
info.DeclarationSyntax,
|
|
278
|
+
info.TargetInterfaceFullName,
|
|
279
|
+
info.GeneratedId,
|
|
280
|
+
false
|
|
281
|
+
)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
else
|
|
285
|
+
{
|
|
286
|
+
// Collision detected for this hash ID!
|
|
287
|
+
// Sort colliding types deterministically (e.g., by fully qualified name)
|
|
288
|
+
var sortedCollidingTypes = group
|
|
289
|
+
.OrderBy(
|
|
290
|
+
m => m.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
|
291
|
+
StringComparer.Ordinal
|
|
292
|
+
)
|
|
293
|
+
.ToList();
|
|
294
|
+
|
|
295
|
+
// The first type keeps the original hash ID (arbitrary but deterministic choice)
|
|
296
|
+
var firstInfo = sortedCollidingTypes[0];
|
|
297
|
+
generatedIds.Add(firstInfo.GeneratedId);
|
|
298
|
+
finalAssignments.Add(
|
|
299
|
+
new FinalMessageInfo(
|
|
300
|
+
firstInfo.TypeSymbol,
|
|
301
|
+
firstInfo.DeclarationSyntax,
|
|
302
|
+
firstInfo.TargetInterfaceFullName,
|
|
303
|
+
firstInfo.GeneratedId,
|
|
304
|
+
true
|
|
305
|
+
)
|
|
306
|
+
); // Mark as part of collision
|
|
307
|
+
// Prepare warning for this type (kept original ID but was part of collision)
|
|
308
|
+
collisionWarnings.Add(
|
|
309
|
+
Diagnostic.Create(
|
|
310
|
+
CollisionWarning,
|
|
311
|
+
firstInfo.DeclarationSyntax.Identifier.GetLocation(),
|
|
312
|
+
firstInfo.TypeSymbol.ToDisplayString(),
|
|
313
|
+
firstInfo.GeneratedId,
|
|
314
|
+
group.Key
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Assign sequential fallback IDs (negative numbers) to the rest
|
|
319
|
+
for (int i = 1; i < sortedCollidingTypes.Count; i++)
|
|
320
|
+
{
|
|
321
|
+
var fallbackInfo = sortedCollidingTypes[i];
|
|
322
|
+
int assignedFallbackId = fallbackIdCounter--; // Assign next negative ID
|
|
323
|
+
while (!generatedIds.Add(assignedFallbackId))
|
|
324
|
+
{
|
|
325
|
+
assignedFallbackId--;
|
|
326
|
+
}
|
|
327
|
+
finalAssignments.Add(
|
|
328
|
+
new FinalMessageInfo(
|
|
329
|
+
fallbackInfo.TypeSymbol,
|
|
330
|
+
fallbackInfo.DeclarationSyntax,
|
|
331
|
+
fallbackInfo.TargetInterfaceFullName,
|
|
332
|
+
assignedFallbackId,
|
|
333
|
+
true
|
|
334
|
+
)
|
|
335
|
+
); // Mark as part of collision, use new ID
|
|
336
|
+
// Prepare warning for this type (received fallback ID)
|
|
337
|
+
collisionWarnings.Add(
|
|
338
|
+
Diagnostic.Create(
|
|
339
|
+
CollisionWarning,
|
|
340
|
+
fallbackInfo.DeclarationSyntax.Identifier.GetLocation(),
|
|
341
|
+
fallbackInfo.TypeSymbol.ToDisplayString(),
|
|
342
|
+
assignedFallbackId,
|
|
343
|
+
group.Key
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- Step 3: Report all collected warnings ---
|
|
351
|
+
foreach (var warning in collisionWarnings)
|
|
352
|
+
{
|
|
353
|
+
context.ReportDiagnostic(warning);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// --- Step 4: Generate Source using the final assigned IDs ---
|
|
357
|
+
foreach (var finalInfo in finalAssignments)
|
|
358
|
+
{
|
|
359
|
+
context.CancellationToken.ThrowIfCancellationRequested();
|
|
360
|
+
|
|
361
|
+
// Generate source using the final ID (hash or fallback)
|
|
362
|
+
string source = GenerateSource( // Call simplified GenerateSource
|
|
363
|
+
finalInfo.TargetInterfaceFullName,
|
|
364
|
+
finalInfo.TypeSymbol,
|
|
365
|
+
finalInfo.FinalId
|
|
366
|
+
); // Pass the FINAL ID
|
|
367
|
+
|
|
368
|
+
context.AddSource(
|
|
369
|
+
$"{finalInfo.TypeSymbol.Name}_{finalInfo.TargetInterfaceFullName.Split('.').Last()}.g.cs",
|
|
370
|
+
SourceText.From(source, Encoding.UTF8)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// --- Stable Hash Function (FNV-1a - remains the same) ---
|
|
376
|
+
private static int ComputeStableHashCode(string text)
|
|
377
|
+
{
|
|
378
|
+
unchecked
|
|
379
|
+
{
|
|
380
|
+
uint hash = 2166136261;
|
|
381
|
+
foreach (char c in text)
|
|
382
|
+
{
|
|
383
|
+
hash = (hash ^ c) * 16777619;
|
|
384
|
+
}
|
|
385
|
+
return (int)hash;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// --- Source Generation Logic (Takes target interface name AND optimization flag) ---
|
|
390
|
+
private static string GenerateSource(
|
|
391
|
+
string targetInterfaceFullName,
|
|
392
|
+
INamedTypeSymbol typeSymbol,
|
|
393
|
+
int finalAssignedId
|
|
394
|
+
) // Now takes the final assigned ID
|
|
395
|
+
{
|
|
396
|
+
string namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace
|
|
397
|
+
? string.Empty
|
|
398
|
+
: typeSymbol.ContainingNamespace.ToDisplayString();
|
|
399
|
+
|
|
400
|
+
string typeNameWithGenerics = typeSymbol.ToDisplayString(
|
|
401
|
+
SymbolDisplayFormat.MinimallyQualifiedFormat
|
|
402
|
+
);
|
|
403
|
+
string fullyQualifiedName = typeSymbol.ToDisplayString(
|
|
404
|
+
SymbolDisplayFormat.FullyQualifiedFormat
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
string typeKind = typeSymbol.TypeKind switch
|
|
408
|
+
{
|
|
409
|
+
TypeKind.Class => "class",
|
|
410
|
+
TypeKind.Struct => "struct",
|
|
411
|
+
_ => throw new InvalidOperationException(
|
|
412
|
+
"Unsupported type kind for message generation"
|
|
413
|
+
),
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
bool alreadyDeclaresInterface = typeSymbol.Interfaces.Any(iface =>
|
|
417
|
+
iface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
|
418
|
+
== targetInterfaceFullName
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
string interfaceDeclaration = alreadyDeclaresInterface
|
|
422
|
+
? ""
|
|
423
|
+
: $": {targetInterfaceFullName}";
|
|
424
|
+
|
|
425
|
+
return $$"""
|
|
426
|
+
// <auto-generated by DxMessageIdGenerator/>
|
|
427
|
+
#pragma warning disable
|
|
428
|
+
#nullable enable annotations
|
|
429
|
+
|
|
430
|
+
namespace {{namespaceName}}
|
|
431
|
+
{
|
|
432
|
+
partial {{typeKind}} {{typeNameWithGenerics}} {{interfaceDeclaration}}
|
|
433
|
+
{
|
|
434
|
+
// Explicitly implement IMessage members using the base interface name
|
|
435
|
+
|
|
436
|
+
/// <inheritdoc/>
|
|
437
|
+
System.Type {{BaseInterfaceFullName}}.MessageType => typeof({{fullyQualifiedName}});
|
|
438
|
+
|
|
439
|
+
/// <inheritdoc/>
|
|
440
|
+
int? {{BaseInterfaceFullName}}.OptimizedMessageId => {{finalAssignedId}};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
""";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -17,12 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
public sealed class PerformanceTests : MessagingTestBase
|
|
19
19
|
{
|
|
20
|
+
private const int NumInvocationsPerIteration = 250;
|
|
21
|
+
|
|
20
22
|
protected override bool MessagingDebugEnabled => false;
|
|
21
23
|
|
|
22
24
|
[Test]
|
|
23
25
|
public void Benchmark()
|
|
24
26
|
{
|
|
25
|
-
TimeSpan timeout = TimeSpan.FromSeconds(
|
|
27
|
+
TimeSpan timeout = TimeSpan.FromSeconds(5);
|
|
26
28
|
|
|
27
29
|
Debug.Log("| Message Tech | Operations / Second | Allocations? |");
|
|
28
30
|
Debug.Log("| ------------ | ------------------- | ------------ | ");
|
|
@@ -190,10 +192,13 @@
|
|
|
190
192
|
timer.Restart();
|
|
191
193
|
do
|
|
192
194
|
{
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
for (int i = 0; i < NumInvocationsPerIteration; ++i)
|
|
196
|
+
{
|
|
197
|
+
target.SendMessage(
|
|
198
|
+
nameof(SimpleMessageAwareComponent.HandleSlowComplexTargetedMessage),
|
|
199
|
+
message
|
|
200
|
+
);
|
|
201
|
+
}
|
|
197
202
|
} while (timer.Elapsed < timeout);
|
|
198
203
|
|
|
199
204
|
bool allocating;
|
|
@@ -227,19 +232,23 @@
|
|
|
227
232
|
var token = GetToken(component);
|
|
228
233
|
|
|
229
234
|
GameObject go = component.gameObject;
|
|
235
|
+
InstanceId target = go;
|
|
230
236
|
token.RegisterGameObjectTargeted<ComplexTargetedMessage>(go, Handle);
|
|
231
237
|
// Pre-warm
|
|
232
|
-
message.
|
|
238
|
+
message.EmitTargeted(target);
|
|
233
239
|
|
|
234
240
|
timer.Restart();
|
|
235
241
|
do
|
|
236
242
|
{
|
|
237
|
-
|
|
243
|
+
for (int i = 0; i < NumInvocationsPerIteration; ++i)
|
|
244
|
+
{
|
|
245
|
+
message.EmitTargeted(target);
|
|
246
|
+
}
|
|
238
247
|
} while (timer.Elapsed < timeout);
|
|
239
248
|
bool allocating;
|
|
240
249
|
try
|
|
241
250
|
{
|
|
242
|
-
Assert.That(() => message.
|
|
251
|
+
Assert.That(() => message.EmitTargeted(target), Is.Not.AllocatingGCMemory());
|
|
243
252
|
allocating = false;
|
|
244
253
|
}
|
|
245
254
|
catch
|
|
@@ -265,24 +274,25 @@
|
|
|
265
274
|
{
|
|
266
275
|
int count = 0;
|
|
267
276
|
var token = GetToken(component);
|
|
277
|
+
InstanceId target = component;
|
|
268
278
|
|
|
269
279
|
token.RegisterComponentTargeted<ComplexTargetedMessage>(component, Handle);
|
|
270
280
|
// Pre-warm
|
|
271
|
-
message.
|
|
281
|
+
message.EmitTargeted(target);
|
|
272
282
|
|
|
273
283
|
timer.Restart();
|
|
274
284
|
do
|
|
275
285
|
{
|
|
276
|
-
|
|
286
|
+
for (int i = 0; i < NumInvocationsPerIteration; ++i)
|
|
287
|
+
{
|
|
288
|
+
message.EmitTargeted(target);
|
|
289
|
+
}
|
|
277
290
|
} while (timer.Elapsed < timeout);
|
|
278
291
|
|
|
279
292
|
bool allocating;
|
|
280
293
|
try
|
|
281
294
|
{
|
|
282
|
-
Assert.That(
|
|
283
|
-
() => message.EmitComponentTargeted(component),
|
|
284
|
-
Is.Not.AllocatingGCMemory()
|
|
285
|
-
);
|
|
295
|
+
Assert.That(() => message.EmitTargeted(target), Is.Not.AllocatingGCMemory());
|
|
286
296
|
allocating = false;
|
|
287
297
|
}
|
|
288
298
|
catch
|
|
@@ -309,22 +319,23 @@
|
|
|
309
319
|
var token = GetToken(component);
|
|
310
320
|
|
|
311
321
|
GameObject go = component.gameObject;
|
|
322
|
+
InstanceId target = go;
|
|
312
323
|
token.RegisterGameObjectTargeted<ComplexTargetedMessage>(go, Handle);
|
|
313
324
|
// Pre-warm
|
|
314
|
-
message.
|
|
325
|
+
message.EmitTargeted(target);
|
|
315
326
|
|
|
316
327
|
timer.Restart();
|
|
317
328
|
do
|
|
318
329
|
{
|
|
319
|
-
|
|
330
|
+
for (int i = 0; i < NumInvocationsPerIteration; ++i)
|
|
331
|
+
{
|
|
332
|
+
message.EmitTargeted(target);
|
|
333
|
+
}
|
|
320
334
|
} while (timer.Elapsed < timeout);
|
|
321
335
|
bool allocating;
|
|
322
336
|
try
|
|
323
337
|
{
|
|
324
|
-
Assert.That(
|
|
325
|
-
() => message.EmitGameObjectTargeted(component.gameObject),
|
|
326
|
-
Is.Not.AllocatingGCMemory()
|
|
327
|
-
);
|
|
338
|
+
Assert.That(() => message.EmitTargeted(target), Is.Not.AllocatingGCMemory());
|
|
328
339
|
allocating = false;
|
|
329
340
|
}
|
|
330
341
|
catch
|
|
@@ -349,6 +360,7 @@
|
|
|
349
360
|
{
|
|
350
361
|
int count = 0;
|
|
351
362
|
var token = GetToken(component);
|
|
363
|
+
InstanceId target = component;
|
|
352
364
|
|
|
353
365
|
token.RegisterComponentTargeted<ComplexTargetedMessage>(component, Handle);
|
|
354
366
|
// Pre-warm
|
|
@@ -357,16 +369,16 @@
|
|
|
357
369
|
timer.Restart();
|
|
358
370
|
do
|
|
359
371
|
{
|
|
360
|
-
|
|
372
|
+
for (int i = 0; i < NumInvocationsPerIteration; ++i)
|
|
373
|
+
{
|
|
374
|
+
message.EmitTargeted(target);
|
|
375
|
+
}
|
|
361
376
|
} while (timer.Elapsed < timeout);
|
|
362
377
|
|
|
363
378
|
bool allocating;
|
|
364
379
|
try
|
|
365
380
|
{
|
|
366
|
-
Assert.That(
|
|
367
|
-
() => message.EmitComponentTargeted(component),
|
|
368
|
-
Is.Not.AllocatingGCMemory()
|
|
369
|
-
);
|
|
381
|
+
Assert.That(() => message.EmitTargeted(target), Is.Not.AllocatingGCMemory());
|
|
370
382
|
allocating = false;
|
|
371
383
|
}
|
|
372
384
|
catch
|
|
@@ -399,7 +411,10 @@
|
|
|
399
411
|
timer.Restart();
|
|
400
412
|
do
|
|
401
413
|
{
|
|
402
|
-
|
|
414
|
+
for (int i = 0; i < NumInvocationsPerIteration; ++i)
|
|
415
|
+
{
|
|
416
|
+
message.EmitUntargeted();
|
|
417
|
+
}
|
|
403
418
|
} while (timer.Elapsed < timeout);
|
|
404
419
|
|
|
405
420
|
bool allocating;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
namespace DxMessaging.Tests.Runtime.Scripts.Components
|
|
2
|
+
{
|
|
3
|
+
using Messages;
|
|
4
|
+
using Unity;
|
|
5
|
+
using UnityEngine;
|
|
6
|
+
|
|
7
|
+
[DisallowMultipleComponent]
|
|
8
|
+
public sealed class GenericMessageAwareComponent : MessageAwareComponent
|
|
9
|
+
{
|
|
10
|
+
protected override void RegisterMessageHandlers()
|
|
11
|
+
{
|
|
12
|
+
base.RegisterMessageHandlers();
|
|
13
|
+
_ = _messageRegistrationToken.RegisterUntargeted(
|
|
14
|
+
(ref GenericUntargetedMessage<int> message) =>
|
|
15
|
+
Debug.Log("Received generic int message.")
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.wallstop-studios.dxmessaging",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-rc17",
|
|
4
4
|
"displayName": "DxMessaging",
|
|
5
5
|
"description": "Synchronous Event Bus for Unity",
|
|
6
6
|
"unity": "2021.3",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"com.wallstop-studios.unity-helpers" : "2.0.0-
|
|
8
|
+
"com.wallstop-studios.unity-helpers" : "2.0.0-rc58"
|
|
9
9
|
},
|
|
10
10
|
"keywords": [
|
|
11
11
|
"messaging",
|