update server to implement felica polling

This commit is contained in:
ppc
2025-01-14 15:46:35 +00:00
parent b33670d849
commit 31c56d2449
5 changed files with 93 additions and 56 deletions
-1
View File
@@ -3,7 +3,6 @@
<PropertyGroup>
<PublishAot>true</PublishAot>
<OutputType>Library</OutputType>
<EventSourceSupport>false</EventSourceSupport>
<TargetFramework>net9.0-windows</TargetFramework>
<OptimizationPreference>Size</OptimizationPreference>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
+20 -7
View File
@@ -13,11 +13,11 @@ internal class CardPresenter
private readonly Lock _cardLock = new();
private StoredCard _currentCard;
public record StoredCard(byte[] Value, string OriginalValue, long ExpiresAt)
public record StoredCard(byte[] AccessCode, byte[] IDm, long ExpiresAt)
{
public bool Expired => Environment.TickCount64 >= ExpiresAt;
public override string ToString() => string.Join(" ", Enumerable.Range(0, 5).Select(x => OriginalValue.Substring(x * 4, 4)));
public override string ToString() => FormatHexCode(AccessCode);
}
/// <summary>
@@ -31,10 +31,16 @@ internal class CardPresenter
}
}
public StoredCard TakeActiveCard()
public StoredCard TakeActiveCard(bool? requireIDm = null)
{
lock (_cardLock)
{
// IDm and requireIDm are mutually exclusive
if (_currentCard == null || (requireIDm.HasValue && (_currentCard.IDm == null) ^ requireIDm.Value))
{
return null;
}
var card = _currentCard;
_currentCard = null;
@@ -45,13 +51,14 @@ internal class CardPresenter
/// <summary>
/// Sets the card
/// </summary>
/// <param name="cardId">The card id to set</param>
/// <param name="accessCode">The access code of the card to use</param>
/// <param name="idm">The FeliCa card IDm, if a physical card was used</param>
/// <param name="validFor">How long the card should be presented to the game for in milliseconds</param>
/// <returns>Whether the card was set successfully</returns>
public StoredCard SetCard(string cardId, long validFor = 5000)
public StoredCard SetCard(string accessCode, string idmHex, long validFor = 5000)
{
// ensure the matrix code is 20-digits long, otherwise pad with zeros
var matrixCode = cardId.Replace(" ", "").PadLeft(20, '0');
var matrixCode = accessCode.Replace(" ", "").PadLeft(20, '0');
var bytes = new byte[10];
for (var i = 0; i < bytes.Length; i++)
@@ -67,7 +74,8 @@ internal class CardPresenter
}
}
var card = new StoredCard(bytes, matrixCode, Environment.TickCount64 + validFor);
var idmBytes = !string.IsNullOrWhiteSpace(idmHex) && idmHex.Length <= 16 && idmHex.All(char.IsAsciiHexDigit) ? Convert.FromHexString(idmHex.PadLeft(16, '0')) : null;
var card = new StoredCard(bytes, idmBytes, Environment.TickCount64 + validFor);
lock (_cardLock)
{
@@ -76,4 +84,9 @@ internal class CardPresenter
return card;
}
/// <summary>
/// Formats a byte array as a hex string, inserting a space every 2 characters
/// </summary>
public static string FormatHexCode(byte[] code) => string.Join(" ", code.Chunk(2).Select(Convert.ToHexString));
}
+17 -12
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -10,30 +9,36 @@ internal static class Config
internal const string IOSection = "aimeio";
private const string ConfigFileName = @".\segatools.ini";
/// <summary>
/// Valid physical card identification methods.
/// IDm - uses the idm of the card to generate a 20-digit identifier (also known as the 0008 number)
/// Access - prefer the real access code of the card. if not available (i.e. Suica card), fallback to the IDm.
/// </summary>
private static readonly string[] ValidPhysicalCardIdentifiers = ["idm", "access"];
static Config()
{
GameId = ReadKey(IOSection, "gameId", 4);
ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName);
var physicalCardIdentifierPreference = ReadKey(IOSection, "physicalCardIdentifier", 10);
PhysicalCardIdentifierPreference = ValidPhysicalCardIdentifiers.Contains(physicalCardIdentifierPreference) ? physicalCardIdentifierPreference : ValidPhysicalCardIdentifiers[^1];
PhysicalCardUseIDm = PInvoke.GetPrivateProfileInt(IOSection, "usePhysicalCardIdm", 0, ConfigFileName) > 0;
EnableAimeTxt = PInvoke.GetPrivateProfileInt(IOSection, "enableKeyboardMode", 1, ConfigFileName) > 0;
AimeTxtPath = ReadKey("aime", "aimePath", PInvoke.MAX_PATH, @"DEVICE\aime.txt");
}
public static string GameId { get; }
/// <summary>
/// The server name to show on devices. Defaults to the computer name.
/// </summary>
public static string ServerName { get; }
public static string PhysicalCardIdentifierPreference { get; }
/// <summary>
/// Whether physically scanned cards should have their IDm forwarded to the game instead of the access code.
/// </summary>
public static bool PhysicalCardUseIDm { get; }
/// <summary>
/// Whether holding the enter key should trigger a card scan using the code from the aime.txt file
/// </summary>
public static bool EnableAimeTxt { get; }
/// <summary>
/// Path to the aime.txt file. Defaults to .\DEVICE\aime.txt
/// </summary>
public static string AimeTxtPath { get; }
internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null)
+24 -13
View File
@@ -36,7 +36,7 @@ public static class DllMain
CancellationTokenRegistration? cancellationRegistration = null;
cancellationRegistration = App.Lifetime.ApplicationStarted.Register(() =>
{
App.Logger.LogInformation("AMNet Server ({gameId}) started successfully.", Config.GameId);
App.Logger.LogInformation("AMNet Server ({gameId}) started successfully.", Config.GameId ?? "SXXX");
App.Logger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress);
// ReSharper disable once AccessToModifiedClosure
@@ -71,7 +71,7 @@ public static class DllMain
using var aimeTxtReader = new StreamReader(File.OpenRead(Config.AimeTxtPath), Encoding.UTF8);
var fileName = Path.GetFileName(Config.AimeTxtPath);
var cardResult = App.Services.GetRequiredService<CardPresenter>().SetCard(aimeTxtReader.ReadLine());
var cardResult = App.Services.GetRequiredService<CardPresenter>().SetCard(aimeTxtReader.ReadLine(), null);
if (cardResult == null)
{
@@ -98,25 +98,36 @@ public static class DllMain
return 1;
}
var card = App.Services.GetRequiredService<CardPresenter>().TakeActiveCard();
// if physical cards are being directed to
var card = App.Services.GetRequiredService<CardPresenter>().TakeActiveCard(Config.PhysicalCardUseIDm ? false : null);
if (card?.Expired != false)
{
return 1;
}
try
{
Marshal.Copy(card.Value, 0, luid, (int)luidSize);
return 0;
}
finally
{
App.Logger.LogInformation("Card read in: {cardId}", card);
}
Marshal.Copy(card.AccessCode, 0, luid, (int)luidSize);
App.Logger.LogInformation("Access Code read: {cardId}", CardPresenter.FormatHexCode(card.AccessCode));
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_felica_id")]
public static unsafe int GetFelicaId(byte unitNo, ulong* idm) => 1; // felica not supported
public static int GetFelicaId(byte unitNo, IntPtr idm)
{
if (!Config.PhysicalCardUseIDm || unitNo != 0)
{
return 1;
}
var card = App.Services.GetRequiredService<CardPresenter>().TakeActiveCard(true);
if (card?.Expired != false)
{
return 1;
}
Marshal.Copy(card.IDm, 0, idm, sizeof(ulong));
App.Logger.LogInformation("FeliCa IDm read: {idm}", card.IDm);
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")]
public static void SetLedColour(byte unitNo, byte r, byte g, byte b)
+32 -23
View File
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
@@ -21,11 +22,14 @@ internal static partial class WebServer
[property: JsonPropertyName("gameId")] string GameId,
[property: JsonPropertyName("serverName")] string ServerName,
[property: JsonPropertyName("sessionUptime")] long SessionUptime,
[property: JsonPropertyName("timeSinceLastPoll")] long? TimeSinceLastPoll,
[property: JsonPropertyName("physicalCardIdentifierPreference")] string PhysicalCardIdentifierPreference);
[property: JsonPropertyName("timeSinceLastPoll")] long? TimeSinceLastPoll);
public record CardReadRequest(
[property: JsonPropertyName("cardId")] string MatrixCode);
[property: JsonPropertyName("cardId")] string AccessCode,
[property: JsonPropertyName("physicalCardIDm")] string PhysicalCardIDm);
public record ServerErrorMessage(
[property: JsonPropertyName("message")] string Message);
public static WebApplication BuildServer(params string[] listenAddresses)
{
@@ -83,8 +87,7 @@ internal static partial class WebServer
Config.GameId,
Config.ServerName,
Environment.TickCount64 - startedAt,
lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt,
Config.PhysicalCardIdentifierPreference);
lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt);
return Results.Ok(state);
}
@@ -99,23 +102,25 @@ internal static partial class WebServer
}
catch (JsonException e)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsync(e.Message);
await WriteErrorResponse(ctx, StatusCodes.Status400BadRequest, e.Message);
return;
}
if (string.IsNullOrEmpty(request?.MatrixCode))
if (string.IsNullOrEmpty(request?.AccessCode))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsync("Card id not provided.");
await WriteErrorResponse(ctx, StatusCodes.Status400BadRequest, "Access Code not provided.");
return;
}
if (request.MatrixCode.Length is 0 or < 20)
if (request.AccessCode.Length is 0 or < 20 || !request.AccessCode.Any(char.IsAsciiDigit))
{
ctx.Response.StatusCode = 422;
await ctx.Response.WriteAsync("Invalid card id length.");
await WriteErrorResponse(ctx, StatusCodes.Status422UnprocessableEntity, "Invalid Access Code.");
return;
}
if (request.AccessCode.All(x => x == '0'))
{
await WriteErrorResponse(ctx, StatusCodes.Status403Forbidden, "All-zero access codes are forbidden.");
return;
}
@@ -124,26 +129,30 @@ internal static partial class WebServer
if (currentCard?.Expired == false)
{
var timeLeft = Math.Ceiling(TimeSpan.FromMilliseconds(currentCard.ExpiresAt - Environment.TickCount64).TotalSeconds);
ctx.Response.StatusCode = 429;
ctx.Response.Headers.RetryAfter = timeLeft.ToString(CultureInfo.InvariantCulture);
ctx.Response.StatusCode = StatusCodes.Status429TooManyRequests;
ctx.Response.Headers.RetryAfter = Math.Ceiling(TimeSpan.FromMilliseconds(currentCard.ExpiresAt - Environment.TickCount64).TotalSeconds).ToString(CultureInfo.InvariantCulture);
return;
}
if (cardPresenter.SetCard(request.MatrixCode) != null)
if (cardPresenter.SetCard(request.AccessCode, request.PhysicalCardIDm) != null)
{
ctx.Response.StatusCode = 202;
ctx.Response.StatusCode = StatusCodes.Status202Accepted;
}
else
{
ctx.Response.StatusCode = 422;
await ctx.Response.WriteAsync("Invalid card id format.");
await WriteErrorResponse(ctx, StatusCodes.Status422UnprocessableEntity, "Invalid card id format.");
}
}
private static Task WriteErrorResponse(HttpContext ctx, int code, string message)
{
ctx.Response.StatusCode = code;
return ctx.Response.WriteAsJsonAsync(new ServerErrorMessage(message), SerializerContext.Default.ServerErrorMessage);
}
[JsonSerializable(typeof(SystemState))]
[JsonSerializable(typeof(CardReadRequest))]
[JsonSerializable(typeof(ServerErrorMessage))]
[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.Never)]
private partial class SerializerContext : JsonSerializerContext;
}