diff --git a/AMNet.Server/AMNet.Server.csproj b/AMNet.Server/AMNet.Server.csproj
deleted file mode 100644
index 85373f3..0000000
--- a/AMNet.Server/AMNet.Server.csproj
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
- true
- Library
- net9.0-windows
- Size
- true
-
-
-
- AMNet
- ppc
- amnet
- AMNet Server
- Copyright (c) 2024-25 ppc
- Server component for the AMNet IC Card switcher
-
-
-
- None
- false
- false
- false
- false
- true
- false
- false
- false
- false
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
diff --git a/AMNet.Server/CardPresenter.cs b/AMNet.Server/CardPresenter.cs
deleted file mode 100644
index b2d18fe..0000000
--- a/AMNet.Server/CardPresenter.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-
-namespace AMNet.Server;
-
-///
-/// Represents a storage location to load/read cards from
-///
-internal class CardPresenter
-{
- private readonly Lock _cardLock = new();
- private StoredCard _currentCard;
-
- public record StoredCard(byte[] AccessCode, byte[] IDm, long ExpiresAt)
- {
- public bool Expired => Environment.TickCount64 >= ExpiresAt;
-
- public override string ToString() => FormatHexCode(AccessCode);
- }
-
- ///
- /// Returns the currently active card without clearing it
- ///
- public StoredCard PeekActiveCard()
- {
- lock (_cardLock)
- {
- return _currentCard;
- }
- }
-
- 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;
-
- return card;
- }
- }
-
- ///
- /// Sets the card
- ///
- /// The access code of the card to use
- /// The FeliCa card IDm, if a physical card was used
- /// How long the card should be presented to the game for in milliseconds
- /// Whether the card was set successfully
- public StoredCard SetCard(string accessCode, string idmHex, long validFor = 5000)
- {
- // ensure the matrix code is 20-digits long, otherwise pad with zeros
- var matrixCode = accessCode.Replace(" ", "").PadLeft(20, '0');
- var bytes = new byte[10];
-
- for (var i = 0; i < bytes.Length; i++)
- {
- var value = matrixCode.Substring(i * 2, 2);
- if (byte.TryParse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b))
- {
- bytes[i] = b;
- }
- else
- {
- return null;
- }
- }
-
- 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)
- {
- _currentCard = card;
- }
-
- return card;
- }
-
- ///
- /// Formats a byte array as a hex string, inserting a space every 2 characters
- ///
- public static string FormatHexCode(byte[] code) => string.Join(" ", code.Chunk(2).Select(Convert.ToHexString));
-}
\ No newline at end of file
diff --git a/AMNet.Server/Config.cs b/AMNet.Server/Config.cs
deleted file mode 100644
index 2213b18..0000000
--- a/AMNet.Server/Config.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System;
-using System.Reflection;
-using Windows.Win32;
-using Windows.Win32.Foundation;
-
-namespace AMNet.Server;
-
-internal static class Config
-{
- internal const string IOSection = "aimeio";
-
- static Config()
- {
- GameId = ReadKey(IOSection, "gameId", 4);
- ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName);
-
- PhysicalCardUseIDm = PInvoke.GetPrivateProfileInt(IOSection, "useAimeDBForPhysicalCards", 0, ConfigFilePath) > 0;
- EnableAimeTxt = PInvoke.GetPrivateProfileInt(IOSection, "enableKeyboardMode", 1, ConfigFilePath) > 0;
- AimeTxtPath = ReadKey("aime", "aimePath", PInvoke.MAX_PATH, @"DEVICE\aime.txt");
- }
-
- public static string GameId { get; }
-
- ///
- /// The server name to show on devices. Defaults to the computer name.
- ///
- public static string ServerName { get; }
-
- ///
- /// Whether physically scanned cards should have their IDm forwarded to the game instead of the access code.
- ///
- public static bool PhysicalCardUseIDm { get; }
-
- ///
- /// Whether holding the enter key should trigger a card scan using the code from the aime.txt file
- ///
- public static bool EnableAimeTxt { get; }
-
- ///
- /// Path to the aime.txt file. Defaults to .\DEVICE\aime.txt
- ///
- public static string AimeTxtPath { get; }
-
- public static Version ServerVersion { get; } = new(Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "1.0");
-
- private static string ConfigFilePath { get; } = Environment.GetEnvironmentVariable("SEGATOOLS_CONFIG_PATH") ?? ".\\segatools.ini";
-
- internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null)
- {
- // +1 for null terminator
- var buffer = stackalloc char[(int)maxLength + 1];
- var bufferStr = new PWSTR(buffer);
-
- fixed (char* pSection = section)
- fixed (char* pKey = key)
- fixed (char* pDefault = @default ?? string.Empty)
- fixed (char* pDefaultPath = ConfigFilePath)
- {
- PInvoke.GetPrivateProfileString(new PCWSTR(pSection), new PCWSTR(pKey), new PCWSTR(pDefault), bufferStr, maxLength + 1, new PCWSTR(pDefaultPath));
- }
-
- return bufferStr.ToString();
- }
-}
\ No newline at end of file
diff --git a/AMNet.Server/DllMain.cs b/AMNet.Server/DllMain.cs
deleted file mode 100644
index 099b46c..0000000
--- a/AMNet.Server/DllMain.cs
+++ /dev/null
@@ -1,144 +0,0 @@
-using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
-using Windows.Win32;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace AMNet.Server;
-
-public static class DllMain
-{
- private const string WebAddress = "http://card.ppc.moe";
-
- // instance metrics
- internal static long LastPollTime = -1;
- internal static long ServerStartedAt;
-
- static DllMain()
- {
- PInvoke.AllocConsole();
-
- App = WebServer.BuildServer(Config.ReadKey(Config.IOSection, "serverAddress", 1024, "http://+:6070").Split(';'));
- }
-
- private static WebApplication App { get; }
-
- [UnmanagedCallersOnly(EntryPoint = "aime_io_get_api_version")]
- public static ushort GetApiVersion() => 0x0100;
-
- [UnmanagedCallersOnly(EntryPoint = "aime_io_init")]
- public static int Init()
- {
- CancellationTokenRegistration? cancellationRegistration = null;
- cancellationRegistration = App.Lifetime.ApplicationStarted.Register(() =>
- {
- App.Logger.LogInformation("AMNet Server v{version} ({gameId}) started.", Config.ServerVersion.ToString(Config.ServerVersion.Build > 0 ? 3 : 2), Config.GameId ?? "SXXX");
- App.Logger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress);
-
- // ReSharper disable once AccessToModifiedClosure
- cancellationRegistration?.Dispose();
- });
-
- Volatile.Write(ref ServerStartedAt, Environment.TickCount64);
- App.RunAsync();
- return 0;
- }
-
- [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_poll")]
- public static int NfcPoll(byte unitNo)
- {
- Volatile.Write(ref LastPollTime, Environment.TickCount64);
-
- if (!Config.EnableAimeTxt)
- {
- return 1;
- }
-
- // check keyboard input for the enter key being held down (0x0D)
- var enterPressed = (PInvoke.GetAsyncKeyState(0x0D) & 0x8000) != 0;
-
- if (!enterPressed)
- {
- return 0;
- }
-
- try
- {
- using var aimeTxtReader = new StreamReader(File.OpenRead(Config.AimeTxtPath), Encoding.UTF8);
-
- var fileName = Path.GetFileName(Config.AimeTxtPath);
- var cardResult = App.Services.GetRequiredService().SetCard(aimeTxtReader.ReadLine(), null);
-
- if (cardResult == null)
- {
- App.Logger.LogWarning("Failed to read card from {file}: invalid format", fileName);
- }
- else
- {
- App.Logger.LogInformation("Card loaded from {fileName}", fileName);
- }
- }
- catch (IOException e)
- {
- App.Logger.LogWarning("Failed to read '{path}': {message}", Path.GetFullPath(Config.AimeTxtPath), e.Message);
- }
-
- return 0;
- }
-
- [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_aime_id")]
- public static int GetAimeId(byte unitNo, IntPtr luid, nint luidSize)
- {
- if (unitNo != 0)
- {
- return 1;
- }
-
- // if physical cards are being directed to
- var card = App.Services.GetRequiredService().TakeActiveCard(Config.PhysicalCardUseIDm ? false : null);
- if (card?.Expired != false)
- {
- return 1;
- }
-
- 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)
- {
- if (!Config.PhysicalCardUseIDm || unitNo != 0)
- {
- return 1;
- }
-
- var card = App.Services.GetRequiredService().TakeActiveCard(true);
- if (card?.Expired != false)
- {
- return 1;
- }
-
- ulong idmValue = 0;
- for (var i = 0 ; i < 8 ; i++)
- {
- idmValue = (idmValue << 8) | card.IDm[i];
- }
-
- *idm = idmValue;
-
- App.Logger.LogInformation("FeliCa IDm read: {idm}", CardPresenter.FormatHexCode(card.IDm));
- return 0;
- }
-
- [UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")]
- public static void SetLedColour(byte unitNo, byte r, byte g, byte b)
- {
- // do nothing
- }
-}
\ No newline at end of file
diff --git a/AMNet.Server/NativeMethods.txt b/AMNet.Server/NativeMethods.txt
deleted file mode 100644
index 7976195..0000000
--- a/AMNet.Server/NativeMethods.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-GetPrivateProfileStringW
-GetPrivateProfileInt
-GetAsyncKeyState
-AllocConsole
-MAX_PATH
diff --git a/AMNet.Server/WebServer.cs b/AMNet.Server/WebServer.cs
deleted file mode 100644
index 0e3d127..0000000
--- a/AMNet.Server/WebServer.cs
+++ /dev/null
@@ -1,158 +0,0 @@
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace AMNet.Server;
-
-internal static partial class WebServer
-{
- private const int ApiVersion = 1;
-
- public record SystemState(
- [property: JsonPropertyName("apiVersion")] int ApiVersion,
- [property: JsonPropertyName("gameId")] string GameId,
- [property: JsonPropertyName("serverName")] string ServerName,
- [property: JsonPropertyName("sessionUptime")] long SessionUptime,
- [property: JsonPropertyName("timeSinceLastPoll")] long? TimeSinceLastPoll);
-
- public record CardReadRequest(
- [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)
- {
- var builder = WebApplication.CreateSlimBuilder([]);
-
- builder.WebHost.UseUrls(listenAddresses);
-
- builder.Logging.ClearProviders();
- builder.Logging.AddSimpleConsole(o =>
- {
- o.SingleLine = true;
- o.IncludeScopes = false;
- });
-
- builder.Logging.SetMinimumLevel(LogLevel.Information);
- builder.Logging.AddFilter("Microsoft.AspNetCore.Http.Result", LogLevel.None);
- builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Error);
-
- builder.Services.AddCors(cors =>
- {
- cors.AddDefaultPolicy(policy =>
- {
- policy.AllowAnyMethod();
- policy.AllowAnyOrigin();
- policy.AllowAnyHeader();
-
- policy.SetPreflightMaxAge(TimeSpan.FromHours(6));
- });
- });
-
- builder.Services.ConfigureHttpJsonOptions(options =>
- {
- options.SerializerOptions.TypeInfoResolverChain.Insert(0, SerializerContext.Default);
- });
-
- builder.Services.AddSingleton();
-
- var app = builder.Build();
-
- app.UseCors();
-
- app.MapGet("/amnet/info", ServerInfo);
- app.MapPost("/amnet/signin", ProcessCard);
-
- return app;
- }
-
- private static IResult ServerInfo()
- {
- var startedAt = Volatile.Read(ref DllMain.ServerStartedAt);
- var lastPollAt = Volatile.Read(ref DllMain.LastPollTime);
-
- var state = new SystemState(
- ApiVersion,
- Config.GameId,
- Config.ServerName,
- Environment.TickCount64 - startedAt,
- lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt);
-
- return Results.Ok(state);
- }
-
- private static async Task ProcessCard(HttpContext ctx)
- {
- CardReadRequest request;
-
- try
- {
- request = await ctx.Request.ReadFromJsonAsync();
- }
- catch (JsonException e)
- {
- await WriteErrorResponse(ctx, StatusCodes.Status400BadRequest, e.Message);
- return;
- }
-
- if (string.IsNullOrEmpty(request?.AccessCode))
- {
- await WriteErrorResponse(ctx, StatusCodes.Status400BadRequest, "Access Code not provided.");
- return;
- }
-
- if (request.AccessCode.Length is 0 or < 20 || !request.AccessCode.Any(char.IsAsciiDigit))
- {
- 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;
- }
-
- var cardPresenter = ctx.RequestServices.GetRequiredService();
- var currentCard = cardPresenter.PeekActiveCard();
-
- if (currentCard?.Expired == false)
- {
- 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.AccessCode, request.PhysicalCardIDm) != null)
- {
- ctx.Response.StatusCode = StatusCodes.Status202Accepted;
- }
- else
- {
- 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;
-}
\ No newline at end of file
diff --git a/AMNet.sln b/AMNet.sln
deleted file mode 100644
index 72058e5..0000000
--- a/AMNet.sln
+++ /dev/null
@@ -1,16 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMNet.Server", "AMNet.Server\AMNet.Server.csproj", "{C9FA2545-F15C-4517-B3AB-55B8FC23FA04}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
-EndGlobal