diff --git a/Application/Application.csproj b/Application/Application.csproj
new file mode 100644
index 0000000..647d439
--- /dev/null
+++ b/Application/Application.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Application/DependencyInjection.cs b/Application/DependencyInjection.cs
new file mode 100644
index 0000000..6ccba01
--- /dev/null
+++ b/Application/DependencyInjection.cs
@@ -0,0 +1,15 @@
+using System.Reflection;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Application;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddApplication(this IServiceCollection services)
+ {
+ services.AddMediatR(Assembly.GetExecutingAssembly());
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/Application/Dto/CardDto.cs b/Application/Dto/CardDto.cs
new file mode 100644
index 0000000..74f1d57
--- /dev/null
+++ b/Application/Dto/CardDto.cs
@@ -0,0 +1,33 @@
+using System.Xml.Serialization;
+
+namespace Application.Dto;
+
+public class CardDto
+{
+ [XmlElement(ElementName = "card_id")]
+ public long CardId { get; set; }
+
+ [XmlElement(ElementName = "player_name")]
+ public string PlayerName { get; set; } = string.Empty;
+
+ [XmlElement("score_i1")]
+ public long ScoreI1 { get; set; }
+
+ [XmlElement("fcol1")]
+ public long Fcol1 { get; set; }
+
+ [XmlElement("fcol2")]
+ public long Fcol2 { get; set; }
+
+ [XmlElement("fcol3")]
+ public long Fcol3 { get; set; }
+
+ [XmlElement("achieve_status")]
+ public string AchieveStatus { get; set; } = string.Empty;
+
+ [XmlElement("created")]
+ public string Created { get; set; } = string.Empty;
+
+ [XmlElement("modified")]
+ public string Modified { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/Application/Game/Option/PlayCountQuery.cs b/Application/Game/Option/PlayCountQuery.cs
new file mode 100644
index 0000000..093a7f5
--- /dev/null
+++ b/Application/Game/Option/PlayCountQuery.cs
@@ -0,0 +1,62 @@
+using Application.Interfaces;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Application.Game.Option;
+
+public record PlayCountQuery(long CardId) : IRequest;
+
+public class PlayCountQueryHandler : IRequestHandler
+{
+ private readonly ICardDbContext context;
+
+ private readonly ILogger logger;
+
+ public PlayCountQueryHandler(ICardDbContext context, ILogger logger)
+ {
+ this.context = context;
+ this.logger = logger;
+ }
+
+ public async Task Handle(PlayCountQuery request, CancellationToken cancellationToken)
+ {
+ return await GetPlayCount(request.CardId);
+ }
+
+ private async Task GetPlayCount(long cardId)
+ {
+ var record = await context.CardPlayCounts.FirstOrDefaultAsync(count => count.CardId == cardId);
+ if (record is null)
+ {
+ return 0;
+ }
+
+ var now = DateTime.Now;
+ var lastPlayedTime = record.LastPlayedTime;
+
+ if (now <= lastPlayedTime)
+ {
+ logger.LogWarning("Clock skew detected! " +
+ "Current time: {Now}," +
+ "Last Play Time: {Last}", now, lastPlayedTime);
+ return 0;
+ }
+
+ DateTime start;
+ DateTime end;
+ if (now.Hour >= 8)
+ {
+ start = DateTime.Today.AddHours(8);
+ end = start.AddHours(24);
+ }
+ else
+ {
+ end = DateTime.Today.AddHours(8);
+ start = end.AddHours(-24);
+ }
+
+ var inBetween = lastPlayedTime >= start && lastPlayedTime <= end;
+ return inBetween ? record.PlayCount : 0;
+ }
+}
\ No newline at end of file
diff --git a/Application/Game/Server/CertifyCommand.cs b/Application/Game/Server/CertifyCommand.cs
new file mode 100644
index 0000000..220a44e
--- /dev/null
+++ b/Application/Game/Server/CertifyCommand.cs
@@ -0,0 +1,100 @@
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+using Application.Interfaces;
+using Domain.Config;
+using MediatR;
+using Microsoft.Extensions.Options;
+
+namespace Application.Game.Server;
+
+public record CertifyCommand(string? Gid, string? Mac, string? Random, string? Md5, string Host) : IRequest;
+
+public partial class CertifyCommandHandler : IRequestHandler
+{
+ private readonly RelayConfig relayConfig;
+
+
+ public CertifyCommandHandler(IOptions relayOptions)
+ {
+ relayConfig = relayOptions.Value;
+ }
+
+ public Task Handle(CertifyCommand request, CancellationToken cancellationToken)
+ {
+ if (request.Gid == null)
+ {
+ return Task.FromResult(QuitWithError(ErrorCode.ErrorNoGid));
+ }
+
+ if (request.Mac == null)
+ {
+ return Task.FromResult(QuitWithError(ErrorCode.ErrorNoMac));
+ }
+
+ if (request.Random == null)
+ {
+ return Task.FromResult(QuitWithError(ErrorCode.ErrorNoRandom));
+ }
+
+ if (request.Md5 == null)
+ {
+ return Task.FromResult(QuitWithError(ErrorCode.ErrorNoHash));
+ }
+
+ if (!MacValid(request.Mac))
+ {
+ return Task.FromResult(QuitWithError(ErrorCode.ErrorInvalidMac));
+ }
+
+ if (!Md5Valid(request.Md5))
+ {
+ return Task.FromResult(QuitWithError(ErrorCode.ErrorInvalidHash));
+ }
+
+ var ticket = string.Join(string.Empty,
+ MD5.HashData(Encoding.UTF8.GetBytes(request.Gid)).Select(b => b.ToString("x2")));
+
+ var response = $"host=card_id=7020392000147361,relay_addr={relayConfig.RelayServer},relay_port={relayConfig.RelayPort}\n" +
+ "no=1337\n" +
+ "name=123\n" +
+ "pref=nesys\n" +
+ "addr=nesys@home\n" +
+ "x-next-time=15\n" +
+ $"x-img=http://{request.Host}/news.png\n" +
+ $"x-ranking=http://{request.Host}/ranking/ranking.php\n" +
+ $"ticket={ticket}";
+
+ return Task.FromResult(response);
+ }
+
+ private static bool MacValid(string mac)
+ {
+ return MacRegex().IsMatch(mac);
+ }
+
+ private static bool Md5Valid(string md5)
+ {
+ return Md5Regex().IsMatch(md5);
+ }
+
+ private static string QuitWithError(ErrorCode errorCode)
+ {
+ return $"error={(int)errorCode}";
+ }
+
+ private enum ErrorCode
+ {
+ ErrorNoGid,
+ ErrorNoMac,
+ ErrorNoRandom,
+ ErrorNoHash,
+ ErrorInvalidMac,
+ ErrorInvalidHash
+ }
+
+ [GeneratedRegex("^[a-fA-F0-9]{12}$")]
+ private static partial Regex MacRegex();
+ [GeneratedRegex("^[a-fA-F0-9]{32}$")]
+ private static partial Regex Md5Regex();
+}
diff --git a/Application/Game/Server/GetDataQuery.cs b/Application/Game/Server/GetDataQuery.cs
new file mode 100644
index 0000000..2fbe74b
--- /dev/null
+++ b/Application/Game/Server/GetDataQuery.cs
@@ -0,0 +1,48 @@
+using System.Text;
+using Application.Interfaces;
+using MediatR;
+
+namespace Application.Game.Server;
+
+public record GetDataQuery(string Host) : IRequest;
+
+public class GetDataQueryHandler : IRequestHandler
+{
+ private readonly IEventManagerService eventManagerService;
+
+ public GetDataQueryHandler(IEventManagerService eventManagerService)
+ {
+ this.eventManagerService = eventManagerService;
+ }
+
+ public Task Handle(GetDataQuery request, CancellationToken cancellationToken)
+ {
+ var response = "count=0\n" +
+ "nexttime=0\n";
+ if (!eventManagerService.UseEvents())
+ {
+ return Task.FromResult(response);
+ }
+
+ var host = request.Host;
+ var urlBase = $"http://{host}/events/";
+ var dataString = new StringBuilder();
+ var events = eventManagerService.GetEvents();
+ var count = 0;
+ foreach (var pair in events.Select((@event, i) => new {Value = @event, Index = i}))
+ {
+ var value = pair.Value;
+ var index = pair.Index;
+ var fileUrl = $"{urlBase}{value.Name}";
+ var eventString = $"{index},{fileUrl},{value.NotBefore},{value.NotAfter},{value.Md5},{value.Index}";
+ dataString.Append(eventString).Append('\n');
+ count++;
+ }
+
+ response = $"count={count}\n" +
+ "nexttime=1\n" +
+ $"{dataString}";
+
+ return Task.FromResult(response);
+ }
+}
\ No newline at end of file
diff --git a/Application/Interfaces/ICardDbContext.cs b/Application/Interfaces/ICardDbContext.cs
new file mode 100644
index 0000000..ac455fe
--- /dev/null
+++ b/Application/Interfaces/ICardDbContext.cs
@@ -0,0 +1,19 @@
+using Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Interfaces;
+
+public interface ICardDbContext
+{
+ public DbSet CardBdata { get; set; }
+
+ public DbSet CardDetails { get; set; }
+
+ public DbSet CardMains { get; set; }
+
+ public DbSet CardPlayCounts { get; set; }
+
+ public Task SaveChangesAsync(CancellationToken cancellationToken);
+
+
+}
\ No newline at end of file
diff --git a/Application/Interfaces/IEventManagerService.cs b/Application/Interfaces/IEventManagerService.cs
new file mode 100644
index 0000000..fcbc4f3
--- /dev/null
+++ b/Application/Interfaces/IEventManagerService.cs
@@ -0,0 +1,12 @@
+using Domain.Models;
+
+namespace Application.Interfaces;
+
+public interface IEventManagerService
+{
+ public void InitializeEvents();
+
+ public bool UseEvents();
+
+ public IEnumerable GetEvents();
+}
\ No newline at end of file
diff --git a/Application/Interfaces/IMusicDbContext.cs b/Application/Interfaces/IMusicDbContext.cs
new file mode 100644
index 0000000..75ae500
--- /dev/null
+++ b/Application/Interfaces/IMusicDbContext.cs
@@ -0,0 +1,13 @@
+using Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Interfaces;
+
+public interface IMusicDbContext
+{
+ public DbSet MusicAous { get; set; }
+
+ public DbSet MusicExtras { get; set; }
+
+ public DbSet MusicUnlocks { get; set; }
+}
\ No newline at end of file
diff --git a/Application/Mappers/CardMapper.cs b/Application/Mappers/CardMapper.cs
new file mode 100644
index 0000000..ad86f71
--- /dev/null
+++ b/Application/Mappers/CardMapper.cs
@@ -0,0 +1,11 @@
+using Application.Dto;
+using Domain.Entities;
+using Riok.Mapperly.Abstractions;
+
+namespace Application.Mappers;
+
+[Mapper]
+public static partial class CardMapper
+{
+ public static partial CardDto CardMainToCardDto(this CardMain cardMain);
+}
\ No newline at end of file
diff --git a/Domain/Config/EventConfig.cs b/Domain/Config/EventConfig.cs
new file mode 100644
index 0000000..15a4105
--- /dev/null
+++ b/Domain/Config/EventConfig.cs
@@ -0,0 +1,17 @@
+namespace Domain.Config;
+
+public class EventConfig
+{
+ public const string EVENT_SECTION = "Events";
+
+ public bool UseEvents { get; set; }
+
+ public List EventFiles { get; set; } = new();
+}
+
+public class EventFile
+{
+ public string FileName { get; set; } = string.Empty;
+
+ public int Index { get; set; }
+}
\ No newline at end of file
diff --git a/Domain/Config/RelayConfig.cs b/Domain/Config/RelayConfig.cs
new file mode 100644
index 0000000..be3dc88
--- /dev/null
+++ b/Domain/Config/RelayConfig.cs
@@ -0,0 +1,10 @@
+namespace Domain.Config;
+
+public class RelayConfig
+{
+ public const string RELAY_SECTION = "Relay";
+
+ public string RelayServer { get; set; } = "127.0.0.1";
+
+ public int RelayPort { get; set; } = 3333;
+}
\ No newline at end of file
diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj
new file mode 100644
index 0000000..6836c68
--- /dev/null
+++ b/Domain/Domain.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
diff --git a/Domain/Entities/CardBdatum.cs b/Domain/Entities/CardBdatum.cs
new file mode 100644
index 0000000..21558dc
--- /dev/null
+++ b/Domain/Entities/CardBdatum.cs
@@ -0,0 +1,10 @@
+namespace Domain.Entities;
+
+public partial class CardBdatum
+{
+ public long CardId { get; set; }
+
+ public string? Bdata { get; set; }
+
+ public long BdataSize { get; set; }
+}
diff --git a/Domain/Entities/CardDetail.cs b/Domain/Entities/CardDetail.cs
new file mode 100644
index 0000000..b31db01
--- /dev/null
+++ b/Domain/Entities/CardDetail.cs
@@ -0,0 +1,38 @@
+namespace Domain.Entities;
+
+public partial class CardDetail
+{
+ public long CardId { get; set; }
+
+ public long Pcol1 { get; set; }
+
+ public long Pcol2 { get; set; }
+
+ public long Pcol3 { get; set; }
+
+ public long ScoreI1 { get; set; }
+
+ public long ScoreUi1 { get; set; }
+
+ public long ScoreUi2 { get; set; }
+
+ public long ScoreUi3 { get; set; }
+
+ public long ScoreUi4 { get; set; }
+
+ public long ScoreUi5 { get; set; }
+
+ public long ScoreUi6 { get; set; }
+
+ public long ScoreBi1 { get; set; }
+
+ public string? LastPlayTenpoId { get; set; } = string.Empty;
+
+ public long Fcol1 { get; set; }
+
+ public long Fcol2 { get; set; }
+
+ public long Fcol3 { get; set; }
+
+ public DateTime LastPlayTime { get; set; }
+}
diff --git a/Domain/Entities/CardMain.cs b/Domain/Entities/CardMain.cs
new file mode 100644
index 0000000..f3f092f
--- /dev/null
+++ b/Domain/Entities/CardMain.cs
@@ -0,0 +1,22 @@
+namespace Domain.Entities;
+
+public partial class CardMain
+{
+ public long CardId { get; set; }
+
+ public string PlayerName { get; set; } = string.Empty;
+
+ public long ScoreI1 { get; set; }
+
+ public long Fcol1 { get; set; }
+
+ public long Fcol2 { get; set; }
+
+ public long Fcol3 { get; set; }
+
+ public string AchieveStatus { get; set; } = string.Empty;
+
+ public string? Created { get; set; } = string.Empty;
+
+ public string? Modified { get; set; } = string.Empty;
+}
diff --git a/Domain/Entities/CardPlayCount.cs b/Domain/Entities/CardPlayCount.cs
new file mode 100644
index 0000000..bedb19f
--- /dev/null
+++ b/Domain/Entities/CardPlayCount.cs
@@ -0,0 +1,10 @@
+namespace Domain.Entities;
+
+public partial class CardPlayCount
+{
+ public long CardId { get; set; }
+
+ public long PlayCount { get; set; }
+
+ public DateTime LastPlayedTime { get; set; }
+}
diff --git a/Domain/Entities/MusicAou.cs b/Domain/Entities/MusicAou.cs
new file mode 100644
index 0000000..cf44ecf
--- /dev/null
+++ b/Domain/Entities/MusicAou.cs
@@ -0,0 +1,8 @@
+namespace Domain.Entities;
+
+public class MusicAou
+{
+ public long MusicId { get; set; }
+
+ public bool UseFlag { get; set; }
+}
diff --git a/Domain/Entities/MusicExtra.cs b/Domain/Entities/MusicExtra.cs
new file mode 100644
index 0000000..0a5a617
--- /dev/null
+++ b/Domain/Entities/MusicExtra.cs
@@ -0,0 +1,8 @@
+namespace Domain.Entities;
+
+public class MusicExtra
+{
+ public long MusicId { get; set; }
+
+ public bool UseFlag { get; set; }
+}
diff --git a/Domain/Entities/MusicUnlock.cs b/Domain/Entities/MusicUnlock.cs
new file mode 100644
index 0000000..c9debbc
--- /dev/null
+++ b/Domain/Entities/MusicUnlock.cs
@@ -0,0 +1,18 @@
+namespace Domain.Entities;
+
+public partial class MusicUnlock
+{
+ public long MusicId { get; set; }
+
+ public string Title { get; set; } = string.Empty;
+
+ public string Artist { get; set; } = string.Empty;
+
+ public DateTime ReleaseDate { get; set; }
+
+ public DateTime EndDate { get; set; }
+
+ public bool NewFlag { get; set; }
+
+ public bool UseFlag { get; set; }
+}
diff --git a/Domain/Models/Event.cs b/Domain/Models/Event.cs
new file mode 100644
index 0000000..fcc0d79
--- /dev/null
+++ b/Domain/Models/Event.cs
@@ -0,0 +1,14 @@
+namespace Domain.Models;
+
+public class Event
+{
+ public string Name { get; set; } = string.Empty;
+
+ public string Md5 { get; set; } = string.Empty;
+
+ public int Index { get; set; }
+
+ public string NotBefore { get; set; } = string.Empty;
+
+ public string NotAfter { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/GC-local-server-rewrite.sln b/GC-local-server-rewrite.sln
index d6b4c18..e5e8e8e 100644
--- a/GC-local-server-rewrite.sln
+++ b/GC-local-server-rewrite.sln
@@ -11,6 +11,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MudAdmin", "MudAdmin\MudAdm
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GCRelayServer", "GCRelayServer\GCRelayServer.csproj", "{268178DF-6345-4D9E-A389-9E809CBF39C9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatchServer", "MatchServer\MatchServer.csproj", "{CB91C3D3-ED69-4DC6-A205-B262A08BEE49}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainServer", "MainServer\MainServer.csproj", "{B8C6CA7E-5E58-43BC-8E03-84306916DE39}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\Application.csproj", "{B0691233-0D7E-4694-8923-646E7A3BDBF4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{CB95F4B7-7627-4CCA-A2B6-2D6FD48446C1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{2666D734-0E81-431E-A22D-216218FC9023}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -33,6 +43,26 @@ Global
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CB91C3D3-ED69-4DC6-A205-B262A08BEE49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CB91C3D3-ED69-4DC6-A205-B262A08BEE49}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CB91C3D3-ED69-4DC6-A205-B262A08BEE49}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CB91C3D3-ED69-4DC6-A205-B262A08BEE49}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B8C6CA7E-5E58-43BC-8E03-84306916DE39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B8C6CA7E-5E58-43BC-8E03-84306916DE39}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B8C6CA7E-5E58-43BC-8E03-84306916DE39}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B8C6CA7E-5E58-43BC-8E03-84306916DE39}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B0691233-0D7E-4694-8923-646E7A3BDBF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B0691233-0D7E-4694-8923-646E7A3BDBF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B0691233-0D7E-4694-8923-646E7A3BDBF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B0691233-0D7E-4694-8923-646E7A3BDBF4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CB95F4B7-7627-4CCA-A2B6-2D6FD48446C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CB95F4B7-7627-4CCA-A2B6-2D6FD48446C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CB95F4B7-7627-4CCA-A2B6-2D6FD48446C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CB95F4B7-7627-4CCA-A2B6-2D6FD48446C1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2666D734-0E81-431E-A22D-216218FC9023}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2666D734-0E81-431E-A22D-216218FC9023}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2666D734-0E81-431E-A22D-216218FC9023}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2666D734-0E81-431E-A22D-216218FC9023}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/GC-local-server-rewrite.sln.DotSettings.user b/GC-local-server-rewrite.sln.DotSettings.user
index 46fd807..06d7a7b 100644
--- a/GC-local-server-rewrite.sln.DotSettings.user
+++ b/GC-local-server-rewrite.sln.DotSettings.user
@@ -1,4 +1,5 @@
+
True
True
True
diff --git a/Infrastructure/Common/CertificateService.cs b/Infrastructure/Common/CertificateService.cs
new file mode 100644
index 0000000..a547911
--- /dev/null
+++ b/Infrastructure/Common/CertificateService.cs
@@ -0,0 +1,306 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using CertificateManager;
+using CertificateManager.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Common;
+
+public class CertificateService
+{
+ private const X509KeyUsageFlags ROOT_CA_X509_KEY_USAGE_FLAGS = X509KeyUsageFlags.KeyCertSign |
+ X509KeyUsageFlags.DataEncipherment |
+ X509KeyUsageFlags.KeyEncipherment |
+ X509KeyUsageFlags.DigitalSignature;
+
+ private const X509KeyStorageFlags X509_KEY_STORAGE_FLAGS_MACHINE = X509KeyStorageFlags.PersistKeySet |
+ X509KeyStorageFlags.MachineKeySet |
+ X509KeyStorageFlags.Exportable;
+
+ private const X509KeyUsageFlags CERT_X509_KEY_USAGE_FLAGS = X509KeyUsageFlags.DataEncipherment |
+ X509KeyUsageFlags.KeyEncipherment |
+ X509KeyUsageFlags.DigitalSignature;
+
+ private const string ROOT_CA_CN = "Taito Arcade Machine CA";
+ private const string CERT_CN = "GC local server";
+ private const string CERT_DIR = "Certificates";
+ private const string CERT_FILE_NAME = "cert.pfx";
+ private const string ROOT_CERT_FILE_NAME = "root.pfx";
+ private static readonly string CERT_PATH = Path.Combine(CERT_DIR, CERT_FILE_NAME);
+ private static readonly string ROOT_CERT_PATH = Path.Combine(CERT_DIR, ROOT_CERT_FILE_NAME);
+
+ private ILogger logger;
+
+ private static readonly DistinguishedName ROOT_CA_DISTINGUISHED_NAME = new()
+ {
+ CommonName = ROOT_CA_CN
+ };
+
+ private static readonly DistinguishedName CERT_DISTINGUISHED_NAME = new()
+ {
+ CommonName = CERT_CN
+ };
+
+ private static readonly BasicConstraints ROOT_CA_BASIC_CONSTRAINTS = new()
+ {
+ CertificateAuthority = true,
+ HasPathLengthConstraint = true,
+ PathLengthConstraint = 3,
+ Critical = true
+ };
+
+ public static readonly BasicConstraints CERT_BASIC_CONSTRAINTS = new()
+ {
+ CertificateAuthority = false,
+ HasPathLengthConstraint = false,
+ PathLengthConstraint = 0,
+ Critical = true,
+ };
+
+ private readonly SubjectAlternativeName subjectAlternativeName;
+
+ private static readonly ValidityPeriod VALIDITY_PERIOD = new()
+ {
+ ValidFrom = DateTime.UtcNow,
+ ValidTo = DateTime.UtcNow.AddYears(3)
+ };
+
+ private static readonly OidCollection OID_COLLECTION = new()
+ {
+ OidLookup.ServerAuthentication,
+ OidLookup.AnyPurpose
+ };
+
+ public CertificateService(string serverIp, ILogger logger)
+ {
+ this.logger = logger;
+ subjectAlternativeName = new SubjectAlternativeName
+ {
+ DnsName = new List
+ {
+ "localhost",
+ "cert.nesys.jp",
+ "nesys.taito.co.jp",
+ "fjm170920zero.nesica.net"
+ },
+ IpAddress = System.Net.IPAddress.Parse(serverIp)
+ };
+ }
+
+ public X509Certificate2 InitializeCertificate()
+ {
+ return Environment.OSVersion.Platform == PlatformID.Win32NT
+ ? InitializeCertificateWindows()
+ : InitializeCertificateOthers();
+ }
+
+ private X509Certificate2 InitializeCertificateOthers()
+ {
+ if (CertificateExists())
+ {
+ return new X509Certificate2(CERT_PATH);
+ }
+
+ logger.LogInformation("Existing certs not found! Removing old certificates and genrate new ones...");
+
+ File.Delete(CERT_PATH);
+ File.Delete(ROOT_CERT_PATH);
+
+ return GenerateCertificate();
+ }
+
+ private X509Certificate2 InitializeCertificateWindows()
+ {
+ if (CertificateExists())
+ {
+ var existingCert = GetCertificate(StoreName.My, StoreLocation.LocalMachine, CERT_CN);
+
+ if (existingCert != null)
+ {
+ return existingCert;
+ }
+
+ logger.LogInformation("Existing CN not found! Removing old certificates and genrate new ones...");
+ }
+
+ RemovePreviousCert(StoreName.My, StoreLocation.LocalMachine);
+ RemovePreviousCert(StoreName.Root, StoreLocation.LocalMachine);
+
+ return GenerateCertificate();
+ }
+
+ private X509Certificate2 GenerateCertificate()
+ {
+ var serviceProvider = new ServiceCollection()
+ .AddCertificateManager().BuildServiceProvider();
+
+ var createCertificates = serviceProvider.GetService();
+
+ if (createCertificates == null)
+ {
+ logger.LogError("Cannot initialize CreateCertificates service!");
+ throw new Exception();
+ }
+
+ var rootCa = createCertificates.NewRsaSelfSignedCertificate(
+ ROOT_CA_DISTINGUISHED_NAME,
+ ROOT_CA_BASIC_CONSTRAINTS,
+ VALIDITY_PERIOD,
+ new SubjectAlternativeName(),
+ OID_COLLECTION,
+ ROOT_CA_X509_KEY_USAGE_FLAGS,
+ new RsaConfiguration()
+ );
+
+ var cert = createCertificates.NewRsaChainedCertificate(
+ CERT_DISTINGUISHED_NAME,
+ CERT_BASIC_CONSTRAINTS,
+ VALIDITY_PERIOD,
+ subjectAlternativeName,
+ rootCa,
+ OID_COLLECTION,
+ CERT_X509_KEY_USAGE_FLAGS,
+ new RsaConfiguration()
+ );
+
+ var exportService = serviceProvider.GetService();
+
+ if (exportService == null)
+ {
+ logger.LogError("Cannot initialize ImportExportCertificate service!");
+ throw new Exception();
+ }
+
+ var rootCaPfxBytes = exportService.ExportRootPfx(null, rootCa);
+ var certPfxBytes = exportService.ExportChainedCertificatePfx(null, cert, rootCa);
+
+ var rootCaWithPrivateKey = new X509Certificate2(rootCaPfxBytes, (string)null!,
+ X509_KEY_STORAGE_FLAGS_MACHINE);
+
+ var certWithPrivateKey = new X509Certificate2(certPfxBytes, (string)null!,
+ X509_KEY_STORAGE_FLAGS_MACHINE);
+
+
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ {
+ AddCertToStore(rootCaWithPrivateKey, StoreName.My, StoreLocation.LocalMachine);
+ AddCertToStore(rootCaWithPrivateKey, StoreName.Root, StoreLocation.LocalMachine);
+ AddCertToStore(certWithPrivateKey, StoreName.My, StoreLocation.LocalMachine);
+ }
+
+ Directory.CreateDirectory(CERT_DIR);
+
+ File.WriteAllBytes(ROOT_CERT_PATH, rootCaWithPrivateKey.Export(X509ContentType.Pfx));
+ File.WriteAllBytes(CERT_PATH, certWithPrivateKey.Export(X509ContentType.Pfx));
+
+ return certWithPrivateKey;
+ }
+
+ private void AddCertToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation)
+ {
+ try
+ {
+ var store = new X509Store(storeName, storeLocation);
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(cert);
+
+ store.Close();
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "An exception occurs when adding certificate");
+ }
+ }
+
+ private void RemovePreviousCert(StoreName storeName, StoreLocation storeLocation)
+ {
+ try
+ {
+ var store = new X509Store(storeName, storeLocation);
+ store.Open(OpenFlags.ReadWrite);
+ var result = store.Certificates.Find(X509FindType.FindByIssuerName, ROOT_CA_CN, true);
+
+ if (result.Any())
+ {
+ store.RemoveRange(result);
+ logger.LogInformation("Removed previous certificates!");
+ }
+
+ store.Close();
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "An exception occurs when removing previous certificates");
+ }
+ }
+
+ private bool CertificateExists()
+ {
+ bool certificateExists;
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ {
+ // Non windows just use generated certificate file
+ certificateExists = Path.Exists(Path.Combine(CERT_DIR, "cert.pfx")) &&
+ Path.Exists(Path.Combine(CERT_DIR, "root.pfx"));
+ }
+ else
+ {
+ try
+ {
+ var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
+ store.Open(OpenFlags.ReadOnly);
+ var result = store.Certificates.Find(X509FindType.FindByIssuerName, ROOT_CA_CN, true);
+
+ certificateExists = result.Count == 2;
+
+ store.Close();
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "An exception occurs when checking certificates");
+
+ return false;
+ }
+ }
+
+ if (certificateExists)
+ {
+ logger.LogInformation("Certificate exists!");
+ }
+ else
+ {
+ logger.LogInformation("Certificate not found! Will generate new certs...");
+ }
+
+ return certificateExists;
+ }
+
+ private X509Certificate2? GetCertificate(StoreName storeName, StoreLocation storeLocation, string commonName)
+ {
+ try
+ {
+ var store = new X509Store(storeName, storeLocation);
+ store.Open(OpenFlags.ReadWrite);
+ var result = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName,
+ $"CN={commonName}", true);
+
+ if (result.Any())
+ {
+ logger.LogInformation("Certificate CN={CommonName} found!", commonName);
+
+ return result.First();
+ }
+
+ store.Close();
+
+ return null;
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "An exception occurs when getting certificate {CommonName}", commonName);
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Infrastructure/Common/PathHelper.cs b/Infrastructure/Common/PathHelper.cs
new file mode 100644
index 0000000..cf4a04a
--- /dev/null
+++ b/Infrastructure/Common/PathHelper.cs
@@ -0,0 +1,31 @@
+using System.Diagnostics;
+using Validation;
+
+namespace Infrastructure.Common;
+
+public static class PathHelper
+{
+ public static string DatabasePath = Path.Combine(BasePath, "Database");
+
+ public static string ConfigurationPath = Path.Combine(BasePath, "Configurations");
+
+ public static string BasePath
+ {
+ get
+ {
+ var assemblyPath = Environment.ProcessPath;
+ Assumes.NotNull(assemblyPath);
+
+#if DEBUG
+ var parentFullName = Directory.GetParent(assemblyPath)?.Parent?.Parent?.Parent?.FullName;
+
+ return parentFullName ?? "";
+
+#else
+ var parent = Directory.GetParent(assemblyPath);
+ Assumes.NotNull(parent);
+ return parent.ToString();
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/Infrastructure/DependencyInjection.cs b/Infrastructure/DependencyInjection.cs
new file mode 100644
index 0000000..fa8cac8
--- /dev/null
+++ b/Infrastructure/DependencyInjection.cs
@@ -0,0 +1,46 @@
+using Application.Interfaces;
+using Infrastructure.Common;
+using Infrastructure.Persistence;
+using Infrastructure.Services;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Infrastructure;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddSingleton();
+
+ services.AddDbContext(option =>
+ {
+ var dbName = configuration["MusicDbName"];
+ if (string.IsNullOrEmpty(dbName))
+ {
+ dbName = "music471omni.db3";
+ }
+
+ var path = Path.Combine(PathHelper.DatabasePath, dbName);
+ option.UseSqlite($"Data Source={path}");
+ });
+
+ services.AddDbContext(option =>
+ {
+ var dbName = configuration["CardDbName"];
+ if (string.IsNullOrEmpty(dbName))
+ {
+ dbName = "card.db3";
+ }
+
+ var path = Path.Combine(PathHelper.DatabasePath, dbName);
+ option.UseSqlite($"Data Source={path}");
+ });
+
+ services.AddScoped(provider => provider.GetService() ?? throw new InvalidOperationException());
+ services.AddScoped(provider => provider.GetService() ?? throw new InvalidOperationException());
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/Infrastructure/Exceptions/EventExceptions.cs b/Infrastructure/Exceptions/EventExceptions.cs
new file mode 100644
index 0000000..d486aa6
--- /dev/null
+++ b/Infrastructure/Exceptions/EventExceptions.cs
@@ -0,0 +1,9 @@
+namespace Infrastructure.Exceptions;
+
+public class EventFileNotFoundException : Exception
+{
+}
+
+public class EventFileTypeUnknownException : Exception
+{
+}
\ No newline at end of file
diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj
new file mode 100644
index 0000000..1c474a3
--- /dev/null
+++ b/Infrastructure/Infrastructure.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Infrastructure/Migrations/20230208132952_Initial.Designer.cs b/Infrastructure/Migrations/20230208132952_Initial.Designer.cs
new file mode 100644
index 0000000..503d76a
--- /dev/null
+++ b/Infrastructure/Migrations/20230208132952_Initial.Designer.cs
@@ -0,0 +1,182 @@
+//
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(CardDbContext))]
+ [Migration("20230208132952_Initial")]
+ partial class Initial
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
+
+ modelBuilder.Entity("Domain.Entities.CardBdatum", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("card_id");
+
+ b.Property("Bdata")
+ .HasColumnType("TEXT")
+ .HasColumnName("bdata");
+
+ b.Property("BdataSize")
+ .HasColumnType("INTEGER")
+ .HasColumnName("bdata_size");
+
+ b.HasKey("CardId");
+
+ b.ToTable("card_bdata", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.CardDetail", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("card_id");
+
+ b.Property("Pcol1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("pcol1");
+
+ b.Property("Pcol2")
+ .HasColumnType("INTEGER")
+ .HasColumnName("pcol2");
+
+ b.Property("Pcol3")
+ .HasColumnType("INTEGER")
+ .HasColumnName("pcol3");
+
+ b.Property("Fcol1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol1");
+
+ b.Property("Fcol2")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol2");
+
+ b.Property("Fcol3")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol3");
+
+ b.Property("LastPlayTenpoId")
+ .HasColumnType("TEXT")
+ .HasColumnName("last_play_tenpo_id");
+
+ b.Property("LastPlayTime")
+ .HasColumnType("INTEGER")
+ .HasColumnName("last_play_time");
+
+ b.Property("ScoreBi1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_bi1");
+
+ b.Property("ScoreI1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_i1");
+
+ b.Property("ScoreUi1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui1");
+
+ b.Property("ScoreUi2")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui2");
+
+ b.Property("ScoreUi3")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui3");
+
+ b.Property("ScoreUi4")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui4");
+
+ b.Property("ScoreUi5")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui5");
+
+ b.Property("ScoreUi6")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui6");
+
+ b.HasKey("CardId", "Pcol1", "Pcol2", "Pcol3");
+
+ b.ToTable("card_detail", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.CardMain", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("card_id");
+
+ b.Property("AchieveStatus")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("achieve_status");
+
+ b.Property("Created")
+ .HasColumnType("TEXT")
+ .HasColumnName("created");
+
+ b.Property("Fcol1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol1");
+
+ b.Property("Fcol2")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol2");
+
+ b.Property("Fcol3")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol3");
+
+ b.Property("Modified")
+ .HasColumnType("TEXT")
+ .HasColumnName("modified");
+
+ b.Property("PlayerName")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("player_name");
+
+ b.Property("ScoreI1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_i1");
+
+ b.HasKey("CardId");
+
+ b.ToTable("card_main", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.CardPlayCount", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("card_id");
+
+ b.Property("LastPlayedTime")
+ .HasColumnType("INTEGER")
+ .HasColumnName("last_played_time");
+
+ b.Property("PlayCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("play_count");
+
+ b.HasKey("CardId");
+
+ b.ToTable("CardPlayCount", (string)null);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/20230208132952_Initial.cs b/Infrastructure/Migrations/20230208132952_Initial.cs
new file mode 100644
index 0000000..9ea1b7d
--- /dev/null
+++ b/Infrastructure/Migrations/20230208132952_Initial.cs
@@ -0,0 +1,102 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ ///
+ public partial class Initial : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "card_bdata",
+ columns: table => new
+ {
+ cardid = table.Column(name: "card_id", type: "INTEGER", nullable: false),
+ bdata = table.Column(type: "TEXT", nullable: true),
+ bdatasize = table.Column(name: "bdata_size", type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_card_bdata", x => x.cardid);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "card_detail",
+ columns: table => new
+ {
+ cardid = table.Column(name: "card_id", type: "INTEGER", nullable: false),
+ pcol1 = table.Column(type: "INTEGER", nullable: false),
+ pcol2 = table.Column(type: "INTEGER", nullable: false),
+ pcol3 = table.Column(type: "INTEGER", nullable: false),
+ scorei1 = table.Column(name: "score_i1", type: "INTEGER", nullable: false),
+ scoreui1 = table.Column(name: "score_ui1", type: "INTEGER", nullable: false),
+ scoreui2 = table.Column(name: "score_ui2", type: "INTEGER", nullable: false),
+ scoreui3 = table.Column(name: "score_ui3", type: "INTEGER", nullable: false),
+ scoreui4 = table.Column(name: "score_ui4", type: "INTEGER", nullable: false),
+ scoreui5 = table.Column(name: "score_ui5", type: "INTEGER", nullable: false),
+ scoreui6 = table.Column(name: "score_ui6", type: "INTEGER", nullable: false),
+ scorebi1 = table.Column(name: "score_bi1", type: "INTEGER", nullable: false),
+ lastplaytenpoid = table.Column(name: "last_play_tenpo_id", type: "TEXT", nullable: true),
+ fcol1 = table.Column(type: "INTEGER", nullable: false),
+ fcol2 = table.Column(type: "INTEGER", nullable: false),
+ fcol3 = table.Column(type: "INTEGER", nullable: false),
+ lastplaytime = table.Column(name: "last_play_time", type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_card_detail", x => new { x.cardid, x.pcol1, x.pcol2, x.pcol3 });
+ });
+
+ migrationBuilder.CreateTable(
+ name: "card_main",
+ columns: table => new
+ {
+ cardid = table.Column(name: "card_id", type: "INTEGER", nullable: false),
+ playername = table.Column(name: "player_name", type: "TEXT", nullable: false),
+ scorei1 = table.Column(name: "score_i1", type: "INTEGER", nullable: false),
+ fcol1 = table.Column(type: "INTEGER", nullable: false),
+ fcol2 = table.Column(type: "INTEGER", nullable: false),
+ fcol3 = table.Column(type: "INTEGER", nullable: false),
+ achievestatus = table.Column(name: "achieve_status", type: "TEXT", nullable: false),
+ created = table.Column(type: "TEXT", nullable: true),
+ modified = table.Column(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_card_main", x => x.cardid);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "CardPlayCount",
+ columns: table => new
+ {
+ cardid = table.Column(name: "card_id", type: "INTEGER", nullable: false),
+ playcount = table.Column(name: "play_count", type: "INTEGER", nullable: false),
+ lastplayedtime = table.Column(name: "last_played_time", type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_CardPlayCount", x => x.cardid);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "card_bdata");
+
+ migrationBuilder.DropTable(
+ name: "card_detail");
+
+ migrationBuilder.DropTable(
+ name: "card_main");
+
+ migrationBuilder.DropTable(
+ name: "CardPlayCount");
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/CardDbContextModelSnapshot.cs b/Infrastructure/Migrations/CardDbContextModelSnapshot.cs
new file mode 100644
index 0000000..ddc76ce
--- /dev/null
+++ b/Infrastructure/Migrations/CardDbContextModelSnapshot.cs
@@ -0,0 +1,179 @@
+//
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations
+{
+ [DbContext(typeof(CardDbContext))]
+ partial class CardDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
+
+ modelBuilder.Entity("Domain.Entities.CardBdatum", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("card_id");
+
+ b.Property("Bdata")
+ .HasColumnType("TEXT")
+ .HasColumnName("bdata");
+
+ b.Property("BdataSize")
+ .HasColumnType("INTEGER")
+ .HasColumnName("bdata_size");
+
+ b.HasKey("CardId");
+
+ b.ToTable("card_bdata", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.CardDetail", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("card_id");
+
+ b.Property("Pcol1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("pcol1");
+
+ b.Property("Pcol2")
+ .HasColumnType("INTEGER")
+ .HasColumnName("pcol2");
+
+ b.Property("Pcol3")
+ .HasColumnType("INTEGER")
+ .HasColumnName("pcol3");
+
+ b.Property("Fcol1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol1");
+
+ b.Property("Fcol2")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol2");
+
+ b.Property("Fcol3")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol3");
+
+ b.Property("LastPlayTenpoId")
+ .HasColumnType("TEXT")
+ .HasColumnName("last_play_tenpo_id");
+
+ b.Property("LastPlayTime")
+ .HasColumnType("INTEGER")
+ .HasColumnName("last_play_time");
+
+ b.Property("ScoreBi1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_bi1");
+
+ b.Property("ScoreI1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_i1");
+
+ b.Property("ScoreUi1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui1");
+
+ b.Property("ScoreUi2")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui2");
+
+ b.Property("ScoreUi3")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui3");
+
+ b.Property("ScoreUi4")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui4");
+
+ b.Property("ScoreUi5")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui5");
+
+ b.Property("ScoreUi6")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_ui6");
+
+ b.HasKey("CardId", "Pcol1", "Pcol2", "Pcol3");
+
+ b.ToTable("card_detail", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.CardMain", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("card_id");
+
+ b.Property("AchieveStatus")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("achieve_status");
+
+ b.Property("Created")
+ .HasColumnType("TEXT")
+ .HasColumnName("created");
+
+ b.Property("Fcol1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol1");
+
+ b.Property("Fcol2")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol2");
+
+ b.Property("Fcol3")
+ .HasColumnType("INTEGER")
+ .HasColumnName("fcol3");
+
+ b.Property("Modified")
+ .HasColumnType("TEXT")
+ .HasColumnName("modified");
+
+ b.Property("PlayerName")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("player_name");
+
+ b.Property("ScoreI1")
+ .HasColumnType("INTEGER")
+ .HasColumnName("score_i1");
+
+ b.HasKey("CardId");
+
+ b.ToTable("card_main", (string)null);
+ });
+
+ modelBuilder.Entity("Domain.Entities.CardPlayCount", b =>
+ {
+ b.Property("CardId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("card_id");
+
+ b.Property("LastPlayedTime")
+ .HasColumnType("INTEGER")
+ .HasColumnName("last_played_time");
+
+ b.Property("PlayCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("play_count");
+
+ b.HasKey("CardId");
+
+ b.ToTable("CardPlayCount", (string)null);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Infrastructure/Migrations/MigrationHelper.cs b/Infrastructure/Migrations/MigrationHelper.cs
new file mode 100644
index 0000000..9edc2c0
--- /dev/null
+++ b/Infrastructure/Migrations/MigrationHelper.cs
@@ -0,0 +1,34 @@
+using System.Data;
+using Infrastructure.Common;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+
+namespace Infrastructure.Migrations;
+
+public static class MigrationHelper
+{
+ public static bool Exists(string tableName)
+ {
+ var options = new DbContextOptionsBuilder().UseSqlite($"Data Source={GetConnectionString()}");
+ using var context = new DbContext(options.Options);
+ using var command = context.Database.GetDbConnection().CreateCommand();
+ command.CommandText = $"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='{tableName}';";
+ command.CommandType = CommandType.Text;
+ context.Database.OpenConnection();
+
+ using var reader = command.ExecuteReader();
+
+ return reader.Read()? (long)reader[0] == 1 : false;
+ }
+
+ public static string GetConnectionString()
+ {
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(PathHelper.ConfigurationPath)
+ .AddJsonFile("database.json", optional: false, reloadOnChange: false);
+
+ var cardDbName = builder.Build()["CardDbName"];
+ var cardDbPath = Path.Combine(PathHelper.DatabasePath, cardDbName);
+ return cardDbPath;
+ }
+}
\ No newline at end of file
diff --git a/Infrastructure/Persistence/CardDbContext.cs b/Infrastructure/Persistence/CardDbContext.cs
new file mode 100644
index 0000000..542b43b
--- /dev/null
+++ b/Infrastructure/Persistence/CardDbContext.cs
@@ -0,0 +1,118 @@
+using Application.Interfaces;
+using Domain.Entities;
+using Infrastructure.Common;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Infrastructure.Persistence;
+
+public partial class CardDbContext : DbContext, ICardDbContext
+{
+ public CardDbContext()
+ {
+ }
+
+ public CardDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ public virtual DbSet CardBdata { get; set; } = null!;
+
+ public virtual DbSet CardDetails { get; set; } = null!;
+
+ public virtual DbSet CardMains { get; set; } = null!;
+
+ public virtual DbSet CardPlayCounts { get; set; } = null!;
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (optionsBuilder.IsConfigured)
+ {
+ return;
+ }
+
+ var defaultDb = Path.Combine(PathHelper.DatabasePath, "card.db3");
+ optionsBuilder.UseSqlite($"Data Source={defaultDb}");
+ }
+
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.CardId);
+
+ entity.ToTable("card_bdata");
+
+ entity.Property(e => e.CardId)
+ .ValueGeneratedNever()
+ .HasColumnName("card_id");
+ entity.Property(e => e.Bdata).HasColumnName("bdata");
+ entity.Property(e => e.BdataSize).HasColumnName("bdata_size");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => new { e.CardId, e.Pcol1, e.Pcol2, e.Pcol3 });
+
+ entity.ToTable("card_detail");
+
+ entity.Property(e => e.CardId).HasColumnName("card_id");
+ entity.Property(e => e.Pcol1).HasColumnName("pcol1");
+ entity.Property(e => e.Pcol2).HasColumnName("pcol2");
+ entity.Property(e => e.Pcol3).HasColumnName("pcol3");
+ entity.Property(e => e.Fcol1).HasColumnName("fcol1");
+ entity.Property(e => e.Fcol2).HasColumnName("fcol2");
+ entity.Property(e => e.Fcol3).HasColumnName("fcol3");
+ entity.Property(e => e.LastPlayTenpoId).HasColumnName("last_play_tenpo_id").IsRequired(false);
+ entity.Property(e => e.LastPlayTime).HasColumnName("last_play_time")
+ .HasConversion();
+ entity.Property(e => e.ScoreBi1).HasColumnName("score_bi1");
+ entity.Property(e => e.ScoreI1).HasColumnName("score_i1");
+ entity.Property(e => e.ScoreUi1).HasColumnName("score_ui1");
+ entity.Property(e => e.ScoreUi2).HasColumnName("score_ui2");
+ entity.Property(e => e.ScoreUi3).HasColumnName("score_ui3");
+ entity.Property(e => e.ScoreUi4).HasColumnName("score_ui4");
+ entity.Property(e => e.ScoreUi5).HasColumnName("score_ui5");
+ entity.Property(e => e.ScoreUi6).HasColumnName("score_ui6");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.CardId);
+
+ entity.ToTable("card_main");
+
+ entity.Property(e => e.CardId)
+ .ValueGeneratedNever()
+ .HasColumnName("card_id");
+ entity.Property(e => e.AchieveStatus).HasColumnName("achieve_status");
+ entity.Property(e => e.Created).HasColumnName("created").IsRequired(false);
+ entity.Property(e => e.Fcol1).HasColumnName("fcol1");
+ entity.Property(e => e.Fcol2).HasColumnName("fcol2");
+ entity.Property(e => e.Fcol3).HasColumnName("fcol3");
+ entity.Property(e => e.Modified).HasColumnName("modified").IsRequired(false);
+ entity.Property(e => e.PlayerName).HasColumnName("player_name");
+ entity.Property(e => e.ScoreI1).HasColumnName("score_i1");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.CardId);
+
+ entity.ToTable("CardPlayCount");
+
+ entity.Property(e => e.CardId)
+ .ValueGeneratedNever()
+ .HasColumnName("card_id");
+ entity.Property(e => e.LastPlayedTime).HasColumnName("last_played_time")
+ .HasConversion();
+ entity.Property(e => e.PlayCount).HasColumnName("play_count");
+ });
+
+ OnModelCreatingPartial(modelBuilder);
+ }
+
+ partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
+}
diff --git a/Infrastructure/Persistence/MusicDbContext.cs b/Infrastructure/Persistence/MusicDbContext.cs
new file mode 100644
index 0000000..b2559f8
--- /dev/null
+++ b/Infrastructure/Persistence/MusicDbContext.cs
@@ -0,0 +1,82 @@
+using Application.Interfaces;
+using Domain.Entities;
+using Infrastructure.Common;
+using Microsoft.EntityFrameworkCore;
+
+namespace Infrastructure.Persistence;
+
+public partial class MusicDbContext : DbContext, IMusicDbContext
+{
+ public MusicDbContext()
+ {
+ }
+
+ public MusicDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ public virtual DbSet MusicAous { get; set; } = null!;
+
+ public virtual DbSet MusicExtras { get; set; } = null!;
+
+ public virtual DbSet MusicUnlocks { get; set; } = null!;
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (optionsBuilder.IsConfigured)
+ {
+ return;
+ }
+
+ var defaultDb = Path.Combine(PathHelper.DatabasePath, "music471omni.db3");
+ optionsBuilder.UseSqlite($"Data Source={defaultDb}");
+ }
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.MusicId);
+
+ entity.ToTable("music_aou");
+
+ entity.Property(e => e.MusicId)
+ .ValueGeneratedNever()
+ .HasColumnName("music_id");
+ entity.Property(e => e.UseFlag).HasColumnName("use_flag");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.MusicId);
+
+ entity.ToTable("music_extra");
+
+ entity.Property(e => e.MusicId)
+ .ValueGeneratedNever()
+ .HasColumnName("music_id");
+ entity.Property(e => e.UseFlag).HasColumnName("use_flag");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.MusicId);
+
+ entity.ToTable("music_unlock");
+
+ entity.Property(e => e.MusicId)
+ .ValueGeneratedNever()
+ .HasColumnName("music_id");
+ entity.Property(e => e.Artist).HasColumnName("artist");
+ entity.Property(e => e.Title).HasColumnName("title");
+ entity.Property(e => e.ReleaseDate).HasColumnName("release_date");
+ entity.Property(e => e.EndDate).HasColumnName("end_date");
+ entity.Property(e => e.NewFlag).HasColumnName("new_flag");
+ entity.Property(e => e.UseFlag).HasColumnName("use_flag");
+ });
+
+ OnModelCreatingPartial(modelBuilder);
+ }
+
+ partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
+}
diff --git a/Infrastructure/Services/EventManagerService.cs b/Infrastructure/Services/EventManagerService.cs
new file mode 100644
index 0000000..3a699a2
--- /dev/null
+++ b/Infrastructure/Services/EventManagerService.cs
@@ -0,0 +1,160 @@
+using System.Security.Cryptography;
+using Application.Interfaces;
+using Domain.Config;
+using Domain.Models;
+using Infrastructure.Exceptions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Infrastructure.Services;
+
+public class EventManagerService : IEventManagerService
+{
+ #region Constants
+
+ private const string WWWROOT = "wwwroot";
+
+ private const string EVENT_FOLDER = "events";
+
+ private static readonly DateTimeOffset NOT_BEFORE = new(new DateTime(2013, 1, 1));
+
+ private static readonly DateTimeOffset NOT_AFTER = NOT_BEFORE + new TimeSpan(360 * 20, 0, 0, 0);
+
+ private static readonly string NOT_BEFORE_STRING = NOT_BEFORE.ToUnixTimeSeconds().ToString();
+
+ private static readonly string NOT_AFTER_STRING = NOT_AFTER.ToUnixTimeSeconds().ToString();
+
+ #endregion
+
+ private readonly ILogger logger;
+
+ private readonly EventConfig config;
+
+ private readonly List events;
+
+ private readonly bool useEvents;
+
+ public EventManagerService(IOptions config, ILogger logger)
+ {
+ this.logger = logger;
+ this.config = config.Value;
+ events = new List();
+ useEvents = this.config.UseEvents;
+ }
+
+ public bool UseEvents()
+ {
+ return useEvents;
+ }
+
+ public IEnumerable GetEvents()
+ {
+ return events;
+ }
+
+ public void InitializeEvents()
+ {
+ foreach (var eventData in config.EventFiles)
+ {
+ var filePath = Path.Combine(WWWROOT, EVENT_FOLDER, eventData.FileName);
+ if (!File.Exists(filePath))
+ {
+ logger.LogError("Event file {File} not found at path {Path}!", eventData.FileName,
+ filePath);
+ throw new EventFileNotFoundException();
+ }
+
+ var md5 = ComputeFileMd5(filePath);
+ var @event = new Event
+ {
+ Name = eventData.FileName,
+ Md5 = md5,
+ NotBefore = NOT_BEFORE_STRING,
+ NotAfter = NOT_AFTER_STRING
+ };
+
+ var eventType = DetermineFileType(eventData.FileName);
+ @event.Index = eventType switch
+ {
+ EventFileType.Event => 0,
+ EventFileType.EventRegPic => 1,
+ EventFileType.EventSgRegPic => 2,
+ EventFileType.NewsBigPic => eventData.Index,
+ EventFileType.NewsSmallPic => 1,
+ EventFileType.Telop => 0,
+ EventFileType.EventCmp => 8,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ events.Add(@event);
+ }
+
+ if (events.Exists(event1 => event1.Name.StartsWith("news_big_") && event1.Index == 0))
+ {
+ return;
+ }
+
+ logger.LogWarning("No big news image with index 0! Changing a random one...");
+ events.First(event1 => event1.Name.StartsWith("news_big_")).Index = 0;
+ }
+
+ private EventFileType DetermineFileType(string fileName)
+ {
+ if (fileName.EndsWith(".evt"))
+ {
+ return EventFileType.Event;
+ }
+
+ if (fileName.EndsWith(".cmp"))
+ {
+ return EventFileType.EventCmp;
+ }
+
+ if (fileName.EndsWith(".txt"))
+ {
+ return EventFileType.Telop;
+ }
+
+ if (fileName.Contains("_reg"))
+ {
+ return EventFileType.EventRegPic;
+ }
+
+ if (fileName.Contains("_sgreg"))
+ {
+ return EventFileType.EventSgRegPic;
+ }
+
+ if (fileName.StartsWith("news_big_"))
+ {
+ return EventFileType.NewsBigPic;
+ }
+
+ if (fileName.StartsWith("news_small_"))
+ {
+ return EventFileType.NewsSmallPic;
+ }
+
+ logger.LogError("Unknown event file type for file {File}", fileName);
+ throw new EventFileTypeUnknownException();
+ }
+
+ private static string ComputeFileMd5(string filePath)
+ {
+ using var file = File.OpenRead(filePath);
+ var hash = MD5.HashData(file);
+ var result = BitConverter.ToString(hash).Replace("-", "").ToLower();
+
+ return result;
+ }
+
+ private enum EventFileType
+ {
+ Event,
+ EventRegPic,
+ EventSgRegPic,
+ NewsBigPic,
+ NewsSmallPic,
+ Telop,
+ EventCmp
+ }
+}
\ No newline at end of file
diff --git a/MainServer/.gitignore b/MainServer/.gitignore
new file mode 100644
index 0000000..de92be8
--- /dev/null
+++ b/MainServer/.gitignore
@@ -0,0 +1,442 @@
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### VisualStudio template
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# Ignore card db since we should start from scratch
+/db/card.db3
+Certificates
+wwwroot/events/
+Database/*
diff --git a/MainServer/Configurations/database.json b/MainServer/Configurations/database.json
new file mode 100644
index 0000000..3c9ff35
--- /dev/null
+++ b/MainServer/Configurations/database.json
@@ -0,0 +1,4 @@
+{
+ "CardDbName": "card.db3",
+ "MusicDbName": "music471omni.db3"
+}
\ No newline at end of file
diff --git a/MainServer/Configurations/events.json b/MainServer/Configurations/events.json
new file mode 100644
index 0000000..7f5133b
--- /dev/null
+++ b/MainServer/Configurations/events.json
@@ -0,0 +1,39 @@
+{
+ "Events": {
+ "UseEvents": true,
+ "EventFiles": [
+ {
+ "FileName": "event_103_20201125.evt",
+ "Index": 0
+ },
+ {
+ "FileName": "event_20201125_reg.jpg",
+ "Index": 1
+ },
+ {
+ "FileName": "event_20201125_sgreg.png",
+ "Index": 2
+ },
+ {
+ "FileName": "news_big_20201125_0.jpg",
+ "Index": 0
+ },
+ {
+ "FileName": "news_big_20201125_2.jpg",
+ "Index": 2
+ },
+ {
+ "FileName": "news_small_20201125_1.jpg",
+ "Index": 1
+ },
+ {
+ "FileName": "telop_20201125.txt",
+ "Index": 0
+ },
+ {
+ "FileName": "event_unlock_20201125.cmp",
+ "Index": 8
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Configurations/game.json b/MainServer/Configurations/game.json
new file mode 100644
index 0000000..e0602fb
--- /dev/null
+++ b/MainServer/Configurations/game.json
@@ -0,0 +1,15 @@
+{
+ "AvatarCount": 356,
+ "NavigatorCount": 118,
+ "ItemCount": 21,
+ "SkinCount": 21,
+ "SeCount": 26,
+ "TitleCount": 5530,
+ "UnlockableSongIds":
+ [
+ 11, 13, 149, 273, 291, 320, 321, 371, 378, 384, 464, 471, 474, 475, 492,
+ 494, 498, 520, 548, 551, 558, 561, 565, 570, 577, 583, 612, 615, 622,
+ 632, 659, 666, 668, 670, 672, 676, 680, 682, 685, 686, 697, 700, 701,
+ 711, 720, 749, 875, 876, 877
+ ]
+}
\ No newline at end of file
diff --git a/MainServer/Configurations/logging.json b/MainServer/Configurations/logging.json
new file mode 100644
index 0000000..852c2db
--- /dev/null
+++ b/MainServer/Configurations/logging.json
@@ -0,0 +1,27 @@
+{
+ "Serilog": {
+ "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "Filter": [
+ {
+ "Name": "ByExcluding",
+ "Args": {
+ "expression": "@mt = 'An unhandled exception has occurred while executing the request.'"
+ }
+ }
+ ],
+ "WriteTo": [
+ {
+ "Name": "File",
+ "Args": { "path": "./Logs/log-.txt", "rollingInterval": "Day" }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Configurations/matching.json b/MainServer/Configurations/matching.json
new file mode 100644
index 0000000..b9094c0
--- /dev/null
+++ b/MainServer/Configurations/matching.json
@@ -0,0 +1,6 @@
+{
+ "Relay": {
+ "RelayServer": "127.0.0.1",
+ "RelayPort": 3333
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Configurations/server.json b/MainServer/Configurations/server.json
new file mode 100644
index 0000000..dd5ca8c
--- /dev/null
+++ b/MainServer/Configurations/server.json
@@ -0,0 +1,3 @@
+{
+ "ServerIp": "127.0.0.1"
+}
\ No newline at end of file
diff --git a/MainServer/Controllers/API/TestController.cs b/MainServer/Controllers/API/TestController.cs
new file mode 100644
index 0000000..c60d68f
--- /dev/null
+++ b/MainServer/Controllers/API/TestController.cs
@@ -0,0 +1,24 @@
+using Domain.Entities;
+using Infrastructure.Persistence;
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers.API
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class TestController : ControllerBase
+ {
+ private readonly MusicDbContext context;
+
+ public TestController(MusicDbContext context)
+ {
+ this.context = context;
+ }
+
+ [HttpGet]
+ public MusicUnlock GetOne()
+ {
+ return context.MusicUnlocks.First();
+ }
+ }
+}
diff --git a/MainServer/Controllers/BaseController.cs b/MainServer/Controllers/BaseController.cs
new file mode 100644
index 0000000..c05b299
--- /dev/null
+++ b/MainServer/Controllers/BaseController.cs
@@ -0,0 +1,15 @@
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers;
+
+public abstract class BaseController : ControllerBase where T : BaseController
+{
+ private ILogger? logger;
+
+ private ISender? mediator;
+
+ protected ISender Mediator => (mediator ??= HttpContext.RequestServices.GetService()) ?? throw new InvalidOperationException();
+
+ protected ILogger Logger => (logger ??= HttpContext.RequestServices.GetService>()) ?? throw new InvalidOperationException();
+}
\ No newline at end of file
diff --git a/MainServer/Controllers/Game/AliveController.cs b/MainServer/Controllers/Game/AliveController.cs
new file mode 100644
index 0000000..044abca
--- /dev/null
+++ b/MainServer/Controllers/Game/AliveController.cs
@@ -0,0 +1,25 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers.Game;
+
+[ApiController]
+[Route("alive")]
+public class AliveController : ControllerBase
+{
+ [HttpGet("i.php")]
+ public IActionResult AliveCheck()
+ {
+ var remoteIpAddress = Request.HttpContext.Connection.RemoteIpAddress;
+ var serverIpAddress = Request.HttpContext.Connection.LocalIpAddress;
+ var response = $"REMOTE ADDRESS:{remoteIpAddress}\n" +
+ "SERVER NAME:GCLocalServer\n" +
+ $"SERVER ADDR:{serverIpAddress}";
+ return Ok(response);
+ }
+
+ [HttpGet("/{id}/Alive.txt")]
+ public IActionResult GetAliveFile()
+ {
+ return Ok("");
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Controllers/Game/IncomController.cs b/MainServer/Controllers/Game/IncomController.cs
new file mode 100644
index 0000000..b1132ec
--- /dev/null
+++ b/MainServer/Controllers/Game/IncomController.cs
@@ -0,0 +1,22 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers.Game;
+
+[ApiController]
+[Route("service/incom")]
+public class IncomController : ControllerBase
+{
+ private const string INCOM_RESPONSE = "1+1";
+
+ [HttpPost("incom.php")]
+ public IActionResult Incom()
+ {
+ return Ok(INCOM_RESPONSE);
+ }
+
+ [HttpPost("incomALL.php")]
+ public IActionResult IncomAll()
+ {
+ return Ok(INCOM_RESPONSE);
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Controllers/Game/ResponeController.cs b/MainServer/Controllers/Game/ResponeController.cs
new file mode 100644
index 0000000..3b44268
--- /dev/null
+++ b/MainServer/Controllers/Game/ResponeController.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers.Game;
+
+[ApiController]
+[Route("service/respone")]
+public class ResponeController : ControllerBase
+{
+ [HttpPost("respone.php")]
+ public IActionResult Respone()
+ {
+ return Ok("1");
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Controllers/Game/ServerController.cs b/MainServer/Controllers/Game/ServerController.cs
new file mode 100644
index 0000000..bb4bdb5
--- /dev/null
+++ b/MainServer/Controllers/Game/ServerController.cs
@@ -0,0 +1,41 @@
+using Application.Game.Server;
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers.Game;
+
+[ApiController]
+[Route("server")]
+public class ServerController : BaseController
+{
+ [HttpGet("cursel.php")]
+ public ActionResult GetCursel()
+ {
+ return Ok("1\n");
+ }
+
+ [HttpGet("gameinfo.php")]
+ public ActionResult GetGameInfo()
+ {
+ return Ok("0\n" +
+ "3\n" +
+ "301000,test1\n" +
+ "302000,test2\n" +
+ "303000,test3");
+ }
+
+ [HttpGet("certify.php")]
+ public async Task> Certify(string? gid, string? mac,
+ [FromQuery(Name = "r")]string? random, [FromQuery(Name = "md")]string? md5)
+ {
+ var host = Request.Host.Value;
+ var command = new CertifyCommand(gid, mac, random, md5, host);
+ return Ok(await Mediator.Send(command));
+ }
+
+ [HttpGet("data.php")]
+ public async Task> GetData()
+ {
+ var query = new GetDataQuery(Request.Host.Value);
+ return Ok(await Mediator.Send(query));
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Controllers/Game/ServiceOptionController.cs b/MainServer/Controllers/Game/ServiceOptionController.cs
new file mode 100644
index 0000000..25a22fe
--- /dev/null
+++ b/MainServer/Controllers/Game/ServiceOptionController.cs
@@ -0,0 +1,19 @@
+using Application.Game.Option;
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers.Game;
+
+[ApiController]
+[Route("service/option")]
+public class ServiceOptionController : BaseController
+{
+ [HttpGet("PlayInfo.php")]
+ public async Task> GetPlayCount([FromQuery(Name = "card_id")] long cardId)
+ {
+ var query = new PlayCountQuery(cardId);
+ var count = await Mediator.Send(query);
+
+ return Ok("1\n" +
+ $"{count}");
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Controllers/Game/UpdateController.cs b/MainServer/Controllers/Game/UpdateController.cs
new file mode 100644
index 0000000..3d34936
--- /dev/null
+++ b/MainServer/Controllers/Game/UpdateController.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers.Game;
+
+[ApiController]
+[Route("update/cgi")]
+public class UpdateController : ControllerBase
+{
+ // TODO: Check update properly
+ [HttpGet("check.php")]
+ public IActionResult UpdateCheck()
+ {
+ return NotFound();
+ }
+}
\ No newline at end of file
diff --git a/MainServer/Controllers/Game/UploadController.cs b/MainServer/Controllers/Game/UploadController.cs
new file mode 100644
index 0000000..34869f2
--- /dev/null
+++ b/MainServer/Controllers/Game/UploadController.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace MainServer.Controllers.Game;
+[ApiController]
+[Route("service/upload")]
+public class UploadController : ControllerBase
+{
+ private const string UPLOAD_RESPONSE = "1\n" +
+ "OK";
+
+ [HttpPost("upload.php")]
+ public IActionResult Upload()
+ {
+ return Ok(UPLOAD_RESPONSE);
+ }
+}
\ No newline at end of file
diff --git a/MainServer/MainServer.csproj b/MainServer/MainServer.csproj
new file mode 100644
index 0000000..3bab9c5
--- /dev/null
+++ b/MainServer/MainServer.csproj
@@ -0,0 +1,48 @@
+
+
+
+ net7.0
+ enable
+ enable
+ 11
+ 3.0.0-alpha
+ app.manifest
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MainServer/Program.cs b/MainServer/Program.cs
new file mode 100644
index 0000000..bbc7643
--- /dev/null
+++ b/MainServer/Program.cs
@@ -0,0 +1,103 @@
+using System.Reflection;
+using Application;
+using Application.Interfaces;
+using Domain.Config;
+using Infrastructure;
+using Infrastructure.Common;
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Serilog;
+using Serilog.Extensions.Logging;
+using Throw;
+
+Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console()
+ .CreateBootstrapLogger();
+
+var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?
+ .InformationalVersion;
+Log.Information("GCLocalServer version {Version}", version);
+
+Log.Information("Server starting up...");
+
+try
+{
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Add services to the container.
+ const string configurationsDirectory = "Configurations";
+ builder.Configuration
+ .AddJsonFile($"{configurationsDirectory}/database.json", optional: false, reloadOnChange: false)
+ .AddJsonFile($"{configurationsDirectory}/game.json", optional: false, reloadOnChange: false)
+ .AddJsonFile($"{configurationsDirectory}/logging.json", optional: false, reloadOnChange: false)
+ .AddJsonFile($"{configurationsDirectory}/events.json", optional: true, reloadOnChange: false)
+ .AddJsonFile($"{configurationsDirectory}/matching.json", optional: true, reloadOnChange: false)
+ .AddJsonFile($"{configurationsDirectory}/server.json", optional: true, reloadOnChange: false);
+
+ builder.Services.Configure(
+ builder.Configuration.GetSection(EventConfig.EVENT_SECTION));
+ builder.Services.Configure(
+ builder.Configuration.GetSection(RelayConfig.RELAY_SECTION));
+
+ var serverIp = builder.Configuration["ServerIp"] ?? "127.0.0.1";
+ var certificateManager = new CertificateService(serverIp, new SerilogLoggerFactory(Log.Logger).CreateLogger(""));
+ builder.WebHost.ConfigureKestrel(options =>
+ options.ConfigureHttpsDefaults(adapterOptions =>
+ adapterOptions.ServerCertificate = certificateManager.InitializeCertificate()
+ ));
+
+ builder.Host.UseSerilog((context, configuration) =>
+ {
+ configuration.WriteTo.Console().ReadFrom.Configuration(context.Configuration);
+ });
+
+ builder.Services.AddControllers().AddXmlSerializerFormatters();
+ // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddSwaggerGen();
+
+ builder.Services.AddApplication();
+ builder.Services.AddInfrastructure(builder.Configuration);
+
+ var app = builder.Build();
+
+ using (var scope = app.Services.CreateScope())
+ {
+ var db = scope.ServiceProvider.GetRequiredService();
+ db.Database.Migrate();
+ }
+
+ app.UseSerilogRequestLogging(options =>
+ {
+ options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms, " +
+ "request host: {RequestHost}";
+ options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
+ {
+ diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
+ };
+ });
+
+ var eventService = app.Services.GetService();
+ eventService.ThrowIfNull();
+ eventService.InitializeEvents();
+
+ // Configure the HTTP request pipeline.
+ if (app.Environment.IsDevelopment())
+ {
+ app.UseSwagger();
+ app.UseSwaggerUI();
+ }
+
+ app.MapControllers();
+
+ app.Run();
+}
+catch (Exception ex)
+{
+ Log.Fatal(ex, "Unhandled exception");
+}
+finally
+{
+ Log.Information("Shut down complete");
+ Log.CloseAndFlush();
+}
\ No newline at end of file
diff --git a/MainServer/Properties/launchSettings.json b/MainServer/Properties/launchSettings.json
new file mode 100644
index 0000000..a466b8b
--- /dev/null
+++ b/MainServer/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:55391",
+ "sslPort": 44317
+ }
+ },
+ "profiles": {
+ "MainServer": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7114;http://localhost:5107",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/MainServer/app.manifest b/MainServer/app.manifest
new file mode 100644
index 0000000..3226e83
--- /dev/null
+++ b/MainServer/app.manifest
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MainServer/appsettings.json b/MainServer/appsettings.json
new file mode 100644
index 0000000..4faf77a
--- /dev/null
+++ b/MainServer/appsettings.json
@@ -0,0 +1,3 @@
+{
+ "AllowedHosts": "*"
+}