Added Handler for Paseli consumption. nd add missing fields from SDVX into Facility. And fix plugin dispose error

This commit is contained in:
Kayori
2026-08-11 09:40:52 +02:00
parent e65625ca06
commit 2dfa182852
18 changed files with 598 additions and 61 deletions
+4 -1
View File
@@ -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/
@@ -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);
}
}
@@ -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();
}
}
@@ -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<EaCoinCheckInRequest, EaCoinCheckInResponse>
{
public override void Configure()
{
Module("eacoin");
Method("checkin");
}
public override async Task<EaCoinCheckInResponse> 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
};
}
}
@@ -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<EaCoinCheckoutRequest, EaCoinCheckoutResponse>
{
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
};
}
}
@@ -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<EaCoinConsumeRequest, EaCoinConsumeResponse>
{
public override void Configure()
{
Module("eacoin");
Method("consume");
}
public override async Task<EaCoinConsumeResponse> 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
};
}
}
@@ -23,41 +23,39 @@ public class GetFacilityHandler: HandlerWithoutRequest<GetFacilityResponse>
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<GetFacilityResponse>
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;
}
}
}
@@ -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; }
}
@@ -0,0 +1,10 @@
using System.Xml.Serialization;
namespace Server.Models.Request;
[XmlRoot("eacoin")]
public class EaCoinCheckoutRequest
{
[XmlElement("sessid")]
public string sessionId { get; set; }
}
@@ -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; }
}
@@ -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; }
}
@@ -0,0 +1,10 @@
using System.Xml.Serialization;
namespace Server.Models.Response;
[XmlRoot("eacoin")]
public class EaCoinCheckoutResponse
{
[XmlAttribute("status")]
public int Status { get; set; }
}
@@ -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; }
}
@@ -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; }
}
}
public record FacilityCalendar
{
[XmlElement("year")]
public required XrpcShort Year { get; set; }
// Real wire shape: <holiday __type="s16" __count="35">0 11 41 ...</holiday>
// - 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; } = "";
}
+1
View File
@@ -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<IEaCoinSessionService, EaCoinSessionService>();
builder.Services.AddSingleton<IPluginService>(pluginService);
builder.Services.AddHostedService<PluginWatcher>();
@@ -0,0 +1,38 @@
namespace Server.Services;
public class EaCoinSessionService : IEaCoinSessionService
{
private readonly List<EaCoinSession> 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;
}
@@ -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);
}
+20 -17
View File
@@ -163,30 +163,33 @@ public class PluginService(ILogger logger, PluginRegistry pluginRegistry) : IPlu
var method = plugin.DoesProfileExist;
var parameters = method.Method.GetParameters();
Task<bool> InvokeAsync(IServiceProvider composite)
{
var args = parameters.Select(p =>
{
if (p.Name == "cardId") return (object)cardId;
var isFromServices = p.GetCustomAttribute<FromServicesAttribute>() != 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<bool>)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<FromServicesAttribute>() != 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<bool>)method.DynamicInvoke(args)!;
return await InvokeAsync(hostScope.ServiceProvider);
}
// ── Internal ─────────────────────────────────────────────────────────────