diff --git a/.gitignore b/.gitignore index 66a555f..a3421c8 100644 --- a/.gitignore +++ b/.gitignore @@ -425,4 +425,7 @@ FodyWeavers.xsd # Additional files built by Visual Studio # End of https://www.toptal.com/developers/gitignore/api/dotnetcore,visualstudiocode,visualstudio -/Core/Server/Medusa.db +*.db +.idea/ + +Core/Server/database/ \ No newline at end of file diff --git a/Abstractions/SerializationTypes/XrpcUByte.cs b/Abstractions/SerializationTypes/XrpcUByte.cs new file mode 100644 index 0000000..062b50f --- /dev/null +++ b/Abstractions/SerializationTypes/XrpcUByte.cs @@ -0,0 +1,63 @@ +using System.Xml.Serialization; + +namespace Abstractions.SerializationTypes; + +public struct XrpcUByte +{ + [XmlAttribute("__type")] + public string Type { get; set; } + + [XmlText] + public byte Value { get; set; } + + public XrpcUByte(byte value) + { + Type = "u8"; + Value = value; + } + + public static implicit operator byte(XrpcUByte value) + { + return value.Value; + } + + public static implicit operator XrpcUByte(byte value) + { + return new XrpcUByte(value); + } + + public bool Equals(XrpcUByte other) + { + return Value == other.Value; + } + + public override bool Equals(object obj) + { + if (obj is XrpcUByte other) + { + return Equals(other); + } + + return false; + } + + public static bool operator ==(XrpcUByte left, XrpcUByte right) + { + return left.Equals(right); + } + + public static bool operator !=(XrpcUByte left, XrpcUByte right) + { + return !(left == right); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public static XrpcUByte FromBool(bool v) + { + return new XrpcUByte(v ? (byte)1 : (byte)0); + } +} \ No newline at end of file diff --git a/Abstractions/SerializationTypes/XrpcUShort.cs b/Abstractions/SerializationTypes/XrpcUShort.cs new file mode 100644 index 0000000..c0293af --- /dev/null +++ b/Abstractions/SerializationTypes/XrpcUShort.cs @@ -0,0 +1,57 @@ +using System.Xml.Serialization; + +namespace Abstractions.SerializationTypes; + +public struct XrpcUShort +{ + [XmlAttribute("__type")] + public string Type { get; set; } + + [XmlText] + public ushort Value { get; set; } + + public XrpcUShort(ushort value) + { + Type = "u16"; + Value = value; + } + + public static implicit operator ushort(XrpcUShort value) + { + return value.Value; + } + + public static implicit operator XrpcUShort(ushort value) + { + return new XrpcUShort(value); + } + + public bool Equals(XrpcUShort other) + { + return Value == other.Value; + } + + public override bool Equals(object obj) + { + if (obj is XrpcUShort other) + { + return Equals(other); + } + return false; + } + + public static bool operator ==(XrpcUShort left, XrpcUShort right) + { + return left.Equals(right); + } + + public static bool operator !=(XrpcUShort left, XrpcUShort right) + { + return !(left == right); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } +} diff --git a/Core/Server/Handlers/Common/Coin/EaCoinCheckinHandler.cs b/Core/Server/Handlers/Common/Coin/EaCoinCheckinHandler.cs new file mode 100644 index 0000000..d45f03a --- /dev/null +++ b/Core/Server/Handlers/Common/Coin/EaCoinCheckinHandler.cs @@ -0,0 +1,60 @@ +using Abstractions.Handlers; +using Abstractions.SerializationTypes; +using Abstractions.Services; +using Server.Models.Request; +using Server.Models.Response; +using Server.Services; + +namespace Server.Handlers.Common.Coin; + +public class EaCoinCheckinHandler(ICardService cardService, IEaCoinSessionService sessionService) : Handler +{ + public override void Configure() + { + Module("eacoin"); + Method("checkin"); + } + + public override async Task HandleAsync(EaCoinCheckInRequest req, string model) + { + var card = await cardService.FindByCardId(req.CardId); + + if (card is null) + { + return new EaCoinCheckInResponse + { + Expire = 1200, + Status = 1, + AcStatus = new XrpcUByte(0) + }; + } + + var pinValid = await cardService.ValidatePinAsync(card.KonamiId, req.PassWd); + + if (!pinValid) + { + return new EaCoinCheckInResponse + { + Expire = 1200, + Status = 1, + AcStatus = new XrpcUByte(0) + }; + } + + var session = sessionService.AddSession(card.User.Id, card.KonamiId); + + return new EaCoinCheckInResponse + { + Expire = 1200, + Status = 0, + Sequence = 0, + AcStatus = 0, + AcId = card.User.Id + "-" + card.KonamiId, + AcName = card.User.UserName ?? card.KonamiId, + Balance = card.User.PaseliAmount, + SessionId = session.SessionId, + InShopCharge = 0, + Point = 0 + }; + } +} diff --git a/Core/Server/Handlers/Common/Coin/EaCoinCheckoutHandler.cs b/Core/Server/Handlers/Common/Coin/EaCoinCheckoutHandler.cs new file mode 100644 index 0000000..f480ec3 --- /dev/null +++ b/Core/Server/Handlers/Common/Coin/EaCoinCheckoutHandler.cs @@ -0,0 +1,25 @@ +using Abstractions.Handlers; +using Server.Models.Request; +using Server.Models.Response; +using Server.Services; + +namespace Server.Handlers.Common.Coin; + +public class EaCoinCheckoutHandler(IEaCoinSessionService sessionService) : Handler +{ + public override void Configure() + { + Module("eacoin"); + Method("checkout"); + } + + public override EaCoinCheckoutResponse Handle(EaCoinCheckoutRequest req, string model) + { + sessionService.RemoveSession(req.sessionId); + + return new EaCoinCheckoutResponse() + { + Status = 0 + }; + } +} \ No newline at end of file diff --git a/Core/Server/Handlers/Common/Coin/EaCoinConsumeHandler.cs b/Core/Server/Handlers/Common/Coin/EaCoinConsumeHandler.cs new file mode 100644 index 0000000..df29489 --- /dev/null +++ b/Core/Server/Handlers/Common/Coin/EaCoinConsumeHandler.cs @@ -0,0 +1,83 @@ +using Abstractions.Handlers; +using Microsoft.EntityFrameworkCore; +using Server.Models.Request; +using Server.Models.Response; +using Server.Services; + +namespace Server.Handlers.Common.Coin; + +public class EaCoinConsumeHandler(IEaCoinSessionService sessionService, AppDbContext dbContext) : Handler +{ + public override void Configure() + { + Module("eacoin"); + Method("consume"); + } + + public override async Task HandleAsync(EaCoinConsumeRequest req, string model) + { + var session = sessionService.GetSession(req.sessionId); + + if (session == null) + { + return new EaCoinConsumeResponse() + { + Status = -4, // EA3_EACOIN_ERROR.NOTCHECKIN + AcStatus = 0, + AutoCharge = 0, + Balance = 0 + }; + } + + if (session.IsCharging) + { + return new EaCoinConsumeResponse() + { + Status = -2, // EA3_EACOIN_ERROR.SLOTINUSE + AcStatus = 0, + AutoCharge = 0, + Balance = 0 + }; + } + + session.IsCharging = true; + + var user = await dbContext.Users.FindAsync(session.UserId); + + if (user == null) + { + session.IsCharging = false; + return new EaCoinConsumeResponse() + { + Status = -7, // EA3_EACOIN_ERROR.NOACCOUNT + AcStatus = 0, + AutoCharge = 0, + Balance = 0 + }; + } + + if (user.PaseliAmount < req.Payment) + { + session.IsCharging = false; + return new EaCoinConsumeResponse() + { + Status = -8, // EA3_EACOIN_ERROR.SHORTBALANCE + AcStatus = 0, + AutoCharge = 0, + Balance = user.PaseliAmount + }; + } + + user.PaseliAmount -= req.Payment; + await dbContext.SaveChangesAsync(); + + session.IsCharging = false; + return new EaCoinConsumeResponse() + { + Status = 0, + AcStatus = 0, + AutoCharge = 0, + Balance = user.PaseliAmount + }; + } +} \ No newline at end of file diff --git a/Core/Server/Handlers/Common/GetFacilityHandler.cs b/Core/Server/Handlers/Common/GetFacilityHandler.cs index 8939f48..51176dd 100644 --- a/Core/Server/Handlers/Common/GetFacilityHandler.cs +++ b/Core/Server/Handlers/Common/GetFacilityHandler.cs @@ -23,41 +23,39 @@ public class GetFacilityHandler: HandlerWithoutRequest Country = new XrpcString("JP"), Region = new XrpcString("13"), Name = new XrpcString("Medusa"), - Type = new XrpcULong(0), + Type = new XrpcUByte(0), CompanyCode = new XrpcString("00"), CustomerCode = new XrpcString("0000"), CountryName = new XrpcString("Japan"), CountryJpName = new XrpcString("日本"), RegionName = new XrpcString("Tokyo"), RegionJpName = new XrpcString("東京都"), - Accuracy = new XrpcULong(0), + Accuracy = new XrpcUByte(0), Latitude = new XrpcInt(0), Longitude = new XrpcInt(0) }, Line = new FacilityLine() { Id = new XrpcString("1"), - Class = new XrpcULong(0) + Class = new XrpcUByte(0), + UpClass = new XrpcUByte(0), + Rtt = new XrpcUShort(0) }, PortForward = new FacilityPortForward() { GlobalIp = new XrpcIp4("127.0.0.1"), - GlobalPort = new XrpcULong(5246), - PrivatePort = new XrpcULong(5246) + GlobalPort = new XrpcUShort(5246), + PrivatePort = new XrpcUShort(5246) }, Public = new FacilityPublic() { - Flag = new XrpcULong(1), - Name = new XrpcString("Medusa") + Flag = new XrpcUByte(1), + Name = new XrpcString("Medusa"), + Latitude = new XrpcString("0"), + Longitude = new XrpcString("0") }, Share = new FacilityShare() { - EaCoin = new FacilityEaCoin() - { - NotchAmount = new XrpcLong((long)3000), - NotchCount = new XrpcLong((long)3), - SupplyLimit = new XrpcLong(1000000) - }, Url = new FacilityUrl() { EaPass = new XrpcString("http://eagate.573.jp"), @@ -65,10 +63,32 @@ public class GetFacilityHandler: HandlerWithoutRequest KonamiNetDx = new XrpcString("http://eagate.573.jp"), KonamiId = new XrpcString("http://eagate.573.jp"), EaGate = new XrpcString("http://eagate.573.jp") + }, + EaPass = new FacilityEaPass() + { + Valid = new XrpcUShort(365) + }, + EaCoin = new FacilityEaCoin() + { + NotchAmount = new XrpcInt(0), + NotchCount = new XrpcInt(0), + SupplyLimit = new XrpcInt(1000000) + } + }, + Calendar = new FacilityCalendar() + { + Year = new XrpcShort((short)DateTime.UtcNow.Year), + // TODO: no real holiday calendar wired up yet - empty list is a + // safe default (client just sees "no holidays"), not a guess at + // actual dates. + Holiday = new FacilityHolidayList() + { + Count = 0, + Value = "" } } }; - + return facility; } -} \ No newline at end of file +} diff --git a/Core/Server/Models/Request/EaCoinCheckInRequest.cs b/Core/Server/Models/Request/EaCoinCheckInRequest.cs new file mode 100644 index 0000000..54a7a26 --- /dev/null +++ b/Core/Server/Models/Request/EaCoinCheckInRequest.cs @@ -0,0 +1,18 @@ +using System.Xml.Serialization; + +namespace Server.Models.Request; + +[XmlRoot("eacoin")] +public class EaCoinCheckInRequest +{ + [XmlElement("slotid")] + public required string SlotId { get; set; } + [XmlElement("cardtype")] + public required string CardType { get; set; } + [XmlElement("cardid")] + public required string CardId { get; set; } + [XmlElement("passwd")] + public required string PassWd { get; set; } + [XmlElement("ectype")] + public required string EcType { get; set; } +} \ No newline at end of file diff --git a/Core/Server/Models/Request/EaCoinCheckoutRequest.cs b/Core/Server/Models/Request/EaCoinCheckoutRequest.cs new file mode 100644 index 0000000..055092a --- /dev/null +++ b/Core/Server/Models/Request/EaCoinCheckoutRequest.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace Server.Models.Request; + +[XmlRoot("eacoin")] +public class EaCoinCheckoutRequest +{ + [XmlElement("sessid")] + public string sessionId { get; set; } +} \ No newline at end of file diff --git a/Core/Server/Models/Request/EaCoinConsumeRequest.cs b/Core/Server/Models/Request/EaCoinConsumeRequest.cs new file mode 100644 index 0000000..4d40029 --- /dev/null +++ b/Core/Server/Models/Request/EaCoinConsumeRequest.cs @@ -0,0 +1,31 @@ +using System.Xml.Serialization; + +namespace Server.Models.Request; + +[XmlRoot("eacoin")] +public class EaCoinConsumeRequest +{ + [XmlAttribute("esid")] + public string EsId { get; set; } + + [XmlAttribute("esdate")] + public string EsDate { get; set; } + + [XmlElement("sessid")] + public string sessionId { get; set; } + + [XmlElement("sequence")] + public short Sequence { get; set; } + + [XmlElement("payment")] + public int Payment { get; set; } + + [XmlElement("service")] + public short service { get; set; } + + [XmlElement("itemtype")] + public string ItemType { get; set; } + + [XmlElement("detail")] + public string Detail { get; set; } +} \ No newline at end of file diff --git a/Core/Server/Models/Response/EaCoinCheckInResponse.cs b/Core/Server/Models/Response/EaCoinCheckInResponse.cs new file mode 100644 index 0000000..00ac1ad --- /dev/null +++ b/Core/Server/Models/Response/EaCoinCheckInResponse.cs @@ -0,0 +1,39 @@ +using System.Xml.Serialization; +using Abstractions.SerializationTypes; + +namespace Server.Models.Response; + +[XmlRoot("eacoin")] +[Serializable] +public class EaCoinCheckInResponse +{ + [XmlAttribute("status")] + public required int Status { get; set; } + + [XmlAttribute("expire")] + public required int Expire { get; set; } + + [XmlElement("sequence")] + public XrpcShort Sequence { get; set; } + + [XmlElement("acstatus")] + public XrpcUByte AcStatus { get; set; } + + [XmlElement("acid")] + public XrpcString AcId { get; set; } + + [XmlElement("acname")] + public XrpcString AcName { get; set; } + + [XmlElement("balance")] + public XrpcInt Balance { get; set; } + + [XmlElement("sessid")] + public XrpcString SessionId { get; set; } + + [XmlElement("inshopcharge")] + public XrpcUByte InShopCharge { get; set; } + + [XmlElement("point")] + public XrpcInt Point { get; set; } +} \ No newline at end of file diff --git a/Core/Server/Models/Response/EaCoinCheckoutResponse.cs b/Core/Server/Models/Response/EaCoinCheckoutResponse.cs new file mode 100644 index 0000000..751d4d0 --- /dev/null +++ b/Core/Server/Models/Response/EaCoinCheckoutResponse.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace Server.Models.Response; + +[XmlRoot("eacoin")] +public class EaCoinCheckoutResponse +{ + [XmlAttribute("status")] + public int Status { get; set; } +} \ No newline at end of file diff --git a/Core/Server/Models/Response/EaCoinConsumeResponse.cs b/Core/Server/Models/Response/EaCoinConsumeResponse.cs new file mode 100644 index 0000000..c557935 --- /dev/null +++ b/Core/Server/Models/Response/EaCoinConsumeResponse.cs @@ -0,0 +1,21 @@ +using System.Xml.Serialization; +using Abstractions.SerializationTypes; + +namespace Server.Models.Response; + +[XmlRoot("eacoin")] +[Serializable] +public class EaCoinConsumeResponse +{ + [XmlAttribute("status")] + public int Status { get; set; } + + [XmlElement("acstatus")] + public XrpcUByte AcStatus { get; set; } + + [XmlElement("autocharge")] + public XrpcUByte AutoCharge { get; set; } + + [XmlElement("balance")] + public XrpcInt Balance { get; set; } +} \ No newline at end of file diff --git a/Core/Server/Models/Response/GetFacilityResponse.cs b/Core/Server/Models/Response/GetFacilityResponse.cs index 14d3e06..4148d08 100644 --- a/Core/Server/Models/Response/GetFacilityResponse.cs +++ b/Core/Server/Models/Response/GetFacilityResponse.cs @@ -12,18 +12,21 @@ public class GetFacilityResponse [XmlElement("location")] public required FacilityLocation Location { get; set; } - + [XmlElement("line")] public required FacilityLine Line { get; set; } - - [XmlElement("portforward")] + + [XmlElement("portfw")] public required FacilityPortForward PortForward { get; set; } - + [XmlElement("public")] public required FacilityPublic Public { get; set; } - + [XmlElement("share")] public required FacilityShare Share { get; set; } + + [XmlElement("calendar")] + public required FacilityCalendar Calendar { get; set; } } public record FacilityLocation @@ -41,7 +44,7 @@ public record FacilityLocation public required XrpcString Name { get; set; } [XmlElement("type")] - public required XrpcULong Type { get; set; } + public required XrpcUByte Type { get; set; } [XmlElement("companycode")] public required XrpcString CompanyCode { get; set; } @@ -62,7 +65,7 @@ public record FacilityLocation public required XrpcString RegionJpName { get; set; } [XmlElement("accuracy")] - public required XrpcULong Accuracy { get; set; } + public required XrpcUByte Accuracy { get; set; } [XmlElement("latitude")] public required XrpcInt Latitude { get; set; } @@ -75,68 +78,111 @@ public record FacilityLine { [XmlElement("id")] public required XrpcString Id { get; set; } - + [XmlElement("class")] - public required XrpcULong Class { get; set; } + public required XrpcUByte Class { get; set; } + + [XmlElement("upclass")] + public required XrpcUByte UpClass { get; set; } + + [XmlElement("rtt")] + public required XrpcUShort Rtt { get; set; } } public record FacilityPortForward { [XmlElement("globalip")] public required XrpcIp4 GlobalIp { get; set; } - + [XmlElement("globalport")] - public required XrpcULong GlobalPort { get; set; } - + public required XrpcUShort GlobalPort { get; set; } + [XmlElement("privateport")] - public required XrpcULong PrivatePort { get; set; } + public required XrpcUShort PrivatePort { get; set; } } public record FacilityPublic { [XmlElement("flag")] - public required XrpcULong Flag { get; set; } + public required XrpcUByte Flag { get; set; } [XmlElement("name")] public required XrpcString Name { get; set; } + + [XmlElement("latitude")] + public required XrpcString Latitude { get; set; } + + [XmlElement("longitude")] + public required XrpcString Longitude { get; set; } } public record FacilityShare { - [XmlElement("eacon")] - public required FacilityEaCoin EaCoin { get; set; } - [XmlElement("url")] public required FacilityUrl Url { get; set; } + + [XmlElement("eapass")] + public required FacilityEaPass EaPass { get; set; } + + [XmlElement("eacoin")] + public required FacilityEaCoin EaCoin { get; set; } } public record FacilityEaCoin { [XmlElement("notchamount")] - public required XrpcLong NotchAmount { get; set; } - + public required XrpcInt NotchAmount { get; set; } + [XmlElement("notchcount")] - public required XrpcLong NotchCount { get; set; } - + public required XrpcInt NotchCount { get; set; } + [XmlElement("supplylimit")] - public required XrpcLong SupplyLimit { get; set; } - + public required XrpcInt SupplyLimit { get; set; } +} + +public record FacilityEaPass +{ + [XmlElement("valid")] + public required XrpcUShort Valid { get; set; } } public record FacilityUrl { [XmlElement("eapass")] public required XrpcString EaPass { get; set; } - + [XmlElement("arcadefan")] public required XrpcString ArcadeFan { get; set;} - + [XmlElement("konaminetdx")] public required XrpcString KonamiNetDx { get; set; } - + [XmlElement("konamiid")] public required XrpcString KonamiId { get; set; } - + [XmlElement("eagate")] public required XrpcString EaGate { get; set; } -} \ No newline at end of file +} + +public record FacilityCalendar +{ + [XmlElement("year")] + public required XrpcShort Year { get; set; } + + // Real wire shape: 0 11 41 ... + // - a space-separated list of day-of-year offsets, not a repeated element. + [XmlElement("holiday")] + public required FacilityHolidayList Holiday { get; set; } +} + +public class FacilityHolidayList +{ + [XmlAttribute("__type")] + public string Type { get; set; } = "s16"; + + [XmlAttribute("__count")] + public int Count { get; set; } + + [XmlText] + public string Value { get; set; } = ""; +} diff --git a/Core/Server/Program.cs b/Core/Server/Program.cs index b7cf08f..fdf9ef1 100644 --- a/Core/Server/Program.cs +++ b/Core/Server/Program.cs @@ -90,6 +90,7 @@ builder.Services.AddOpenApi("v1"); var pluginRegistry = new PluginRegistry(); var pluginService = new PluginService(logger, pluginRegistry); builder.Services.AddSingleton(pluginRegistry); +builder.Services.AddSingleton(); builder.Services.AddSingleton(pluginService); builder.Services.AddHostedService(); diff --git a/Core/Server/Services/EaCoinSessionService.cs b/Core/Server/Services/EaCoinSessionService.cs new file mode 100644 index 0000000..964fd77 --- /dev/null +++ b/Core/Server/Services/EaCoinSessionService.cs @@ -0,0 +1,38 @@ +namespace Server.Services; + +public class EaCoinSessionService : IEaCoinSessionService +{ + private readonly List sessions = []; + public void AddSession(int userId, string sessionId, string refId) + { + sessions.Add(new EaCoinSession(sessionId, userId, refId)); + } + + public EaCoinSession AddSession(int userId, string cardid) + { + var sessionId = GenerateSessionId(); + var session = new EaCoinSession(sessionId, userId, cardid); + sessions.Add(session); + return session; + } + + public EaCoinSession? GetSession(string sessionId) + { + return sessions.Find(s => s.SessionId == sessionId); + } + + public void RemoveSession(string sessionId) + { + sessions.Remove(sessions.Find(s => s.SessionId == sessionId)); + } + + private string GenerateSessionId() + { + return Guid.NewGuid().ToString().Replace("-", "").ToUpper(); + } +} + +public record EaCoinSession(string SessionId, int UserId, string RefId) +{ + public bool IsCharging { get; set; } = false; +} \ No newline at end of file diff --git a/Core/Server/Services/IEaCoinSessionService.cs b/Core/Server/Services/IEaCoinSessionService.cs new file mode 100644 index 0000000..9531902 --- /dev/null +++ b/Core/Server/Services/IEaCoinSessionService.cs @@ -0,0 +1,9 @@ +namespace Server.Services; + +public interface IEaCoinSessionService +{ + public void AddSession(int userId, string sessionId, string refId); + public EaCoinSession AddSession(int userId, string cardId); + public EaCoinSession? GetSession(string sessionId); + public void RemoveSession(string sessionId); +} \ No newline at end of file diff --git a/Core/Server/Services/PluginService.cs b/Core/Server/Services/PluginService.cs index 8d7247b..5f60807 100644 --- a/Core/Server/Services/PluginService.cs +++ b/Core/Server/Services/PluginService.cs @@ -163,30 +163,33 @@ public class PluginService(ILogger logger, PluginRegistry pluginRegistry) : IPlu var method = plugin.DoesProfileExist; var parameters = method.Method.GetParameters(); + Task InvokeAsync(IServiceProvider composite) + { + var args = parameters.Select(p => + { + if (p.Name == "cardId") return (object)cardId; + var isFromServices = p.GetCustomAttribute() != null; + return isFromServices + ? composite.GetRequiredService(p.ParameterType) + : throw new InvalidOperationException( + $"Don't know how to resolve parameter '{p.Name}' on plugin delegate DoesProfileExist"); + }).ToArray(); + + return (Task)method.DynamicInvoke(args)!; + } + await using var hostScope = _serviceProvider.CreateAsyncScope(); - IServiceProvider composite; if (slot is not null) { + // pluginScope must stay alive until the delegate has actually run, since + // composite (and anything resolved from it) is only valid while it's open. await using var pluginScope = slot.PluginServices.CreateAsyncScope(); - composite = new PluginServiceProvider(pluginScope.ServiceProvider, hostScope.ServiceProvider); - } - else - { - composite = hostScope.ServiceProvider; + var composite = new PluginServiceProvider(pluginScope.ServiceProvider, hostScope.ServiceProvider); + return await InvokeAsync(composite); } - var args = parameters.Select(p => - { - if (p.Name == "cardId") return (object)cardId; - var isFromServices = p.GetCustomAttribute() != null; - return isFromServices - ? composite.GetRequiredService(p.ParameterType) - : throw new InvalidOperationException( - $"Don't know how to resolve parameter '{p.Name}' on plugin delegate DoesProfileExist"); - }).ToArray(); - - return await (Task)method.DynamicInvoke(args)!; + return await InvokeAsync(hostScope.ServiceProvider); } // ── Internal ─────────────────────────────────────────────────────────────