Skip to content

Commit 660c224

Browse files
committed
[Core] Support custom ECS
1 parent 57d67eb commit 660c224

137 files changed

Lines changed: 639 additions & 400 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Lagrange.Core.Generator/EventLogicSourceGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Lagrange.Core.Generator;
1111
public sealed class EventLogicSourceGenerator : IIncrementalGenerator
1212
{
1313
private const string LogicInterfaceFullName = "Lagrange.Core.Internal.Logic.ILogic";
14-
private const string EventSubscribeAttributeNamespace = "Lagrange.Core.Internal.Events";
14+
private const string EventSubscribeAttributeNamespace = "Lagrange.Core.Services";
1515
private const string EventSubscribeAttributeName = "EventSubscribeAttribute";
1616

1717
private static readonly SymbolDisplayFormat TypeDisplayFormat = SymbolDisplayFormat.FullyQualifiedFormat;

Lagrange.Core.Generator/ServiceSourceGenerator.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ namespace Lagrange.Core.Generator;
1010
[Generator(LanguageNames.CSharp)]
1111
public sealed class ServiceSourceGenerator : IIncrementalGenerator
1212
{
13-
private const string ServiceAttributeFullName = "Lagrange.Core.Internal.Services.ServiceAttribute";
14-
private const string ServiceInterfaceFullName = "Lagrange.Core.Internal.Services.IService";
15-
private const string EventSubscribeAttributeNamespace = "Lagrange.Core.Internal.Events";
13+
private const string ServiceAttributeFullName = "Lagrange.Core.Services.ServiceAttribute";
14+
private const string ServiceInterfaceFullName = "Lagrange.Core.Services.IService";
15+
private const string EventSubscribeAttributeNamespace = "Lagrange.Core.Services";
1616
private const string EventSubscribeAttributeName = "EventSubscribeAttribute";
1717
private const long DefaultRequestType = 0x0C;
1818
private const long DefaultEncryptType = 0x01;
@@ -37,7 +37,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3737
if (context.TargetSymbol is not INamedTypeSymbol { TypeKind: TypeKind.Class } typeSymbol || typeSymbol.IsAbstract) return null;
3838

3939
var serviceInterface = context.SemanticModel.Compilation.GetTypeByMetadataName(ServiceInterfaceFullName);
40-
if (serviceInterface is null || !typeSymbol.AllInterfaces.Any(type => SymbolEqualityComparer.Default.Equals(type, serviceInterface))) return null;
40+
if (serviceInterface is null || !typeSymbol.AllInterfaces.Any(type =>
41+
SymbolEqualityComparer.Default.Equals(type, serviceInterface))) return null;
4142

4243
var serviceAttribute = context.Attributes[0];
4344
if (serviceAttribute.ConstructorArguments.Length == 0 || serviceAttribute.ConstructorArguments[0].Value is not string command) return null;
@@ -120,10 +121,10 @@ private static void Output(SourceProductionContext context, ImmutableArray<Servi
120121
source.AppendLine();
121122
source.AppendLine("internal static class ServiceRegistry");
122123
source.AppendLine("{");
123-
source.AppendLine(" internal static (global::System.Collections.Frozen.FrozenDictionary<string, global::Lagrange.Core.Internal.Services.IService> Services, global::System.Collections.Frozen.FrozenDictionary<global::System.Type, (global::Lagrange.Core.Internal.Services.ServiceAttribute Attribute, global::Lagrange.Core.Internal.Services.IService Instance)> ServicesEventType) Create(global::Lagrange.Core.Common.Protocols protocol, global::System.Collections.Generic.HashSet<string> disabledLog)");
124+
source.AppendLine(" internal static (global::System.Collections.Frozen.FrozenDictionary<string, global::Lagrange.Core.Services.IService> Services, global::System.Collections.Frozen.FrozenDictionary<global::System.Type, (global::Lagrange.Core.Services.ServiceAttribute Attribute, global::Lagrange.Core.Services.IService Instance)> ServicesEventType) Create(global::Lagrange.Core.Common.Protocols protocol, global::System.Collections.Generic.HashSet<string> disabledLog)");
124125
source.AppendLine(" {");
125-
source.AppendLine(" var services = new global::System.Collections.Generic.Dictionary<string, global::Lagrange.Core.Internal.Services.IService>();");
126-
source.AppendLine(" var servicesEventType = new global::System.Collections.Generic.Dictionary<global::System.Type, (global::Lagrange.Core.Internal.Services.ServiceAttribute Attribute, global::Lagrange.Core.Internal.Services.IService Instance)>();");
126+
source.AppendLine(" var services = new global::System.Collections.Generic.Dictionary<string, global::Lagrange.Core.Services.IService>();");
127+
source.AppendLine(" var servicesEventType = new global::System.Collections.Generic.Dictionary<global::System.Type, (global::Lagrange.Core.Services.ServiceAttribute Attribute, global::Lagrange.Core.Services.IService Instance)>();");
127128
source.AppendLine();
128129

129130
for (int i = 0; i < orderedServices.Count; i++)
@@ -135,11 +136,11 @@ private static void Output(SourceProductionContext context, ImmutableArray<Servi
135136
source.AppendLine(" return (services.ToFrozenDictionary(), servicesEventType.ToFrozenDictionary());");
136137
source.AppendLine(" }");
137138
source.AppendLine();
138-
source.AppendLine(" private static void AddEvent(global::System.Type eventType, global::Lagrange.Core.Internal.Services.ServiceAttribute attribute, global::Lagrange.Core.Internal.Services.IService service, global::System.Collections.Generic.Dictionary<global::System.Type, (global::Lagrange.Core.Internal.Services.ServiceAttribute Attribute, global::Lagrange.Core.Internal.Services.IService Instance)> servicesEventType)");
139+
source.AppendLine(" private static void AddEvent(global::System.Type eventType, global::Lagrange.Core.Services.ServiceAttribute attribute, global::Lagrange.Core.Services.IService service, global::System.Collections.Generic.Dictionary<global::System.Type, (global::Lagrange.Core.Services.ServiceAttribute Attribute, global::Lagrange.Core.Services.IService Instance)> servicesEventType)");
139140
source.AppendLine(" {");
140141
source.AppendLine(" if (servicesEventType.ContainsKey(eventType))");
141142
source.AppendLine(" {");
142-
source.AppendLine(" throw new global::System.InvalidOperationException($\"Multiple services for event type: {eventType}\");");
143+
source.AppendLine(" throw new global::Lagrange.Core.Exceptions.ServiceRegistrationException($\"Multiple protocol services are registered for event type '{eventType}'.\");");
143144
source.AppendLine(" }");
144145
source.AppendLine();
145146
source.AppendLine(" servicesEventType[eventType] = (attribute, service);");
@@ -158,15 +159,15 @@ private static void Output(SourceProductionContext context, ImmutableArray<Servi
158159
private static void AppendRegisterMethod(StringBuilder source, int index, ServiceInfo service)
159160
{
160161
source.AppendLine();
161-
source.Append(" private static void Register").Append(index).AppendLine("(global::Lagrange.Core.Common.Protocols protocol, global::System.Collections.Generic.HashSet<string> disabledLog, global::System.Collections.Generic.Dictionary<string, global::Lagrange.Core.Internal.Services.IService> services, global::System.Collections.Generic.Dictionary<global::System.Type, (global::Lagrange.Core.Internal.Services.ServiceAttribute Attribute, global::Lagrange.Core.Internal.Services.IService Instance)> servicesEventType)");
162+
source.Append(" private static void Register").Append(index).AppendLine("(global::Lagrange.Core.Common.Protocols protocol, global::System.Collections.Generic.HashSet<string> disabledLog, global::System.Collections.Generic.Dictionary<string, global::Lagrange.Core.Services.IService> services, global::System.Collections.Generic.Dictionary<global::System.Type, (global::Lagrange.Core.Services.ServiceAttribute Attribute, global::Lagrange.Core.Services.IService Instance)> servicesEventType)");
162163
source.AppendLine(" {");
163-
source.AppendLine(" global::Lagrange.Core.Internal.Services.IService? service = null;");
164+
source.AppendLine(" global::Lagrange.Core.Services.IService? service = null;");
164165

165166
foreach (var subscription in service.Subscriptions)
166167
{
167168
source.Append(" if ((~(global::Lagrange.Core.Common.Protocols)").Append(subscription.Protocol).AppendLine(" & protocol) == global::Lagrange.Core.Common.Protocols.None)");
168169
source.AppendLine(" {");
169-
source.Append(" var attribute = new global::Lagrange.Core.Internal.Services.ServiceAttribute(")
170+
source.Append(" var attribute = new global::Lagrange.Core.Services.ServiceAttribute(")
170171
.Append(ToLiteral(service.Command))
171172
.Append(", (global::Lagrange.Core.Common.Entity.RequestType)")
172173
.Append(service.RequestType)
@@ -179,8 +180,13 @@ private static void AppendRegisterMethod(StringBuilder source, int index, Servic
179180
}
180181

181182
source.AppendLine(";");
182-
source.AppendLine(" if (!services.TryGetValue(attribute.Command, out service))");
183+
source.AppendLine(" if (service is null)");
183184
source.AppendLine(" {");
185+
source.AppendLine(" if (services.ContainsKey(attribute.Command))");
186+
source.AppendLine(" {");
187+
source.AppendLine(" throw new global::Lagrange.Core.Exceptions.ServiceRegistrationException($\"Multiple protocol services are registered for command '{attribute.Command}'.\");");
188+
source.AppendLine(" }");
189+
source.AppendLine();
184190
source.Append(" service = new ").Append(service.ServiceType).AppendLine("();");
185191
source.AppendLine(" services[attribute.Command] = service;");
186192
source.AppendLine(" if (attribute.DisableLog) disabledLog.Add(attribute.Command);");

Lagrange.Core/BotContext.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ internal BotContext(BotConfig config, BotKeystore keystore, BotAppInfo appInfo)
3333

3434
public bool IsOnline { get; internal set; }
3535
public EventInvoker EventInvoker { get; }
36+
37+
/// <summary>
38+
/// Sends a protocol event and parses its typed response.
39+
/// </summary>
40+
public ValueTask<TResponse> SendEvent<TResponse>(ProtocolEvent request) where TResponse : ProtocolEvent =>
41+
EventContext.SendEvent<TResponse>(request);
3642

3743
internal CacheContext CacheContext { get; }
3844
internal PacketContext PacketContext { get; }
@@ -93,4 +99,4 @@ public void Dispose()
9399
SocketContext.Dispose();
94100
EventContext.Dispose();
95101
}
96-
}
102+
}

Lagrange.Core/Common/BotConfig.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Lagrange.Core.Events.EventArgs;
1+
using System.Text.Json.Serialization;
2+
using Lagrange.Core.Events.EventArgs;
3+
using Lagrange.Core.Services;
24

35
namespace Lagrange.Core.Common;
46

@@ -50,6 +52,12 @@ public class BotConfig
5052
/// The Sign Provider for the bot, if null, the bot will use the default sign provider
5153
/// </summary>
5254
public BotSignProvider? SignProvider { get; set; }
55+
56+
/// <summary>
57+
/// Custom protocol services loaded when the bot context is created.
58+
/// </summary>
59+
[JsonIgnore]
60+
public IReadOnlyList<IService> CustomServices { get; init; } = [];
5361
}
5462

5563
/// <summary>
@@ -70,4 +78,4 @@ public enum Protocols : byte
7078
PC = Windows | MacOs | Linux,
7179
Android = AndroidPhone | AndroidPad | AndroidWatch,
7280
All = Windows | MacOs | Linux | AndroidPhone | AndroidPad | AndroidWatch,
73-
}
81+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Lagrange.Core.Events;
2+
3+
/// <summary>
4+
/// Base type for protocol service requests and responses.
5+
/// </summary>
6+
public abstract class ProtocolEvent;
Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
namespace Lagrange.Core.Exceptions;
22

3-
internal class ServiceNotFoundException(string command) : LagrangeException($"Service not found for command: {command}")
3+
/// <summary>
4+
/// Thrown when no protocol service is registered for a command or event type.
5+
/// </summary>
6+
public class ServiceNotFoundException : LagrangeException
47
{
5-
public string Command { get; } = command;
6-
}
8+
public ServiceNotFoundException(string command) : base($"Protocol service not found for command: {command}")
9+
{
10+
Command = command;
11+
}
12+
13+
public ServiceNotFoundException(Type eventType) : base($"Protocol service not found for event type: {eventType}")
14+
{
15+
EventType = eventType;
16+
}
17+
18+
public string? Command { get; }
19+
20+
public Type? EventType { get; }
21+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Lagrange.Core.Exceptions;
2+
3+
/// <summary>
4+
/// Thrown when protocol service registrations are invalid or conflict.
5+
/// </summary>
6+
public class ServiceRegistrationException(string message) : LagrangeException(message);

Lagrange.Core/Internal/Context/EventContext.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Lagrange.Core.Common.Entity;
33
using Lagrange.Core.Events;
44
using Lagrange.Core.Exceptions;
5-
using Lagrange.Core.Internal.Events;
65
using Lagrange.Core.Internal.Logic;
76

87
namespace Lagrange.Core.Internal.Context;
@@ -29,19 +28,12 @@ public async ValueTask<T> SendEvent<T>(ProtocolEvent @event) where T : ProtocolE
2928
{
3029
await HandleOutgoingEvent(@event);
3130
var (frame, attribute) = await _context.ServiceContext.Resolve(@event);
32-
if (frame.Sequence == 0) throw new LagrangeException("The sequence number is 0 for the SSOFrame");
33-
34-
3531
var @return = await _context.PacketContext.SendPacket(frame, attribute);
3632
var resolved = await _context.ServiceContext.Resolve(@return);
3733

38-
if (resolved is T result)
39-
{
40-
await HandleIncomingEvent(result);
41-
return result;
42-
}
43-
44-
throw new LagrangeException($"The event type is not the same as the expected type. Expected: {typeof(T)}, Actual: {resolved.GetType()}");
34+
var result = resolved as T ?? throw new LagrangeException($"The event type is not the same as the expected type. Expected: {typeof(T)}, Actual: {resolved.GetType()}");
35+
await HandleIncomingEvent(result);
36+
return result;
4537
}
4638
catch (Exception e) when (e is not LagrangeException)
4739
{
@@ -112,7 +104,7 @@ public async Task HandleServerPacket(BotSsoPacket packet)
112104
}
113105
catch (ServiceNotFoundException e)
114106
{
115-
_context.LogDebug(Tag, "Service not found for command: {0}", e, e.Command);
107+
_context.LogDebug(Tag, "Service not found for command: {0}", e.Command);
116108
}
117109
catch (Exception e)
118110
{

Lagrange.Core/Internal/Context/PacketContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using Lagrange.Core.Common;
33
using Lagrange.Core.Common.Entity;
44
using Lagrange.Core.Internal.Packets.Struct;
5-
using Lagrange.Core.Internal.Services;
5+
using Lagrange.Core.Services;
66

77
namespace Lagrange.Core.Internal.Context;
88

Lagrange.Core/Internal/Context/ServiceContext.cs

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
using System.Collections.Frozen;
2+
using System.Reflection;
23
using Lagrange.Core.Common;
34
using Lagrange.Core.Common.Entity;
5+
using Lagrange.Core.Events;
46
using Lagrange.Core.Exceptions;
5-
using Lagrange.Core.Internal.Events;
6-
using Lagrange.Core.Internal.Services;
7+
using Lagrange.Core.Services;
78

89
namespace Lagrange.Core.Internal.Context;
910

@@ -23,7 +24,13 @@ public ServiceContext(BotContext context)
2324
{
2425
_context = context;
2526

26-
(_services, _servicesEventType) = ServiceRegistry.Create(context.Config.Protocol, _disabledLog);
27+
var services = new Dictionary<string, IService>(StringComparer.Ordinal);
28+
var servicesEventType = new Dictionary<Type, (ServiceAttribute, IService)>();
29+
AddBuiltInServices(context.Config.Protocol, services, servicesEventType);
30+
AddCustomServices(context.Config.Protocol, context.Config.CustomServices, services, servicesEventType);
31+
32+
_services = services.ToFrozenDictionary(StringComparer.Ordinal);
33+
_servicesEventType = servicesEventType.ToFrozenDictionary();
2734
}
2835

2936
public ValueTask<ProtocolEvent> Resolve(BotSsoPacket ssoPacket)
@@ -36,7 +43,7 @@ public ValueTask<ProtocolEvent> Resolve(BotSsoPacket ssoPacket)
3643

3744
public async ValueTask<(BotSsoPacket, ServiceAttribute)> Resolve(ProtocolEvent @event)
3845
{
39-
if (!_servicesEventType.TryGetValue(@event.GetType(), out var handler)) return default;
46+
if (!_servicesEventType.TryGetValue(@event.GetType(), out var handler)) throw new ServiceNotFoundException(@event.GetType());
4047

4148
var (attr, service) = handler;
4249
if (!handler.Attribute.DisableLog) _context.LogTrace(Tag, "Outgoing SSOFrame: {0}", handler.Attribute.Command);
@@ -49,4 +56,67 @@ public int GetNewSequence()
4956
Interlocked.CompareExchange(ref _sequence, 5000000, 9900000);
5057
return Interlocked.Increment(ref _sequence);
5158
}
59+
60+
private void AddBuiltInServices(
61+
Protocols protocol,
62+
Dictionary<string, IService> services,
63+
Dictionary<Type, (ServiceAttribute, IService)> servicesEventType)
64+
{
65+
var (builtInServices, builtInEventTypes) = ServiceRegistry.Create(protocol, _disabledLog);
66+
foreach ((string command, var service) in builtInServices) services.Add(command, service);
67+
68+
foreach (var (eventType, registration) in builtInEventTypes)
69+
{
70+
servicesEventType.Add(eventType, registration);
71+
}
72+
}
73+
74+
private void AddCustomServices(
75+
Protocols protocol,
76+
IReadOnlyList<IService> customServices,
77+
Dictionary<string, IService> services,
78+
Dictionary<Type, (ServiceAttribute, IService)> servicesEventType)
79+
{
80+
ArgumentNullException.ThrowIfNull(customServices);
81+
82+
foreach (var service in customServices.ToArray())
83+
{
84+
if (service is null) throw new ServiceRegistrationException("Custom protocol services cannot contain null entries.");
85+
86+
var serviceType = service.GetType();
87+
var attribute = serviceType.GetCustomAttribute<ServiceAttribute>(false) ?? throw new ServiceRegistrationException($"Custom protocol service {serviceType} must have {nameof(ServiceAttribute)}.");
88+
if (string.IsNullOrWhiteSpace(attribute.Command)) throw new ServiceRegistrationException($"Custom protocol service {serviceType} must specify a non-empty command.");
89+
90+
var subscriptions = serviceType.GetCustomAttributes<EventSubscribeAttribute>(false).ToArray();
91+
ValidateSubscriptions(serviceType, subscriptions);
92+
93+
var activeSubscriptions = subscriptions.Where(subscription => IsActive(subscription.Protocols, protocol)).ToArray();
94+
if (subscriptions.Length > 0 && activeSubscriptions.Length == 0) continue;
95+
96+
if (!services.TryAdd(attribute.Command, service)) throw new ServiceRegistrationException($"Multiple protocol services are registered for command '{attribute.Command}'.");
97+
98+
if (attribute.DisableLog) _disabledLog.Add(attribute.Command);
99+
100+
foreach (var subscription in activeSubscriptions)
101+
{
102+
var eventType = subscription.EventType;
103+
if (servicesEventType.ContainsKey(eventType)) throw new ServiceRegistrationException($"Multiple protocol services are registered for event type '{eventType}'.");
104+
105+
servicesEventType.Add(eventType, (attribute, service));
106+
}
107+
}
108+
}
109+
110+
private static void ValidateSubscriptions(Type serviceType, EventSubscribeAttribute[] subscriptions)
111+
{
112+
var eventTypes = new HashSet<Type>();
113+
foreach (var subscription in subscriptions)
114+
{
115+
if (!typeof(ProtocolEvent).IsAssignableFrom(subscription.EventType)) throw new ServiceRegistrationException($"Event type '{subscription.EventType}' on {serviceType} must derive from {nameof(ProtocolEvent)}.");
116+
if (subscription.Protocols == Protocols.None) throw new ServiceRegistrationException($"Event subscription '{subscription.EventType}' on {serviceType} must specify at least one protocol.");
117+
if (!eventTypes.Add(subscription.EventType)) throw new ServiceRegistrationException($"Custom protocol service {serviceType} registers event type '{subscription.EventType}' more than once.");
118+
}
119+
}
120+
121+
private static bool IsActive(Protocols supported, Protocols selected) => (~supported & selected) == Protocols.None;
52122
}

0 commit comments

Comments
 (0)