using System; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Text.Json; using System.Threading; using Windows.Win32; using Windows.Win32.Foundation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace AMNet.Server; public static class DllMain { private const int ApiVersion = 1; private const string WebAddress = "http://card.ppc.moe"; private static ILogger _serverLogger; private static string[] _serverAddresses; private static string _serverName; private static string _gameId; private static (byte[] IdBytes, string OriginalId, long Expires)? _currentCard; private static readonly object _cardLock = new(); static DllMain() { PInvoke.AllocConsole(); } [UnmanagedCallersOnly(EntryPoint = "aime_io_get_api_version")] public static ushort GetApiVersion() => 0x0100; [UnmanagedCallersOnly(EntryPoint = "aime_io_init")] public static int Init() { LoadConfiguration(); var builder = WebServer.BuildServer(); builder.WebHost.UseUrls(_serverAddresses); var app = builder.Build(); _serverLogger = app.Logger; app.UseCors(); app.MapGet("/amnet/info", () => Results.Ok(new SystemState(ApiVersion, _gameId, _serverName))); app.MapPost("/amnet/signin", async ctx => { CardReadRequest request; try { request = await ctx.Request.ReadFromJsonAsync(); } catch (JsonException e) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsync(e.Message); return; } if (string.IsNullOrEmpty(request?.MatrixCode)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsync("Card id not provided."); return; } if (request.MatrixCode.Length is 0 or < 20) { ctx.Response.StatusCode = 422; await ctx.Response.WriteAsync("Invalid card id length."); return; } if (request.MatrixCode.Any(x => !char.IsNumber(x))) { ctx.Response.StatusCode = 422; await ctx.Response.WriteAsync("Invalid card id format."); return; } lock (_cardLock) { // ratelimit check if (_currentCard?.Expires > Environment.TickCount64) { ctx.Response.StatusCode = 429; ctx.Response.Headers.RetryAfter = ((int)TimeSpan.FromMilliseconds(_currentCard.Value.Expires - Environment.TickCount64).TotalSeconds).ToString(); return; } // ensure the matrix code is 20-digits long, otherwise pad with zeros var matrixCode = request.MatrixCode.PadLeft(20, '0'); var bytes = new byte[10]; for (var i = 0; i < 10; i++) { var value = matrixCode.Substring(i * 2, 2); bytes[i] = byte.Parse(value, NumberStyles.HexNumber); } // it's called a tick count, but it uses milliseconds despite having a unit of time called a tick??? _currentCard = (bytes, matrixCode, Environment.TickCount64 + 5000); } ctx.Response.StatusCode = 202; }); CancellationTokenRegistration cancellationRegistration = default; cancellationRegistration = app.Lifetime.ApplicationStarted.Register(() => { _serverLogger.LogInformation("AMNet Server ({gameId}) started successfully.", _gameId); _serverLogger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress); cancellationRegistration.Dispose(); }); app.RunAsync(); return 0; } [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_poll")] public static int NfcPoll(byte unitNo) { if (_currentCard.HasValue && _currentCard.Value.Expires < Environment.TickCount64) { lock (_cardLock) { _currentCard = null; } } // there's no polling here (handled by the webserver) return 0; } [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_aime_id")] public static int GetAimeId(byte unitNo, nint luid, nint luidSize) { lock (_cardLock) { if (unitNo != 0 || _currentCard == null) { return 1; } if (_currentCard.Value.Expires < Environment.TickCount64) { _serverLogger.LogWarning("Submitted card id expired before it could be read."); _currentCard = null; return 1; } Marshal.Copy(_currentCard.Value.IdBytes, 0, luid, (int)luidSize); _serverLogger.LogInformation("Card read in: {0}", string.Join(" ", Enumerable.Range(0, 5).Select(x => _currentCard.Value.OriginalId.Substring(x * 4, 4)))); _currentCard = null; } return 0; } [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_felica_id")] public static unsafe int GetFelicaId(byte unitNo, ulong* idm) { // felica not supported return 1; } [UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")] public static void SetLedColour(byte unitNo, byte r, byte g, byte b) { // do nothing } private static unsafe void LoadConfiguration() { const string configFileName = @".\segatools.ini"; var gameId = stackalloc char[5]; var gameIdStr = new PWSTR(gameId); var serverName = stackalloc char[64]; var serverNameStr = new PWSTR(serverName); var serverAddress = stackalloc char[1024]; var serverAddressStr = new PWSTR(serverAddress); PInvoke.GetPrivateProfileString("aimeio", "gameId", string.Empty, gameIdStr, 5, configFileName); PInvoke.GetPrivateProfileString("aimeio", "serverName", Environment.MachineName, serverNameStr, 64, configFileName); PInvoke.GetPrivateProfileString("aimeio", "serverAddress", "http://+:6070", serverAddressStr, 1024, configFileName); _gameId = gameIdStr.ToString(); _serverName = serverNameStr.ToString(); _serverAddresses = serverAddressStr.ToString().Split(';'); } }