diff --git a/Application/Application.csproj b/Application/Application.csproj index 647d439..a11bca2 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -11,11 +11,14 @@ + + + diff --git a/Application/Common/Behaviours/LoggingBehaviour.cs b/Application/Common/Behaviours/LoggingBehaviour.cs new file mode 100644 index 0000000..89f20fa --- /dev/null +++ b/Application/Common/Behaviours/LoggingBehaviour.cs @@ -0,0 +1,24 @@ +using MediatR.Pipeline; +using Microsoft.Extensions.Logging; + +namespace Application.Common.Behaviours; + +public class LoggingBehaviour : IRequestPreProcessor where TRequest : notnull +{ + private readonly ILogger logger; + + // ReSharper disable once ContextualLoggerProblem + public LoggingBehaviour(ILogger logger) + { + this.logger = logger; + } + + public Task Process(TRequest request, CancellationToken cancellationToken) + { + var requestName = typeof(TRequest).Name; + + logger.LogInformation("Received request: {RequestName}, content: {Request}", requestName, request); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Application/Common/Behaviours/UnhandledExceptionBehaviour.cs b/Application/Common/Behaviours/UnhandledExceptionBehaviour.cs new file mode 100644 index 0000000..b3b6155 --- /dev/null +++ b/Application/Common/Behaviours/UnhandledExceptionBehaviour.cs @@ -0,0 +1,33 @@ +using MediatR; +using Microsoft.Extensions.Logging; + +namespace Application.Common.Behaviours; + +public class UnhandledExceptionBehaviour : IPipelineBehavior + where TRequest : IRequest +{ + private readonly ILogger logger; + + // ReSharper disable once ContextualLoggerProblem + public UnhandledExceptionBehaviour(ILogger logger) + { + this.logger = logger; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + try + { + return await next(); + } + catch (Exception ex) + { + var requestName = typeof(TRequest).Name; + + logger.LogError(ex, "Unhandled Exception for Request {Name} {@Request}", requestName, request); + + throw; + } + } + +} \ No newline at end of file diff --git a/Application/Common/Exceptions/CardExistsException.cs b/Application/Common/Exceptions/CardExistsException.cs new file mode 100644 index 0000000..f41b018 --- /dev/null +++ b/Application/Common/Exceptions/CardExistsException.cs @@ -0,0 +1,8 @@ +namespace Application.Common.Exceptions; + +public class CardExistsException : Exception +{ + public CardExistsException(string? message) : base(message) + { + } +} \ No newline at end of file diff --git a/Application/Common/Extensions/XmlSerializationExtensions.cs b/Application/Common/Extensions/XmlSerializationExtensions.cs new file mode 100644 index 0000000..5e5efc2 --- /dev/null +++ b/Application/Common/Extensions/XmlSerializationExtensions.cs @@ -0,0 +1,17 @@ +using ChoETL; +using Throw; + +namespace Application.Common; + +public static class XmlSerializationExtensions +{ + public static T DeserializeCardData(this string source) where T : class + { + using var reader = new ChoXmlReader(new StringReader(source)).WithXPath("/root/data"); + + var result = reader.Read(); + result.ThrowIfNull(); + + return result; + } +} \ No newline at end of file diff --git a/Application/Common/Models/ServiceError.cs b/Application/Common/Models/ServiceError.cs new file mode 100644 index 0000000..3fb8248 --- /dev/null +++ b/Application/Common/Models/ServiceError.cs @@ -0,0 +1,140 @@ +namespace Application.Common.Models; + +/// +/// All errors contained in ServiceResult objects must return an error of this type +/// Error codes allow the caller to easily identify the received error and take action. +/// Error messages allow the caller to easily show error messages to the end user. +/// +/// Taken from https://github.com/iayti/CleanArchitecture/blob/master/src/Common/CleanArchitecture.Application/Common/Models/ServiceError.cs +/// +[Serializable] +public class ServiceError +{ + /// + /// CTOR + /// + public ServiceError(string message, int code) + { + Message = message; + Code = code; + } + + public ServiceError() + { + } + + /// + /// Human readable error message + /// + public string Message { get; } = string.Empty; + + /// + /// Machine readable error code + /// + public int Code { get; } + + /// + /// Default error for when we receive an exception + /// + public static ServiceError DefaultError => new("An unknown exception occured.", 999); + + /// + /// Default validation error. Use this for invalid parameters in controller actions and service methods. + /// + public static ServiceError ModelStateError(string validationError) + { + return new ServiceError(validationError, 998); + } + + /// + /// Use this for unauthorized responses. + /// + public static ServiceError ForbiddenError => new("You are not authorized to call this action.", 998); + + /// + /// Use this to send a custom error message + /// + public static ServiceError CustomMessage(string errorMessage) + { + return new ServiceError(errorMessage, 997); + } + + public static ServiceError UserNotFound => new("User with this id does not exist", 996); + + public static ServiceError UserFailedToCreate => new("Failed to create User.", 995); + + public static ServiceError Canceled => new("The request canceled successfully!", 994); + + public static ServiceError NotFound => new("The specified resource was not found.", 990); + + public static ServiceError ValidationFormat => new("Request object format is not true.", 901); + + public static ServiceError Validation => new("One or more validation errors occurred.", 900); + + public static ServiceError SearchAtLeastOneCharacter => + new("Search parameter must have at least one character!", 898); + + /// + /// Default error for when we receive an exception + /// + public static ServiceError ServiceProviderNotFound => + new("Service Provider with this name does not exist.", 700); + + public static ServiceError ServiceProvider => new("Service Provider failed to return as expected.", 600); + + public static ServiceError DateTimeFormatError => + new("Date format is not true. Date format must be like yyyy-MM-dd (2019-07-19)", 500); + + #region Override Equals Operator + + /// + /// Use this to compare if two errors are equal + /// Ref: https://msdn.microsoft.com/ru-ru/library/ms173147(v=vs.80).aspx + /// + public override bool Equals(object? obj) + { + // If parameter cannot be cast to ServiceError or is null return false. + var error = obj as ServiceError; + + // Return true if the error codes match. False if the object we're comparing to is null + // or if it has a different code. + return Code == error?.Code; + } + + public bool Equals(ServiceError error) + { + // Return true if the error codes match. False if the object we're comparing to is null + // or if it has a different code. + return Code == error?.Code; + } + + public override int GetHashCode() + { + return Code; + } + + public static bool operator ==(ServiceError? a, ServiceError? b) + { + // If both are null, or both are same instance, return true. + if (ReferenceEquals(a, b)) + { + return true; + } + + // If one is null, but not both, return false. + if (a is null || b is null) + { + return false; + } + + // Return true if the fields match: + return a.Equals(b); + } + + public static bool operator !=(ServiceError a, ServiceError b) + { + return !(a == b); + } + + #endregion +} \ No newline at end of file diff --git a/Application/Common/Models/ServiceResult.cs b/Application/Common/Models/ServiceResult.cs new file mode 100644 index 0000000..849f197 --- /dev/null +++ b/Application/Common/Models/ServiceResult.cs @@ -0,0 +1,65 @@ +namespace Application.Common.Models; + +/// +/// A standard response for service calls. +/// +/// Return data type +public class ServiceResult : ServiceResult +{ + public T? Data { get; set; } + + public ServiceResult(T? data) + { + Data = data; + } + + public ServiceResult(T? data, ServiceError error) : base(error) + { + Data = data; + } + + public ServiceResult(ServiceError error) : base(error) + { + + } +} + +public class ServiceResult +{ + public bool Succeeded => Error == null; + + public ServiceError? Error { get; set; } + + public ServiceResult(ServiceError? error) + { + error ??= ServiceError.DefaultError; + + Error = error; + } + + public ServiceResult() { } + + #region Helper Methods + + public static ServiceResult Failed(ServiceError error) + { + return new ServiceResult(error); + } + + public static ServiceResult Failed(ServiceError error) + { + return new ServiceResult(error); + } + + public static ServiceResult Failed(T data, ServiceError error) + { + return new ServiceResult(data, error); + } + + public static ServiceResult Success(T data) + { + return new ServiceResult(data); + } + + #endregion +} \ No newline at end of file diff --git a/Application/DependencyInjection.cs b/Application/DependencyInjection.cs index 6ccba01..6e630d0 100644 --- a/Application/DependencyInjection.cs +++ b/Application/DependencyInjection.cs @@ -1,4 +1,7 @@ using System.Reflection; +using Application.Common.Behaviours; +using Application.Game.Card; +using Application.Interfaces; using MediatR; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +13,8 @@ public static class DependencyInjection { services.AddMediatR(Assembly.GetExecutingAssembly()); + services.AddScoped(); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); return services; } } \ No newline at end of file diff --git a/Application/Dto/CardDto.cs b/Application/Dto/CardDto.cs index 74f1d57..26b1516 100644 --- a/Application/Dto/CardDto.cs +++ b/Application/Dto/CardDto.cs @@ -1,4 +1,5 @@ -using System.Xml.Serialization; +using System.ComponentModel; +using System.Xml.Serialization; namespace Application.Dto; @@ -8,6 +9,7 @@ public class CardDto public long CardId { get; set; } [XmlElement(ElementName = "player_name")] + [DefaultValue("")] public string PlayerName { get; set; } = string.Empty; [XmlElement("score_i1")] @@ -23,11 +25,14 @@ public class CardDto public long Fcol3 { get; set; } [XmlElement("achieve_status")] + [DefaultValue("")] public string AchieveStatus { get; set; } = string.Empty; [XmlElement("created")] + [DefaultValue("")] public string Created { get; set; } = string.Empty; [XmlElement("modified")] + [DefaultValue("")] public string Modified { get; set; } = string.Empty; } \ No newline at end of file diff --git a/Application/Game/Card/CardDependencyAggregate.cs b/Application/Game/Card/CardDependencyAggregate.cs new file mode 100644 index 0000000..1f9d580 --- /dev/null +++ b/Application/Game/Card/CardDependencyAggregate.cs @@ -0,0 +1,15 @@ +using Application.Interfaces; + +namespace Application.Game.Card; + +public class CardDependencyAggregate : ICardDependencyAggregate +{ + public CardDependencyAggregate(ICardDbContext cardDbContext, IMusicDbContext musicDbContext) + { + CardDbContext = cardDbContext; + MusicDbContext = musicDbContext; + } + + public ICardDbContext CardDbContext { get; } + public IMusicDbContext MusicDbContext { get; } +} \ No newline at end of file diff --git a/Application/Game/Card/CardRegisterCommand.cs b/Application/Game/Card/CardRegisterCommand.cs new file mode 100644 index 0000000..1d94fc5 --- /dev/null +++ b/Application/Game/Card/CardRegisterCommand.cs @@ -0,0 +1,36 @@ +using Application.Common; +using Application.Common.Exceptions; +using Application.Common.Models; +using Application.Dto; +using Application.Interfaces; +using Application.Mappers; +using MediatR; +using Microsoft.Extensions.Logging; +using Throw; + +namespace Application.Game.Card; + +public record CardRegisterCommand(long CardId, string Data) : IRequestWrapper; + +public class CardRegisterCommandHandler : CardRequestHandlerBase +{ + public CardRegisterCommandHandler(ICardDependencyAggregate aggregate) : base(aggregate) + { + } + + public override async Task> Handle(CardRegisterCommand request, CancellationToken cancellationToken) + { + var exists = CardDbContext.CardMains.Any(card => card.CardId == request.CardId); + if (!exists) + { + return ServiceResult.Failed(ServiceError.CustomMessage($"Card {request.CardId} already exists!")); + } + + var card = request.Data.DeserializeCardData().CardDtoToCardMain(); + card.CardId = request.CardId; + CardDbContext.CardMains.Add(card); + await CardDbContext.SaveChangesAsync(cancellationToken); + + return new ServiceResult(request.Data); + } +} \ No newline at end of file diff --git a/Application/Game/Card/CardRequest.cs b/Application/Game/Card/CardRequest.cs new file mode 100644 index 0000000..f95ee8d --- /dev/null +++ b/Application/Game/Card/CardRequest.cs @@ -0,0 +1,23 @@ +using Domain; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; + +namespace Application.Game.Card; + +public class CardRequest +{ + [ModelBinder(Name = "mac_addr")] + public string Mac { get; set; } = string.Empty; + + [ModelBinder(Name = "cmd_str")] + public int CardCommandType { get; set; } + + [ModelBinder(Name = "type")] + public int CardRequestType { get; set; } + + [ModelBinder(Name = "card_no")] + public long CardId { get; set; } + + [ModelBinder(Name = "data")] + public string Data { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Application/Game/Card/CardRequestHandlerBase.cs b/Application/Game/Card/CardRequestHandlerBase.cs new file mode 100644 index 0000000..5e0d925 --- /dev/null +++ b/Application/Game/Card/CardRequestHandlerBase.cs @@ -0,0 +1,21 @@ +using Application.Common.Models; +using Application.Interfaces; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace Application.Game.Card; + +public abstract class CardRequestHandlerBase: IRequestHandlerWrapper + where TIn : IRequestWrapper +{ + public ICardDbContext CardDbContext { get; } + public IMusicDbContext MusicDbContext { get; } + + public CardRequestHandlerBase(ICardDependencyAggregate aggregate) + { + CardDbContext = aggregate.CardDbContext; + MusicDbContext = aggregate.MusicDbContext; + } + + public abstract Task> Handle(TIn request, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Application/Interfaces/ICardDependencyAggregate.cs b/Application/Interfaces/ICardDependencyAggregate.cs new file mode 100644 index 0000000..6944404 --- /dev/null +++ b/Application/Interfaces/ICardDependencyAggregate.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Logging; + +namespace Application.Interfaces; + +public interface ICardDependencyAggregate +{ + ICardDbContext CardDbContext { get; } + IMusicDbContext MusicDbContext { get; } +} \ No newline at end of file diff --git a/Application/Interfaces/IRequestWrapper.cs b/Application/Interfaces/IRequestWrapper.cs new file mode 100644 index 0000000..79c50a3 --- /dev/null +++ b/Application/Interfaces/IRequestWrapper.cs @@ -0,0 +1,14 @@ +using Application.Common.Models; +using MediatR; + +namespace Application.Interfaces; + +public interface IRequestWrapper : IRequest> +{ + +} + +public interface IRequestHandlerWrapper : IRequestHandler> where TIn : IRequestWrapper +{ + +} \ No newline at end of file diff --git a/Application/Mappers/CardMapper.cs b/Application/Mappers/CardMapper.cs index ad86f71..8779e56 100644 --- a/Application/Mappers/CardMapper.cs +++ b/Application/Mappers/CardMapper.cs @@ -8,4 +8,6 @@ namespace Application.Mappers; public static partial class CardMapper { public static partial CardDto CardMainToCardDto(this CardMain cardMain); + + public static partial CardMain CardDtoToCardMain(this CardDto cardDto); } \ No newline at end of file diff --git a/Domain/Config/GameConfig.cs b/Domain/Config/GameConfig.cs new file mode 100644 index 0000000..c121938 --- /dev/null +++ b/Domain/Config/GameConfig.cs @@ -0,0 +1,20 @@ +namespace Domain.Config; + +public class GameConfig +{ + public const string GAME_SECTION = "Game"; + + public int AvatarCount { get; set; } + + public int NavigatorCount { get; set; } + + public int ItemCount { get; set; } + + public int SkinCount { get; set; } + + public int SeCount { get; set; } + + public int TitleCount { get; set; } + + public List UnlockableSongIds { get; set; } = new(); +} \ No newline at end of file diff --git a/Domain/Enums/CardCommandType.cs b/Domain/Enums/CardCommandType.cs new file mode 100644 index 0000000..2bb09ff --- /dev/null +++ b/Domain/Enums/CardCommandType.cs @@ -0,0 +1,9 @@ +namespace Domain; + +public enum CardCommandType +{ + CardReadRequest = 256, + CardWriteRequest = 768, + RegisterRequest = 512, + ReissueRequest = 1536 +} \ No newline at end of file diff --git a/Domain/Enums/CardRequestType.cs b/Domain/Enums/CardRequestType.cs new file mode 100644 index 0000000..c593f45 --- /dev/null +++ b/Domain/Enums/CardRequestType.cs @@ -0,0 +1,55 @@ +namespace Domain; + +public enum CardRequestType +{ + #region Read + ReadCard = 259, + ReadCardDetail = 260, + ReadCardDetails = 261, + ReadCardBData = 264, + ReadAvatar = 418, + ReadItem = 420, + ReadSkin = 422, + ReadTitle = 424, + ReadMusic = 428, + ReadEventReward = 441, + ReadNavigator = 443, + ReadMusicExtra = 465, + ReadMusicAou = 467, + ReadCoin = 468, + ReadUnlockReward = 507, + ReadUnlockKeynum = 509, + ReadSoundEffect = 8458, + ReadGetMessage = 8461, + ReadCond = 8465, + ReadTotalTrophy = 8468, + #endregion + + #region Session + GetSession = 401, + StartSession = 402, + #endregion + + + #region Write + WriteCard = 771, + WriteCardDetail = 772, + WriteCardBData = 776, + WriteAvatar = 929, + WriteItem = 931, + WriteTitle = 935, + WriteMusicDetail = 941, + WriteNavigator = 954, + WriteCoin = 980, + WriteSkin = 933, + WriteUnlockKeynum = 1020, + WriteSoundEffect = 8969, + #endregion + + + #region Online Matching + StartOnlineMatching = 8705, + UpdateOnlineMatching = 8961, + UploadOnlineMatchingResult = 8709, + #endregion +} \ No newline at end of file diff --git a/Domain/Enums/CardReturnCode.cs b/Domain/Enums/CardReturnCode.cs new file mode 100644 index 0000000..341e4f8 --- /dev/null +++ b/Domain/Enums/CardReturnCode.cs @@ -0,0 +1,27 @@ +namespace Domain; + +public enum CardReturnCode +{ + /// + /// Normal + /// 処理は正常に完了しました in debug string + /// + Ok = 1, + + /// + /// New card + /// 未登録のカードです in debug string + /// + CardNotRegistered = 23, + + /// + /// Not reissue, to determine whether it is a new card or reissued card + /// 再発行予約がありません in debug string + /// + NotReissue = 27, + + /// + /// Server side validation error + /// + Unknown = 999 +} \ No newline at end of file diff --git a/Infrastructure/Migrations/20230208132952_Initial.cs b/Infrastructure/Migrations/20230208132952_Initial.cs index 9ea1b7d..1af7824 100644 --- a/Infrastructure/Migrations/20230208132952_Initial.cs +++ b/Infrastructure/Migrations/20230208132952_Initial.cs @@ -10,77 +10,89 @@ namespace Infrastructure.Migrations /// 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); - }); + if (!MigrationHelper.Exists("card_bdata")) + { + 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 }); - }); + if (!MigrationHelper.Exists("card_detail")) + { + 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); - }); + if (!MigrationHelper.Exists("card_main")) + { + 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); - }); + if (!MigrationHelper.Exists("CardPlayCount")) + { + 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); + }); + } } /// diff --git a/Infrastructure/Migrations/MigrationHelper.cs b/Infrastructure/Migrations/MigrationHelper.cs index 9edc2c0..b51cd32 100644 --- a/Infrastructure/Migrations/MigrationHelper.cs +++ b/Infrastructure/Migrations/MigrationHelper.cs @@ -20,14 +20,14 @@ public static class MigrationHelper return reader.Read()? (long)reader[0] == 1 : false; } - - public static string GetConnectionString() + + private static string GetConnectionString() { var builder = new ConfigurationBuilder() .SetBasePath(PathHelper.ConfigurationPath) .AddJsonFile("database.json", optional: false, reloadOnChange: false); - var cardDbName = builder.Build()["CardDbName"]; + var cardDbName = builder.Build()["CardDbName"] ?? "card.db3"; var cardDbPath = Path.Combine(PathHelper.DatabasePath, cardDbName); return cardDbPath; } diff --git a/MainServer/Configurations/game.json b/MainServer/Configurations/game.json index e0602fb..1c7a24e 100644 --- a/MainServer/Configurations/game.json +++ b/MainServer/Configurations/game.json @@ -1,15 +1,17 @@ { - "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 - ] + "Game": { + "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/Controllers/API/TestController.cs b/MainServer/Controllers/API/TestController.cs index c60d68f..d487e61 100644 --- a/MainServer/Controllers/API/TestController.cs +++ b/MainServer/Controllers/API/TestController.cs @@ -20,5 +20,20 @@ namespace MainServer.Controllers.API { return context.MusicUnlocks.First(); } + + [HttpPost] + public ActionResult TestXmlInputOutput([FromForm(Name = "my_model")]TestModel model, + [FromForm(Name = "my_type")]int type) + { + return Ok($"{model.Name}\n{model.Age}\n{type}"); + } } + + public class TestModel + { + public string Name { get; set; } = string.Empty; + + public int Age { get; set; } + } + } diff --git a/MainServer/Controllers/Game/CardController.cs b/MainServer/Controllers/Game/CardController.cs new file mode 100644 index 0000000..85eb907 --- /dev/null +++ b/MainServer/Controllers/Game/CardController.cs @@ -0,0 +1,134 @@ +using System.Net; +using Application.Common.Models; +using Application.Game.Card; +using Domain; +using Microsoft.AspNetCore.Mvc; +using Throw; + +namespace MainServer.Controllers.Game; + +[ApiController] +[Route("service/card")] +public class CardController : BaseController +{ + [HttpPost("cardn.cgi")] + public async Task> CardService([FromForm]CardRequest request) + { + var cardRequestType = (CardRequestType)request.CardRequestType; + var cardCommandType = (CardCommandType)request.CardCommandType; + + cardCommandType.Throw().IfOutOfRange(); + if (cardCommandType is CardCommandType.CardReadRequest or CardCommandType.CardWriteRequest) + { + cardRequestType.Throw().IfOutOfRange(); + } + + request.Data = WebUtility.UrlDecode(request.Data); + var result = ServiceResult.Failed(ServiceError.DefaultError); + switch (cardCommandType) + { + case CardCommandType.CardReadRequest: + { + switch (cardRequestType) + { + case CardRequestType.ReadCard: + break; + case CardRequestType.ReadCardDetail: + break; + case CardRequestType.ReadCardDetails: + break; + case CardRequestType.ReadCardBData: + break; + case CardRequestType.ReadAvatar: + break; + case CardRequestType.ReadItem: + break; + case CardRequestType.ReadSkin: + break; + case CardRequestType.ReadTitle: + break; + case CardRequestType.ReadMusic: + break; + case CardRequestType.ReadEventReward: + break; + case CardRequestType.ReadNavigator: + break; + case CardRequestType.ReadMusicExtra: + break; + case CardRequestType.ReadMusicAou: + break; + case CardRequestType.ReadCoin: + break; + case CardRequestType.ReadUnlockReward: + break; + case CardRequestType.ReadUnlockKeynum: + break; + case CardRequestType.ReadSoundEffect: + break; + case CardRequestType.ReadGetMessage: + break; + case CardRequestType.ReadCond: + break; + case CardRequestType.ReadTotalTrophy: + break; + case CardRequestType.GetSession: + break; + case CardRequestType.StartSession: + break; + case CardRequestType.WriteCard: + + break; + case CardRequestType.WriteCardDetail: + break; + case CardRequestType.WriteCardBData: + break; + case CardRequestType.WriteAvatar: + break; + case CardRequestType.WriteItem: + break; + case CardRequestType.WriteTitle: + break; + case CardRequestType.WriteMusicDetail: + break; + case CardRequestType.WriteNavigator: + break; + case CardRequestType.WriteCoin: + break; + case CardRequestType.WriteSkin: + break; + case CardRequestType.WriteUnlockKeynum: + break; + case CardRequestType.WriteSoundEffect: + break; + case CardRequestType.StartOnlineMatching: + break; + case CardRequestType.UpdateOnlineMatching: + break; + case CardRequestType.UploadOnlineMatchingResult: + break; + default: + throw new ArgumentOutOfRangeException(message: "Should not happen", paramName:null); + } + break; + } + case CardCommandType.CardWriteRequest: + break; + case CardCommandType.RegisterRequest: + result = await Mediator.Send(new CardRegisterCommand(request.CardId, request.Data)); + break; + case CardCommandType.ReissueRequest: + break; + default: + throw new ArgumentOutOfRangeException(message: "Should not happen", paramName:null); + } + + if (result.Succeeded) + { + return Ok(result.Data); + } + + var errorMessage = $"{(int)CardReturnCode.Unknown}\n" + + $"{result.Error!.Message}"; + return Ok(errorMessage); + } +} \ No newline at end of file diff --git a/MainServer/Filters/ApiExceptionFilterAttributes.cs b/MainServer/Filters/ApiExceptionFilterAttributes.cs new file mode 100644 index 0000000..9dfe2bf --- /dev/null +++ b/MainServer/Filters/ApiExceptionFilterAttributes.cs @@ -0,0 +1,81 @@ +using System.Diagnostics; +using Application.Common.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace MainServer.Filters; + +/// +/// Exception filters +/// +public class ApiExceptionFilterService : ExceptionFilterAttribute +{ + private readonly IDictionary> exceptionHandlers; + + private readonly ILogger logger; + + /// + /// Constructor + /// + public ApiExceptionFilterService(ILogger logger) + { + this.logger = logger; + // Register known exception types and handlers. + exceptionHandlers = new Dictionary> + { + { typeof(ArgumentOutOfRangeException), HandleArgumentOutOfRangeException } + }; + } + + /// + /// On exception event + /// + /// + public override void OnException(ExceptionContext context) + { + HandleException(context); + + base.OnException(context); + } + + private void HandleException(ExceptionContext context) + { + var type = context.Exception.GetType(); + if (exceptionHandlers.ContainsKey(type)) + { + exceptionHandlers[type].Invoke(context); + return; + } + + HandleUnknownException(context); + } + + private static void HandleUnknownException(ExceptionContext context) + { + var details = ServiceResult.Failed(ServiceError.DefaultError); + + context.Result = new ObjectResult(details) + { + StatusCode = StatusCodes.Status500InternalServerError + }; + + context.ExceptionHandled = true; + } + + private void HandleArgumentOutOfRangeException(ExceptionContext context) + { + logger.LogError(context.Exception, ""); + var exception = context.Exception as ArgumentOutOfRangeException; + Debug.Assert(exception != null, nameof(exception) + " != null"); + + var variable = exception.ParamName ?? "Unknown"; + var details = ServiceResult.Failed(ServiceError.CustomMessage($"Argument {variable} out of bounds!")); + + context.Result = new ObjectResult(details) + { + StatusCode = StatusCodes.Status400BadRequest + }; + + context.ExceptionHandled = true; + } +} \ No newline at end of file diff --git a/MainServer/MainServer.csproj b/MainServer/MainServer.csproj index 3bab9c5..b0f561c 100644 --- a/MainServer/MainServer.csproj +++ b/MainServer/MainServer.csproj @@ -43,6 +43,7 @@ + diff --git a/MainServer/Program.cs b/MainServer/Program.cs index bbc7643..d493464 100644 --- a/MainServer/Program.cs +++ b/MainServer/Program.cs @@ -5,6 +5,7 @@ using Domain.Config; using Infrastructure; using Infrastructure.Common; using Infrastructure.Persistence; +using MainServer.Filters; using Microsoft.EntityFrameworkCore; using Serilog; using Serilog.Extensions.Logging; @@ -38,6 +39,8 @@ try builder.Configuration.GetSection(EventConfig.EVENT_SECTION)); builder.Services.Configure( builder.Configuration.GetSection(RelayConfig.RELAY_SECTION)); + builder.Services.Configure( + builder.Configuration.GetSection(GameConfig.GAME_SECTION)); var serverIp = builder.Configuration["ServerIp"] ?? "127.0.0.1"; var certificateManager = new CertificateService(serverIp, new SerilogLoggerFactory(Log.Logger).CreateLogger("")); @@ -51,7 +54,9 @@ try configuration.WriteTo.Console().ReadFrom.Configuration(context.Configuration); }); - builder.Services.AddControllers().AddXmlSerializerFormatters(); + builder.Services.AddControllers(options => + options.Filters.Add()); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -80,21 +85,23 @@ try var eventService = app.Services.GetService(); eventService.ThrowIfNull(); eventService.InitializeEvents(); - - // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } + // app.UseExceptionHandler(); + app.UseStaticFiles(); + app.MapControllers(); app.Run(); } catch (Exception ex) { - Log.Fatal(ex, "Unhandled exception"); + Log.Fatal(ex, "Unhandled exception in startup"); } finally {