├── .gitattributes ├── .github ├── funding.yml └── workflows │ └── run-unit-tests.yml ├── KONGOR.MasterServer ├── appsettings.json ├── Constants │ └── RateLimiterPolicies.cs ├── appsettings.Development.json ├── appsettings.Production.json ├── Controllers │ ├── Ascension │ │ └── AscensionController.cs │ ├── StorageStatusController │ │ └── StorageStatusController.cs │ ├── ClientRequesterController │ │ ├── ClientRequesterControllerServerList.cs │ │ ├── ClientRequesterControllerGuides.cs │ │ └── ClientRequesterControllerStats.cs │ ├── ServerRequesterController │ │ └── ServerRequesterController.cs │ ├── StatsRequesterController │ │ └── StatsRequesterController.cs │ └── PatcherController │ │ └── PatcherController.cs ├── Extensions │ ├── Collections │ │ ├── GameDataExtensions.cs │ │ └── EnumerableExtensions.cs │ └── Cache │ │ └── DistributedCacheExtensions.Friends.cs ├── Internals │ ├── WebApplicationMarker.cs │ └── UsingDirectives.cs ├── Models │ ├── Configuration │ │ └── OperationalConfiguration.cs │ ├── RequestResponse │ │ ├── Patch │ │ │ ├── PatchDetails.cs │ │ │ ├── LatestPatchRequestForm.cs │ │ │ └── LatestPatchResponse.cs │ │ ├── GameData │ │ │ ├── GuideResponseError.cs │ │ │ ├── GuideListResponse.cs │ │ │ └── GuideResponseSuccess.cs │ │ ├── SRP │ │ │ ├── SRPAuthenticationResponseStageOne.cs │ │ │ ├── SRPAuthenticationSessionDataStageTwo.cs │ │ │ └── SRPAuthenticationFailureResponse.cs │ │ └── Store │ │ │ └── StoreItemDiscountCoupon.cs │ └── ServerManagement │ │ ├── MatchServerManager.cs │ │ └── MatchServer.cs ├── KONGOR.MasterServer.csproj ├── Properties │ └── launchSettings.json └── Handlers │ └── Patch │ └── PatchHandlers.cs ├── ZORGATH.WebPortal.API ├── appsettings.json ├── Contracts │ ├── EmailAddressControllerDTOs.cs │ └── UserControllerDTOs.cs ├── Constants │ ├── RateLimiterPolicies.cs │ └── OutputCachePolicies.cs ├── Services │ └── Email │ │ ├── IEmailService.cs │ │ └── EmailService.cs ├── Internals │ ├── WebApplicationMarker.cs │ └── UsingDirectives.cs ├── appsettings.Development.json ├── appsettings.Production.json ├── Models │ └── Configuration │ │ └── OperationalConfiguration.cs ├── ZORGATH.WebPortal.API.csproj ├── Properties │ └── launchSettings.json ├── Validators │ └── PasswordValidator.cs ├── Helpers │ └── EmailAddressHelpers.cs ├── Handlers │ └── SRPRegistrationHandlers.cs ├── Extensions │ └── UserClaimsExtensions.cs └── Controllers │ └── EmailAddressController.cs ├── MERRICK.DatabaseContext ├── appsettings.json ├── appsettings.Development.json ├── appsettings.Production.json ├── Enumerations │ ├── ClanTier.cs │ └── AccountType.cs ├── Constants │ ├── UserRoles.cs │ ├── UserRoleClaims.cs │ └── Claims.cs ├── Entities │ ├── Utility │ │ ├── Role.cs │ │ └── Token.cs │ ├── Relational │ │ ├── IgnoredPeer.cs │ │ ├── BannedPeer.cs │ │ └── FriendedPeer.cs │ ├── Core │ │ ├── Clan.cs │ │ ├── User.cs │ │ └── Account.cs │ ├── Game │ │ └── HeroGuide.cs │ └── Statistics │ │ └── MatchStatistics.cs ├── Data │ ├── DataFiles.cs │ └── DeserializationDTOs.cs ├── Extensions │ ├── GameDataExtensions.cs │ └── EnumerableExtensions.cs ├── Helpers │ └── InMemoryHelpers.cs ├── MERRICK.DatabaseContext.csproj ├── Services │ ├── DatabaseHealthCheck.cs │ └── DatabaseInitializer.cs ├── Properties │ └── launchSettings.json ├── Internals │ └── UsingDirectives.cs ├── Persistence │ └── MerrickContext.cs └── MERRICK.cs ├── TRANSMUTANSTEIN.ChatServer ├── appsettings.json ├── appsettings.Development.json ├── appsettings.Production.json ├── Contracts │ ├── IAsynchronousCommandProcessor.cs │ └── ISynchronousCommandProcessor.cs ├── Attributes │ └── ChatCommandAttribute.cs ├── Internals │ ├── Context.cs │ └── UsingDirectives.cs ├── Domain │ ├── Core │ │ ├── ChatServer.cs │ │ └── ChatSessionMetadata.cs │ ├── Matchmaking │ │ ├── MatchmakingGroupMember.cs │ │ └── MatchmakingGroupInformation.cs │ └── Communication │ │ └── Whisper.cs ├── CommandProcessors │ ├── Matchmaking │ │ ├── GroupLeave.cs │ │ ├── GroupJoin.cs │ │ ├── GroupInvite.cs │ │ ├── GroupPlayerReadyStatus.cs │ │ ├── GroupPlayerLoadingStatus.cs │ │ ├── GroupRejectInvite.cs │ │ ├── GroupJoinQueue.cs │ │ ├── GroupLeaveQueue.cs │ │ └── GroupCreate.cs │ ├── Channels │ │ ├── LeaveChannel.cs │ │ ├── JoinChannel.cs │ │ ├── KickFromChannel.cs │ │ ├── SetChannelPassword.cs │ │ ├── JoinChannelPassword.cs │ │ ├── SilenceChannelMember.cs │ │ └── SendChannelMessage.cs │ ├── Communication │ │ └── SendWhisper.cs │ ├── Actions │ │ └── TrackPlayerAction.cs │ ├── Social │ │ ├── ApproveFriend.cs │ │ ├── AddFriend.cs │ │ └── RemoveFriend.cs │ ├── Statistics │ │ └── SeasonStats.cs │ └── Connection │ │ ├── ServerHandshake.cs │ │ └── ServerManagerHandshake.cs ├── TRANSMUTANSTEIN.ChatServer.csproj ├── Properties │ └── launchSettings.json ├── Extensions │ └── Protocol │ │ └── AccountExtensions.cs ├── Services │ ├── ChatServerHealthCheck.cs │ ├── ChatService.cs │ └── MatchmakingService.cs └── Utilities │ └── Log.cs ├── ASPIRE.ApplicationHost ├── appsettings.json ├── Internals │ └── UsingDirectives.cs ├── appsettings.Development.json ├── appsettings.Production.json ├── Properties │ └── launchSettings.json └── ASPIRE.ApplicationHost.csproj ├── global.json ├── ASPIRE.Tests ├── Internals │ ├── TypeAliases.cs │ └── UsingDirectives.cs ├── ZORGATH.WebPortal.API │ ├── Models │ │ └── JWTAuthenticationData.cs │ ├── Infrastructure │ │ └── ZORGATHServiceProvider.cs │ └── Tests │ │ └── EmailAddressRegistrationTests.cs ├── Properties │ └── launchSettings.json ├── KONGOR.MasterServer │ ├── Models │ │ └── SRPAuthenticationData.cs │ └── Infrastructure │ │ └── KONGORServiceProvider.cs └── ASPIRE.Tests.csproj ├── .vsconfig ├── .aspire └── settings.json ├── ASPIRE.SourceGenerator ├── AnalyzerReleases.Unshipped.md ├── Internals │ └── UsingDirectives.cs ├── AnalyzerReleases.Shipped.md ├── Attributes │ └── AutoImplementMissingMembersAttribute.cs └── ASPIRE.SourceGenerator.csproj ├── BannedSymbols.txt ├── ASPIRE.Common ├── Constants │ └── TextConstant.cs ├── Internals │ └── UsingDirectives.cs ├── Compatibility │ ├── Client.cs │ └── Server.cs ├── ASPIRE.Common.csproj ├── Communication │ └── ChatChannels.cs └── Extensions │ └── Cryptography │ └── CryptographyExtensions.cs ├── DAWNBRINGER.WebPortal.UI ├── DAWNBRINGER.WebPortal.UI.csproj └── DAWNBRINGER.cs ├── .config └── dotnet-tools.json ├── NEXUS.DistributedApplication.slnx ├── Directory.Build.targets ├── Directory.Build.props └── nuget.config /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: K-O-N-G-O-R 2 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*" 3 | } 4 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*" 3 | } 4 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*" 3 | } 4 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*" 3 | } 4 | -------------------------------------------------------------------------------- /ASPIRE.ApplicationHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Common Configuration Goes Here */ 3 | } 4 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "runner": "Microsoft.Testing.Platform" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ASPIRE.Tests/Internals/TypeAliases.cs: -------------------------------------------------------------------------------- 1 | global using Role = MERRICK.DatabaseContext.Entities.Utility.Role; 2 | -------------------------------------------------------------------------------- /.vsconfig: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "extensions": [ 4 | "https://marketplace.visualstudio.com/items?itemName=SharpDevelopTeam.ILSpy", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Error" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Error" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.aspire/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appHostPath": "../ASPIRE.ApplicationHost/ASPIRE.ApplicationHost.csproj", 3 | "features": { 4 | "execCommandEnabled": "true" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Contracts/EmailAddressControllerDTOs.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Contracts; 2 | 3 | public record RegisterEmailAddressDTO(string EmailAddress, string ConfirmEmailAddress); 4 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Enumerations/ClanTier.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Enumerations; 2 | 3 | public enum ClanTier 4 | { 5 | None, 6 | Member, 7 | Officer, 8 | Leader 9 | } 10 | -------------------------------------------------------------------------------- /ASPIRE.SourceGenerator/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | ; Unshipped Analyzer Release 2 | ; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | -------------------------------------------------------------------------------- /ASPIRE.ApplicationHost/Internals/UsingDirectives.cs: -------------------------------------------------------------------------------- 1 | global using Aspire.Hosting.Redis; 2 | 3 | global using Microsoft.Extensions.Configuration; 4 | global using Microsoft.Extensions.Hosting; 5 | 6 | global using Projects; 7 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Contracts/IAsynchronousCommandProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Contracts; 2 | 3 | public interface IAsynchronousCommandProcessor 4 | { 5 | public Task Process(ChatSession session, ChatBuffer buffer); 6 | } 7 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Contracts/ISynchronousCommandProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Contracts; 2 | 3 | public interface ISynchronousCommandProcessor 4 | { 5 | public void Process(ChatSession session, ChatBuffer buffer); 6 | } 7 | -------------------------------------------------------------------------------- /BannedSymbols.txt: -------------------------------------------------------------------------------- 1 | M:System.Threading.Thread.Sleep(System.Int32); Do not block the thread. Use 'Task.Delay(...)' for non-blocking delays. 2 | M:System.Threading.Thread.Sleep(System.TimeSpan); Do not block the thread. Use 'Task.Delay(...)' for non-blocking delays. 3 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Constants/RateLimiterPolicies.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Constants; 2 | 3 | public static class RateLimiterPolicies 4 | { 5 | public const string Relaxed = "Relaxed Rate Limit"; 6 | public const string Strict = "Strict Rate Limit"; 7 | } 8 | -------------------------------------------------------------------------------- /ASPIRE.ApplicationHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug" 5 | } 6 | }, 7 | 8 | "ChatServer": { 9 | "Host": "localhost", 10 | "Port": 11031 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Constants/RateLimiterPolicies.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Constants; 2 | 3 | public static class RateLimiterPolicies 4 | { 5 | public const string Relaxed = "Relaxed Rate Limit"; 6 | public const string Strict = "Strict Rate Limit"; 7 | } 8 | -------------------------------------------------------------------------------- /ASPIRE.ApplicationHost/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Error" 5 | } 6 | }, 7 | 8 | "ChatServer": { 9 | "Host": "chat.kongor.net", 10 | "Port": 11031 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Attributes/ChatCommandAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Attributes; 2 | 3 | [AttributeUsage(AttributeTargets.Class)] 4 | public class ChatCommandAttribute(ushort command) : Attribute 5 | { 6 | public ushort Command { get; init; } = command; 7 | } 8 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Constants/UserRoles.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Constants; 2 | 3 | public static class UserRoles 4 | { 5 | public const string Administrator = "ADMINISTRATOR"; 6 | public const string User = "USER"; 7 | 8 | public const string AllRoles = "ADMINISTRATOR,USER"; 9 | } 10 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Utility/Role.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Utility; 2 | 3 | [Index(nameof(Name), IsUnique = true)] 4 | public class Role 5 | { 6 | [Key] 7 | public int ID { get; set; } 8 | 9 | [StringLength(20)] 10 | public required string Name { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /ASPIRE.SourceGenerator/Internals/UsingDirectives.cs: -------------------------------------------------------------------------------- 1 | global using ASPIRE.SourceGenerator.Attributes; 2 | 3 | global using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | global using Microsoft.CodeAnalysis.CSharp; 5 | global using Microsoft.CodeAnalysis; 6 | 7 | global using System.Collections.Immutable; 8 | global using System.Text; 9 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Relational/IgnoredPeer.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Relational; 2 | 3 | [Index(nameof(Name), IsUnique = true)] 4 | public class IgnoredPeer 5 | { 6 | public required int ID { get; set; } 7 | 8 | [MaxLength(15)] 9 | public required string Name { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /ASPIRE.Common/Constants/TextConstant.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Common.Constants; 2 | 3 | public static class TextConstant 4 | { 5 | public const string EmptyString = ""; 6 | 7 | public const char NULL = '\0'; 8 | 9 | public const string Whitespace = " "; 10 | 11 | public const char WhitespaceCharacter = ' '; 12 | } 13 | -------------------------------------------------------------------------------- /ASPIRE.Tests/ZORGATH.WebPortal.API/Models/JWTAuthenticationData.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Tests.ZORGATH.WebPortal.API.Models; 2 | 3 | /// 4 | /// Result Of A Complete Authentication Flow 5 | /// 6 | public sealed record JWTAuthenticationData(int UserID, string AccountName, string EmailAddress, string AuthenticationToken); 7 | -------------------------------------------------------------------------------- /DAWNBRINGER.WebPortal.UI/DAWNBRINGER.WebPortal.UI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Internals/Context.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Internals; 2 | 3 | public static class Context 4 | { 5 | public static ConcurrentDictionary ChatSessions { get; set; } = []; 6 | 7 | public static ConcurrentDictionary ChatChannels { get; set; } = []; 8 | } 9 | -------------------------------------------------------------------------------- /DAWNBRINGER.WebPortal.UI/DAWNBRINGER.cs: -------------------------------------------------------------------------------- 1 | namespace DAWNBRINGER.WebPortal.UI; 2 | 3 | public class DAWNBRINGER 4 | { 5 | // Entry Point For The Web Portal UI Application 6 | public static void Main() 7 | { 8 | // TODO: Implement Full Web Portal UI Application 9 | Console.WriteLine("Hello, World!"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Data/DataFiles.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Data; 2 | 3 | public static class DataFiles 4 | { 5 | private static readonly string BasePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; 6 | 7 | public static readonly string HeroGuides = Path.Combine(BasePath, "Data", "HeroGuides.json"); 8 | } 9 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug" 5 | } 6 | }, 7 | 8 | "Operational": { 9 | "CDN": { 10 | "PrimaryPatchURL": "http://localhost:55555/patch", 11 | "SecondaryPatchURL": "http://localhost:55555/patch" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Error" 5 | } 6 | }, 7 | 8 | "Operational": { 9 | "CDN": { 10 | "PrimaryPatchURL": "http://api.kongor.net/patch", 11 | "SecondaryPatchURL": "http://api.kongor.net/patch" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Constants/UserRoleClaims.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Constants; 2 | 3 | public static class UserRoleClaims 4 | { 5 | public static readonly List Administrator = [new Claim(Claims.UserRole, UserRoles.Administrator, ClaimValueTypes.String)]; 6 | public static readonly List User = [new Claim(Claims.UserRole, UserRoles.User, ClaimValueTypes.String)]; 7 | } 8 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Relational/BannedPeer.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Relational; 2 | 3 | [Index(nameof(Name), IsUnique = true)] 4 | public class BannedPeer 5 | { 6 | public required int ID { get; set; } 7 | 8 | [MaxLength(15)] 9 | public required string Name { get; set; } 10 | 11 | [MaxLength(30)] 12 | public required string BanReason { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Constants/OutputCachePolicies.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Constants; 2 | 3 | public static class OutputCachePolicies 4 | { 5 | public const string CacheForThirtySeconds = "Cache For 30 Seconds"; 6 | public const string CacheForFiveMinutes = "Cache For 5 Minutes"; 7 | public const string CacheForOneDay = "Cache For 1 Day"; 8 | public const string CacheForOneWeek = "Cache For 1 Week"; 9 | } 10 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Services/Email/IEmailService.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Services.Email; 2 | 3 | public interface IEmailService 4 | { 5 | public Task SendEmailAddressRegistrationLink(string emailAddress, string token); 6 | 7 | public Task SendEmailAddressRegistrationConfirmation(string emailAddress, string accountName); 8 | 9 | // TODO: Define Email Service (Two Implementations: Real, Console) 10 | } 11 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Extensions/GameDataExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Extensions; 2 | 3 | // TODO: Move To Shared Project To Remove Duplication 4 | 5 | public static class GameDataExtensions 6 | { 7 | public static IList PipeSeparatedStringToList(this string input) 8 | => input.Split('|').ToList(); 9 | 10 | public static string ListToPipeSeparatedString(this IList input) 11 | => string.Join('|', input); 12 | } 13 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Controllers/Ascension/AscensionController.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Controllers.Ascension; 2 | 3 | [ApiController] 4 | [Route(TextConstant.EmptyString)] 5 | public class AscensionController : ControllerBase 6 | { 7 | [HttpGet("/", Name = "Ascension Root")] 8 | [HttpGet("index.php", Name = "Ascension Index")] 9 | public IActionResult GetAscension() 10 | => Ok(@"{ ""error_code"": 100, ""data"": { ""is_season_match"": true } }"); 11 | } 12 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Extensions/Collections/GameDataExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Extensions.Collections; 2 | 3 | // TODO: Move To Shared Project To Remove Duplication 4 | 5 | public static class GameDataExtensions 6 | { 7 | public static IList PipeSeparatedStringToList(this string input) 8 | => input.Split('|').ToList(); 9 | 10 | public static string ListToPipeSeparatedString(this IList input) 11 | => string.Join('|', input); 12 | } 13 | -------------------------------------------------------------------------------- /ASPIRE.SourceGenerator/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ; Shipped Analyzer Releases 2 | ; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ## Release 1.0.0 5 | 6 | ### New Rules 7 | 8 | Rule ID | Category | Severity | Notes 9 | --------|-----------------------|----------|------------------------------------- 10 | NX0001 | NEXUS.SourceGenerator | Error | AutoImplementMissingMembersGenerator 11 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Internals/WebApplicationMarker.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Internals; 2 | 3 | /// 4 | /// Used in the unit/integration tests project as a marker for the KONGOR.MasterServer assembly. 5 | /// It acts as the type parameter for WebApplicationFactory, in order to point it to this compilation unit. 6 | /// Any type in this project would also work, however using this pattern is the preferred practice. 7 | /// 8 | public interface KONGORAssemblyMarker { } 9 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/Configuration/OperationalConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.Configuration; 2 | 3 | public class OperationalConfiguration 4 | { 5 | public const string ConfigurationSection = "Operational"; 6 | public required OperationalConfigurationCDN CDN { get; set; } 7 | } 8 | 9 | public class OperationalConfigurationCDN 10 | { 11 | public required string PrimaryPatchURL { get; set; } 12 | public required string SecondaryPatchURL { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Relational/FriendedPeer.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Relational; 2 | 3 | [Index(nameof(Name), IsUnique = true)] 4 | public class FriendedPeer 5 | { 6 | public required int ID { get; set; } 7 | 8 | [MaxLength(15)] 9 | public required string Name { get; set; } 10 | 11 | [StringLength(4)] 12 | public required string? ClanTag { get; set; } 13 | 14 | [MaxLength(15)] 15 | public required string FriendGroup { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Internals/WebApplicationMarker.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Internals; 2 | 3 | /// 4 | /// Used in the unit/integration tests project as a marker for the ZORGATH.WebPortal.API assembly. 5 | /// It acts as the type parameter for WebApplicationFactory, in order to point it to this compilation unit. 6 | /// Any type in this project would also work, however using this pattern is the preferred practice. 7 | /// 8 | public interface ZORGATHAssemblyMarker { } 9 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Data/DeserializationDTOs.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Data; 2 | 3 | public class DeserializationDTOs 4 | { 5 | public record GuideGetDTO(int GuideID, string Name, string HeroName, string HeroIdentifier, string Intro, string Content, 6 | IList StartingItems, IList EarlyGameItems, IList CoreItems, IList LuxuryItems, 7 | IList AbilityQueue, int AuthorID, float Rating, int UpVotes, int DownVotes, bool Public, bool Featured); 8 | } 9 | -------------------------------------------------------------------------------- /ASPIRE.Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "ASPIRE.Tests": { 5 | "commandName": "Project", 6 | "commandLineArgs": "--log-level information --report-trx", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development", 9 | "DOTNET_ENVIRONMENT": "Development" 10 | }, 11 | "dotnetRunMessages": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/Patch/PatchDetails.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.Patch; 2 | 3 | public class PatchDetails 4 | { 5 | public required string DistributionIdentifier { get; set; } 6 | public required string Version { get; set; } 7 | public required string FullVersion { get; set; } 8 | public required string ManifestArchiveSHA1Hash { get; set; } 9 | public required string ManifestArchiveSizeInBytes { get; set; } 10 | public required bool Latest { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Contracts/UserControllerDTOs.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Contracts; 2 | 3 | public record RegisterUserAndMainAccountDTO(string Token, string Name, string Password, string ConfirmPassword); 4 | 5 | public record LogInUserDTO(string Name, string Password); 6 | 7 | public record GetBasicUserDTO(int ID, string EmailAddress, List Accounts); 8 | 9 | public record GetBasicAccountDTO(int ID, string Name); 10 | 11 | public record GetAuthenticationTokenDTO(int UserID, string TokenType, string Token); 12 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug" 5 | } 6 | }, 7 | 8 | "Operational": { 9 | "JWT": { 10 | "SigningKey": "L44QvGhD54$1VZWEPLq9#*VN@*jIQhO&*5V4SDuRm^rMTl*UC!C*85SDZ2ge4R2xgq5Ywoj8c5zpR9xL5skvZhgBcqQGlWsKhsi!eYf&3Ih4#urP@O53E5#yQ*!qAwbs", 11 | "Issuer": "https://localhost:55556", 12 | "Audience": "https://localhost:55557", 13 | "DurationInHours": 24 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ASPIRE.Common/Internals/UsingDirectives.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.AspNetCore.Builder; 2 | global using Microsoft.AspNetCore.Diagnostics.HealthChecks; 3 | global using Microsoft.Extensions.DependencyInjection; 4 | global using Microsoft.Extensions.Diagnostics.HealthChecks; 5 | global using Microsoft.Extensions.Hosting; 6 | global using Microsoft.Extensions.Logging; 7 | 8 | global using OpenTelemetry.Metrics; 9 | global using OpenTelemetry.Trace; 10 | global using OpenTelemetry; 11 | 12 | global using System.Security.Cryptography; 13 | global using System.Text; 14 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Error" 5 | } 6 | }, 7 | 8 | "Operational": { 9 | "JWT": { 10 | "SigningKey": "X^uPJTuJJq7u0iz3hj3N9YBYi8VHcqkUn0&wV&qMSpBd%6MBrJ1v#qE2O%Lqq!3k%XkgiSM2lwuxgKo!wgaGC$9z#INgTngDu7N5zjLYusKh%29qIqmCHxh*%7z%$xwm", 11 | "Issuer": "https://portal.api.kongor.net", 12 | "Audience": "https://portal.ui.kongor.net", 13 | "DurationInHours": 24 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ASPIRE.SourceGenerator/Attributes/AutoImplementMissingMembersAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.SourceGenerator.Attributes; 2 | 3 | /// 4 | /// Marks a partial class for automatic implementation of missing interface members. 5 | /// The source generator will create stub implementations that throw for all interface members not explicitly implemented in the class. 6 | /// 7 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 8 | public sealed class AutoImplementMissingMembersAttribute : Attribute; 9 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Domain/Core/ChatServer.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Domain.Core; 2 | 3 | public class ChatServer(IPAddress address, int port, IServiceProvider serviceProvider) : TCPServer(address, port) 4 | { 5 | private IServiceProvider ServiceProvider { get; set; } = serviceProvider; 6 | 7 | protected override TCPSession CreateSession() => new ChatSession(this, ServiceProvider); 8 | 9 | protected override void OnError(SocketError error) 10 | { 11 | Log.Error($"Chat Server Caught A Socket Error With Code {error}"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/ServerManagement/MatchServerManager.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.ServerManagement; 2 | 3 | public class MatchServerManager 4 | { 5 | public required int HostAccountID { get; set; } 6 | 7 | public required string HostAccountName { get; set; } 8 | 9 | public required int ID { get; set; } 10 | 11 | public required string IPAddress { get; set; } 12 | 13 | public string Cookie { get; set; } = Guid.CreateVersion7().ToString(); 14 | 15 | public DateTimeOffset TimestampRegistered { get; set; } = DateTimeOffset.UtcNow; 16 | } 17 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Models/Configuration/OperationalConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Models.Configuration; 2 | 3 | public class OperationalConfiguration 4 | { 5 | public const string ConfigurationSection = "Operational"; 6 | public required OperationalConfigurationJWT JWT { get; set; } 7 | } 8 | 9 | public class OperationalConfigurationJWT 10 | { 11 | public required string SigningKey { get; set; } 12 | public required string Issuer { get; set; } 13 | public required string Audience { get; set; } 14 | public required int DurationInHours { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/GameData/GuideResponseError.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.GameData; 2 | 3 | public class GuideResponseError(int hostTime) 4 | { 5 | [PhpProperty("errors")] 6 | public string Errors => "no_guides_found"; 7 | 8 | [PhpProperty("success")] 9 | public int Success => 0; 10 | 11 | [PhpProperty("hosttime")] 12 | public int HostTime { get; set; } = hostTime; 13 | 14 | [PhpProperty("vested_threshold")] 15 | public int VestedThreshold => 5; 16 | 17 | [PhpProperty(0)] 18 | public bool Zero => true; 19 | } 20 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Core/Clan.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Core; 2 | 3 | [Index(nameof(Name), nameof(Tag), IsUnique = true)] 4 | public class Clan 5 | { 6 | [Key] 7 | public int ID { get; set; } 8 | 9 | [MaxLength(30)] 10 | public required string Name { get; set; } 11 | 12 | [MaxLength(4)] 13 | public required string Tag { get; set; } 14 | 15 | public List Members { get; set; } = []; 16 | 17 | public DateTimeOffset TimestampCreated { get; set; } = DateTimeOffset.UtcNow; 18 | 19 | public string GetChatChannelName() => $"Clan {Name}"; 20 | } 21 | -------------------------------------------------------------------------------- /ASPIRE.Tests/KONGOR.MasterServer/Models/SRPAuthenticationData.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Tests.KONGOR.MasterServer.Models; 2 | 3 | /// 4 | /// Result Of SRP Authentication Attempt 5 | /// 6 | public class SRPAuthenticationData 7 | { 8 | public required Account Account { get; init; } 9 | 10 | public string? Name { get; init; } 11 | 12 | public string? Email { get; init; } 13 | 14 | public required bool Success { get; init; } 15 | 16 | public string? ServerProof { get; init; } 17 | 18 | public string? Cookie { get; init; } 19 | 20 | public string? ErrorMessage { get; init; } 21 | } 22 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Helpers/InMemoryHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Helpers; 2 | 3 | public static class InMemoryHelpers 4 | { 5 | public static MerrickContext GetInMemoryMerrickContext(string? identifier = null) 6 | { 7 | DbContextOptionsBuilder builder = new (); 8 | 9 | builder.UseInMemoryDatabase(identifier ?? Guid.CreateVersion7().ToString()); 10 | 11 | DbContextOptions options = builder.Options; 12 | 13 | MerrickContext context = new (options); 14 | 15 | context.Database.EnsureCreated(); 16 | 17 | return context; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ASPIRE.SourceGenerator/ASPIRE.SourceGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | true 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "10.0.0", 7 | "commands": [ 8 | "dotnet-ef" 9 | ], 10 | "rollForward": false 11 | }, 12 | "aspire.cli": { 13 | "version": "13.0.2", 14 | "commands": [ 15 | "aspire" 16 | ], 17 | "rollForward": false 18 | }, 19 | "powershell": { 20 | "version": "7.5.4", 21 | "commands": [ 22 | "pwsh" 23 | ], 24 | "rollForward": false 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Utility/Token.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Utility; 2 | 3 | public class Token 4 | { 5 | [Key] 6 | public int ID { get; set; } 7 | 8 | public required TokenPurpose Purpose { get; set; } 9 | 10 | public required string EmailAddress { get; set; } 11 | 12 | public DateTimeOffset TimestampCreated { get; set; } = DateTimeOffset.UtcNow; 13 | 14 | public DateTimeOffset? TimestampConsumed { get; set; } 15 | 16 | public required Guid Value { get; set; } 17 | 18 | public required string Data { get; set; } 19 | } 20 | 21 | public enum TokenPurpose 22 | { 23 | EmailAddressVerification, 24 | EmailAddressUpdate 25 | } 26 | -------------------------------------------------------------------------------- /ASPIRE.Common/Compatibility/Client.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Common.Compatibility; 2 | 3 | public static class Client 4 | { 5 | # region The Client Version, As Defined By "shell_common.h" 6 | public const int MAJOR_VERSION = 4; 7 | public const int MINOR_VERSION = 10; 8 | public const int MICRO_VERSION = 1; 9 | public const int HOTFIX_VERSION = 0; 10 | # endregion 11 | 12 | public static string GetVersion() 13 | # pragma warning disable CS8520 14 | => HOTFIX_VERSION is 0 15 | ? $"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}" 16 | : $"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}.{HOTFIX_VERSION}"; 17 | # pragma warning restore CS8520 18 | } 19 | -------------------------------------------------------------------------------- /ASPIRE.Common/Compatibility/Server.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Common.Compatibility; 2 | 3 | public static class Server 4 | { 5 | # region The Server Version, As Defined By "shell_common.h" 6 | public const int MAJOR_VERSION = 4; 7 | public const int MINOR_VERSION = 10; 8 | public const int MICRO_VERSION = 1; 9 | public const int HOTFIX_VERSION = 0; 10 | # endregion 11 | 12 | public static string GetVersion() 13 | # pragma warning disable CS8520 14 | => HOTFIX_VERSION is 0 15 | ? $"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}" 16 | : $"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}.{HOTFIX_VERSION}"; 17 | # pragma warning restore CS8520 18 | } 19 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupLeave.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_LEAVE)] 4 | public class GroupLeave : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | GroupLeaveRequestData requestData = new (buffer); 9 | 10 | MatchmakingGroup 11 | .GetByMemberAccountID(session.Account.ID) 12 | .RemoveMember(session.Account.ID); 13 | } 14 | } 15 | 16 | public class GroupLeaveRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | } 20 | -------------------------------------------------------------------------------- /NEXUS.DistributedApplication.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Channels/LeaveChannel.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Channels; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_LEAVE_CHANNEL)] 4 | public class LeaveChannel : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | LeaveChannelRequestData requestData = new (buffer); 9 | 10 | ChatChannel 11 | .Get(session, requestData.ChannelName) 12 | .Leave(session); 13 | } 14 | } 15 | 16 | public class LeaveChannelRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public string ChannelName = buffer.ReadString(); 21 | } 22 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Channels/JoinChannel.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Channels; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_JOIN_CHANNEL)] 4 | public class JoinChannel : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | JoinChannelRequestData requestData = new (buffer); 9 | 10 | ChatChannel 11 | .GetOrCreate(session, requestData.ChannelName) 12 | .Join(session); 13 | } 14 | } 15 | 16 | public class JoinChannelRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public string ChannelName = buffer.ReadString(); 21 | } 22 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Communication/SendWhisper.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Communication; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_WHISPER)] 4 | public class SendWhisper : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | WhisperRequestData requestData = new (buffer); 9 | 10 | Whisper 11 | .Create(requestData.Message) 12 | .Send(session, requestData.TargetName); 13 | } 14 | } 15 | 16 | public class WhisperRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public string TargetName = buffer.ReadString(); 21 | 22 | public string Message = buffer.ReadString(); 23 | } 24 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/ZORGATH.WebPortal.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Controllers/StorageStatusController/StorageStatusController.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Controllers.StorageStatusController; 2 | 3 | [ApiController] 4 | [Route("master/storage/status")] 5 | [Consumes("application/x-www-form-urlencoded")] 6 | public class StorageStatusController : ControllerBase 7 | { 8 | [HttpPost(Name = "Storage Status")] 9 | public IActionResult StorageStatus([FromForm] Dictionary formData) 10 | { 11 | // TODO: Implement Storage Status Controller 12 | 13 | return Ok(@"a:4:{s:7:""success"";b:1;s:4:""data"";N;s:18:""cloud_storage_info"";a:4:{s:10:""account_id"";s:6:""195592"";s:9:""use_cloud"";s:1:""0"";s:16:""cloud_autoupload"";s:1:""0"";s:16:""file_modify_time"";s:19:""2021-01-10 11:39:47"";}s:8:""messages"";s:0:"""";}"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/TRANSMUTANSTEIN.ChatServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Channels/KickFromChannel.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Channels; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_CHANNEL_KICK)] 4 | public class KickFromChannel : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | KickFromChannelRequestData requestData = new (buffer); 9 | 10 | ChatChannel 11 | .Get(session, requestData.ChannelID) 12 | .Kick(session, requestData.TargetAccountID); 13 | } 14 | } 15 | 16 | public class KickFromChannelRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public int ChannelID = buffer.ReadInt32(); 21 | 22 | public int TargetAccountID = buffer.ReadInt32(); 23 | } 24 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupJoin.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_JOIN)] 4 | public class GroupJoin : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | GroupJoinRequestData requestData = new (buffer); 9 | 10 | MatchmakingGroup 11 | .GetByMemberAccountName(requestData.InviteIssuerName) 12 | .Join(session); 13 | } 14 | } 15 | 16 | public class GroupJoinRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public string ClientVersion = buffer.ReadString(); 21 | 22 | public string InviteIssuerName = buffer.ReadString(); 23 | } 24 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupInvite.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_INVITE)] 4 | public class GroupInvite(MerrickContext merrick) : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | GroupInviteRequestData requestData = new (buffer); 9 | 10 | MatchmakingGroup 11 | .GetByMemberAccountID(session.Account.ID) 12 | .Invite(session, merrick, requestData.InviteReceiverName); 13 | } 14 | } 15 | 16 | public class GroupInviteRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public string InviteReceiverName = buffer.ReadString(); 21 | } 22 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/KONGOR.MasterServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Channels/SetChannelPassword.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Channels; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_CHANNEL_SET_PASSWORD)] 4 | public class SetChannelPassword : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | SetChannelPasswordRequestData requestData = new (buffer); 9 | 10 | ChatChannel channel = ChatChannel.Get(session, requestData.ChannelID); 11 | 12 | channel.SetPassword(session, requestData.Password); 13 | } 14 | } 15 | 16 | public class SetChannelPasswordRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public int ChannelID = buffer.ReadInt32(); 21 | 22 | public string Password = buffer.ReadString(); 23 | } 24 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Channels/JoinChannelPassword.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Channels; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_JOIN_CHANNEL_PASSWORD)] 4 | public class JoinPasswordProtectedChannel : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | JoinPasswordProtectedChannelRequestData requestData = new (buffer); 9 | 10 | ChatChannel 11 | .GetOrCreate(session, requestData.ChannelName) 12 | .Join(session, requestData.Password); 13 | } 14 | } 15 | 16 | public class JoinPasswordProtectedChannelRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public string ChannelName = buffer.ReadString(); 21 | 22 | public string Password = buffer.ReadString(); 23 | } 24 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/MERRICK.DatabaseContext.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupPlayerReadyStatus.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_PLAYER_READY_STATUS)] 4 | public class GroupPlayerReadyStatus : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | GroupPlayerReadyStatusRequestData requestData = new (buffer); 9 | 10 | MatchmakingGroup 11 | .GetByMemberAccountID(session.Account.ID) 12 | .SendPlayerReadinessStatusUpdate(session, requestData.GameType); 13 | } 14 | } 15 | 16 | public class GroupPlayerReadyStatusRequestData(ChatBuffer buffer) 17 | { 18 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 19 | 20 | public byte ReadyStatus = buffer.ReadInt8(); 21 | 22 | public ChatProtocol.TMMGameType GameType = (ChatProtocol.TMMGameType) buffer.ReadInt8(); 23 | } 24 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Enumerations/AccountType.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Enumerations; 2 | 3 | public enum AccountType 4 | { 5 | // built-in types 6 | Disabled = 0, // prevented from logging into HoN 7 | Trial = 1, // has limited access to in-game functions 8 | ServerHost = 2, // has permissions to host game servers 9 | Normal = 3, // free-to-play account 10 | Legacy = 4, // purchased HoN during the beta 11 | Staff = 5, // has access to admin functions and can execute admin commands 12 | GameMaster = 6, // can suspend players 13 | MatchModerator = 7, // can spectate and moderate matches 14 | MatchCaster = 8, // can spectate matches 15 | 16 | // custom types 17 | Streamer = 101, // has top priority in the matchmaking queue 18 | VIP = 102 // has top priority in the matchmaking queue 19 | } 20 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Actions/TrackPlayerAction.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Actions; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_TRACK_PLAYER_ACTION)] 4 | public class TrackPlayerAction : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | TrackPlayerActionRequestData requestData = new (buffer); 9 | 10 | // TODO: Do Something With This Data 11 | 12 | // TODO: Look Into Updating The Online Players Count And Matchmaking Details Using Custom Action OBTAIN_DETAILED_ONLINE_STATUS (Or Equivalent) 13 | 14 | Log.Error(@"Unhandled User Action: ""{RequestData.Action}""", requestData.Action); 15 | } 16 | } 17 | 18 | public class TrackPlayerActionRequestData(ChatBuffer buffer) 19 | { 20 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 21 | 22 | public ChatProtocol.ActionCampaign Action = (ChatProtocol.ActionCampaign) buffer.ReadInt8(); 23 | } 24 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "TRANSMUTANSTEIN.ChatServer Development": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "applicationUrl": "https://localhost:55554", 8 | "launchUrl": "health", 9 | "environmentVariables": { 10 | "APPLICATION_URL": "https://localhost:55554", 11 | "ASPNETCORE_ENVIRONMENT": "Development", 12 | "DOTNET_ENVIRONMENT": "Development" 13 | }, 14 | "dotnetRunMessages": true 15 | }, 16 | "TRANSMUTANSTEIN.ChatServer Production": { 17 | "commandName": "Project", 18 | "launchBrowser": true, 19 | "applicationUrl": "https://chat.kongor.net", 20 | "launchUrl": "health", 21 | "environmentVariables": { 22 | "APPLICATION_URL": "https://chat.kongor.net", 23 | "ASPNETCORE_ENVIRONMENT": "Production", 24 | "DOTNET_ENVIRONMENT": "Production" 25 | }, 26 | "dotnetRunMessages": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Extensions/Protocol/AccountExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Extensions.Protocol; 2 | 3 | public static class AccountExtensions 4 | { 5 | public static byte GetChatClientFlags(this Account account) 6 | { 7 | byte flags = 0x0; 8 | 9 | if (account.Type == AccountType.Staff) 10 | { 11 | flags |= Convert.ToByte(ChatProtocol.ChatClientType.CHAT_CLIENT_IS_STAFF); 12 | } 13 | 14 | if (account.ClanTier == ClanTier.Leader) 15 | { 16 | flags |= Convert.ToByte(ChatProtocol.ChatClientType.CHAT_CLIENT_IS_CLAN_LEADER); 17 | } 18 | 19 | if (account.ClanTier == ClanTier.Officer) 20 | { 21 | flags |= Convert.ToByte(ChatProtocol.ChatClientType.CHAT_CLIENT_IS_OFFICER); 22 | } 23 | 24 | // TODO: Do Something With ChatProtocol.ChatClientType.CHAT_CLIENT_IS_PREMIUM And ChatProtocol.ChatClientType.CHAT_CLIENT_IS_VERIFIED 25 | 26 | return flags; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Social/ApproveFriend.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Social; 2 | 3 | /// 4 | /// Handles friend approval requests. 5 | /// Approves a pending friend request from another player, creating a bi-directional friendship. 6 | /// 7 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_REQUEST_BUDDY_APPROVE)] 8 | public class ApproveFriend(MerrickContext merrick, IDatabase distributedCacheStore) : IAsynchronousCommandProcessor 9 | { 10 | public async Task Process(ChatSession session, ChatBuffer buffer) 11 | { 12 | ApproveFriendRequestData requestData = new (buffer); 13 | 14 | await Friend 15 | .WithAccountName(requestData.FriendNickname) 16 | .Approve(session, merrick, distributedCacheStore); 17 | } 18 | } 19 | 20 | public class ApproveFriendRequestData(ChatBuffer buffer) 21 | { 22 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 23 | 24 | public string FriendNickname = buffer.ReadString(); 25 | } 26 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Services/DatabaseHealthCheck.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Services; 2 | 3 | public class DatabaseHealthCheck(DatabaseInitializer initializer) : IHealthCheck 4 | { 5 | public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) 6 | { 7 | Task? task = initializer.ExecuteTask; 8 | 9 | return task switch 10 | { 11 | { IsCompletedSuccessfully: true } => Task.FromResult(HealthCheckResult.Healthy("[HEALTHY] Database Is Accepting Connections")), 12 | { IsFaulted: true } => Task.FromResult(HealthCheckResult.Unhealthy($"[UNHEALTHY] {task.Exception.InnerException?.Message}", task.Exception)), 13 | { IsCanceled: true } => Task.FromResult(HealthCheckResult.Unhealthy("[UNHEALTHY] Database Initialization Was Cancelled")), 14 | _ => Task.FromResult(HealthCheckResult.Unhealthy("[UNHEALTHY] Database Initialization Is In Progress")) 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Services/ChatServerHealthCheck.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Services; 2 | 3 | public class ChatServerHealthCheck : IHealthCheck 4 | { 5 | public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) 6 | { 7 | if (ChatService.ChatServer is null) 8 | return Task.FromResult(HealthCheckResult.Unhealthy("Chat Server Is Not Running")); 9 | 10 | return ChatService.ChatServer.IsStarted switch 11 | { 12 | true when ChatService.ChatServer.IsAccepting => Task.FromResult(HealthCheckResult.Healthy("[HEALTHY] Chat Server Is Running And Accepting Connections")), 13 | true when ChatService.ChatServer.IsAccepting is false => Task.FromResult(HealthCheckResult.Degraded("[DEGRADED] Chat Server Is Running But Is Not Accepting Connections")), 14 | _ => Task.FromResult(HealthCheckResult.Unhealthy("[UNHEALTHY] Unknown Chat Server Status")) 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ASPIRE.Common/ASPIRE.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | library 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ASPIRE.Common/Communication/ChatChannels.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Common.Communication; 2 | 3 | public static class ChatChannels 4 | { 5 | // "KONGOR" is a special channel name that maps to "KONGOR 1", then to "KONGOR 2" if "KONGOR 1" is full, then to "KONGOR 3" if "KONGOR 2" is full, and so on. 6 | public const string GeneralChannel = "KONGOR"; // TODO: Implement Channel Load Balancing 7 | 8 | public const string GameMastersChannel = "GAME MASTERS"; 9 | public const string GuestsChannel = "GUESTS"; 10 | public const string ServerHostsChannel = "HOSTS"; // TODO: Implement Commands To Query Hosts From This Channel As Administrator 11 | public const string StreamersChannel = "STREAMERS"; 12 | public const string VIPChannel = "VIP"; 13 | 14 | // "TERMINAL" is a special channel from which chat server commands can be executed. 15 | public const string StaffChannel = "TERMINAL"; // TODO: Implement TERMINAL Command Palette 16 | 17 | public static readonly string[] AllDefaultChannels = [ GeneralChannel, GameMastersChannel, GuestsChannel, ServerHostsChannel, StreamersChannel, VIPChannel, StaffChannel ]; 18 | } 19 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Domain/Matchmaking/MatchmakingGroupMember.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Domain.Matchmaking; 2 | 3 | public class MatchmakingGroupMember(ChatSession session) 4 | { 5 | public Account Account = session.Account; 6 | 7 | public ChatSession Session = session; 8 | 9 | public required byte Slot { get; set; } 10 | 11 | public required bool IsLeader { get; set; } 12 | 13 | public required bool IsReady { get; set; } 14 | 15 | public required bool IsInGame { get; set; } 16 | 17 | public required bool IsEligibleForMatchmaking { get; set; } 18 | 19 | public required byte LoadingPercent { get; set; } 20 | 21 | public string Country { get; set; } = "NEWERTH"; 22 | 23 | /// 24 | /// Whether Or Not The Group Member Has Access To All Of The Group's Game Modes 25 | /// 26 | public bool HasGameModeAccess { get; set; } = true; 27 | 28 | /// 29 | /// The Group Member's Game Mode Access, Delimited By "|" (e.g. "true|true|false") 30 | /// 31 | public required string GameModeAccess { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Social/AddFriend.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Social; 2 | 3 | /// 4 | /// Handles friend addition requests. 5 | /// Adds the target account to the requester's friend list, and persists the changes to the database. 6 | /// If the target has already made a friend addition request to the requester, immediately creates a bi-directional friendship. 7 | /// 8 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_REQUEST_BUDDY_ADD)] 9 | public class AddFriend(MerrickContext merrick, IDatabase distributedCacheStore) : IAsynchronousCommandProcessor 10 | { 11 | public async Task Process(ChatSession session, ChatBuffer buffer) 12 | { 13 | AddFriendRequestData requestData = new (buffer); 14 | 15 | await Friend 16 | .WithAccountName(requestData.FriendNickname) 17 | .Add(session, merrick, distributedCacheStore); 18 | } 19 | } 20 | 21 | public class AddFriendRequestData(ChatBuffer buffer) 22 | { 23 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 24 | 25 | public string FriendNickname = buffer.ReadString(); 26 | } 27 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "KONGOR.MasterServer Development": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "applicationUrl": "http://localhost:55555", 8 | "launchUrl": "swagger", 9 | "environmentVariables": { 10 | "APPLICATION_URL": "http://localhost:55555", 11 | "ASPNETCORE_ENVIRONMENT": "Development", 12 | "DOTNET_ENVIRONMENT": "Development" 13 | }, 14 | "dotnetRunMessages": true 15 | }, 16 | "KONGOR.MasterServer Production": { 17 | "commandName": "Project", 18 | "launchBrowser": true, 19 | "applicationUrl": "http://api.kongor.net", 20 | "launchUrl": "swagger", 21 | "environmentVariables": { 22 | "APPLICATION_URL": "http://api.kongor.net", 23 | "ASPNETCORE_ENVIRONMENT": "Production", 24 | "DOTNET_ENVIRONMENT": "Production" 25 | }, 26 | "dotnetRunMessages": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "MERRICK.DatabaseContext Development": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "applicationUrl": "https://localhost:55553", 8 | "launchUrl": "health", 9 | "environmentVariables": { 10 | "APPLICATION_URL": "https://localhost:55553", 11 | "ASPNETCORE_ENVIRONMENT": "Development", 12 | "DOTNET_ENVIRONMENT": "Development" 13 | }, 14 | "dotnetRunMessages": true 15 | }, 16 | "MERRICK.DatabaseContext Production": { 17 | "commandName": "Project", 18 | "launchBrowser": true, 19 | "applicationUrl": "https://database.kongor.net", 20 | "launchUrl": "health", 21 | "environmentVariables": { 22 | "APPLICATION_URL": "https://database.kongor.net", 23 | "ASPNETCORE_ENVIRONMENT": "Production", 24 | "DOTNET_ENVIRONMENT": "Production" 25 | }, 26 | "dotnetRunMessages": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "ZORGATH.WebPortal.API Development": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "applicationUrl": "https://localhost:55556", 8 | "launchUrl": "swagger", 9 | "environmentVariables": { 10 | "APPLICATION_URL": "https://localhost:55556", 11 | "ASPNETCORE_ENVIRONMENT": "Development", 12 | "DOTNET_ENVIRONMENT": "Development" 13 | }, 14 | "dotnetRunMessages": true 15 | }, 16 | "ZORGATH.WebPortal.API Production": { 17 | "commandName": "Project", 18 | "launchBrowser": true, 19 | "applicationUrl": "https://portal.api.kongor.net", 20 | "launchUrl": "swagger", 21 | "environmentVariables": { 22 | "APPLICATION_URL": "https://portal.api.kongor.net", 23 | "ASPNETCORE_ENVIRONMENT": "Production", 24 | "DOTNET_ENVIRONMENT": "Production" 25 | }, 26 | "dotnetRunMessages": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupPlayerLoadingStatus.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | /// 4 | /// Processes loading status updates for a matchmaking group member. 5 | /// When all members reach 100% loading status it automatically joins the queue, complementing which handles explicit queue join requests from the group leader. 6 | /// Both paths validate the same conditions. 7 | /// 8 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_PLAYER_LOADING_STATUS)] 9 | public class GroupPlayerLoadingStatus : ISynchronousCommandProcessor 10 | { 11 | public void Process(ChatSession session, ChatBuffer buffer) 12 | { 13 | GroupPlayerLoadingStatusRequestData requestData = new (buffer); 14 | 15 | MatchmakingGroup 16 | .GetByMemberAccountID(session.Account.ID) 17 | .SendLoadingStatusUpdate(session, requestData.LoadingPercent); 18 | } 19 | } 20 | 21 | public class GroupPlayerLoadingStatusRequestData(ChatBuffer buffer) 22 | { 23 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 24 | 25 | public byte LoadingPercent = buffer.ReadInt8(); 26 | } 27 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | net10.0 24 | latest 25 | major 26 | enable 27 | enable 28 | nullable 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ASPIRE.ApplicationHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "ASPIRE.ApplicationHost Development": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "applicationUrl": "https://localhost:55550", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development", 10 | "DOTNET_ENVIRONMENT": "Development", 11 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:55551", 12 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:55552" 13 | }, 14 | "dotnetRunMessages": true 15 | }, 16 | "ASPIRE.ApplicationHost Production": { 17 | "commandName": "Project", 18 | "launchBrowser": true, 19 | "applicationUrl": "https://aspire.kongor.net", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Production", 22 | "DOTNET_ENVIRONMENT": "Production", 23 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://telemetry.kongor.net", 24 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://resources.kongor.net" 25 | }, 26 | "dotnetRunMessages": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Controllers/ClientRequesterController/ClientRequesterControllerServerList.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Controllers.ClientRequesterController; 2 | 3 | public partial class ClientRequesterController 4 | { 5 | private async Task GetServerList() 6 | { 7 | string? cookie = Request.Form["cookie"]; 8 | 9 | if (cookie is null) 10 | return BadRequest(@"Missing Value For Form Parameter ""cookie"""); 11 | 12 | string? gameType = Request.Form.ContainsKey("gametype") ? Request.Form["gametype"].ToString() : null; 13 | 14 | List servers = await DistributedCache.GetMatchServers(); 15 | 16 | switch (gameType) 17 | { 18 | case "10": 19 | return Ok(PhpSerialization.Serialize(new ServerForJoinListResponse(servers, cookie))); 20 | 21 | case "90": 22 | string? region = Request.Form.ContainsKey("region") ? Request.Form["region"].ToString() : null; 23 | 24 | return Ok(PhpSerialization.Serialize(new ServerForCreateListResponse(servers, region, cookie))); 25 | 26 | default: 27 | Logger.LogError($@"[BUG] Unknown Server List Game Type ""{gameType ?? "NULL"}"""); 28 | 29 | return UnprocessableEntity($@"Unknown Server List Game Type ""{gameType ?? "NULL"}"""); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/run-unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: "Unit/Integration Tests" 2 | 3 | on: 4 | 5 | workflow_dispatch: # enable manual trigger 6 | 7 | schedule: 8 | - cron: "0 4 * * *" 9 | 10 | pull_request: 11 | branches: [ "main" ] 12 | 13 | jobs: 14 | 15 | build-and-test: 16 | 17 | runs-on: "ubuntu-latest" 18 | 19 | steps: # latest action versions can be inspected at https://github.com/actions 20 | 21 | - name: "Check Out Branch" 22 | uses: "actions/checkout@v4" 23 | 24 | - name: "Install .NET 10" 25 | uses: "actions/setup-dotnet@v4" 26 | with: 27 | dotnet-version: "10.x" 28 | 29 | - name: "Print .NET Version" 30 | run: "dotnet --version" 31 | 32 | - name: "Restore Dependencies" 33 | run: "dotnet restore NEXUS.DistributedApplication.slnx" 34 | 35 | - name: "Build Solution" 36 | run: "dotnet build NEXUS.DistributedApplication.slnx --no-restore" 37 | 38 | - name: "Run Tests" 39 | run: "dotnet test --solution NEXUS.DistributedApplication.slnx --no-build --verbosity normal --report-trx --results-directory test-results" 40 | 41 | - name: "Publish Test Results" 42 | uses: "actions/upload-artifact@v4" 43 | with: 44 | name: "Published Test Results" 45 | path: "test-results" 46 | compression-level: "0" 47 | if-no-files-found: "error" 48 | retention-days: "28" 49 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Core/User.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Core; 2 | 3 | [Index(nameof(EmailAddress), IsUnique = true)] 4 | public class User 5 | { 6 | [Key] 7 | public int ID { get; set; } 8 | 9 | [MaxLength(30)] 10 | public required string EmailAddress { get; set; } 11 | 12 | public required Role Role { get; set; } 13 | 14 | [StringLength(64)] 15 | public required string SRPPasswordSalt { get; set; } 16 | 17 | [StringLength(64)] 18 | public required string SRPPasswordHash { get; set; } 19 | 20 | [StringLength(84)] 21 | public string PBKDF2PasswordHash { get; set; } = null!; 22 | 23 | public List Accounts { get; set; } = []; 24 | 25 | public DateTimeOffset TimestampCreated { get; set; } = DateTimeOffset.UtcNow; 26 | 27 | public DateTimeOffset TimestampLastActive { get; set; } = DateTimeOffset.UtcNow; 28 | 29 | public int GoldCoins { get; set; } = 0; 30 | 31 | public int SilverCoins { get; set; } = 0; 32 | 33 | public int PlinkoTickets { get; set; } = 0; 34 | 35 | public int TotalLevel { get; set; } = 0; 36 | 37 | public int TotalExperience { get; set; } = 0; 38 | 39 | public List OwnedStoreItems { get; set; } = ["ai.Default Icon", "cc.white", "t.Standard"]; 40 | 41 | [NotMapped] 42 | public bool IsAdministrator => Role.Name.Equals(UserRoles.Administrator); 43 | } 44 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/ServerManagement/MatchServer.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.ServerManagement; 2 | 3 | public class MatchServer 4 | { 5 | public required int HostAccountID { get; set; } 6 | 7 | public required string HostAccountName { get; set; } 8 | 9 | public required int ID { get; set; } 10 | 11 | public required string Name { get; set; } 12 | 13 | public required int Instance { get; set; } 14 | 15 | public required string IPAddress { get; set; } 16 | 17 | public required int Port { get; set; } 18 | 19 | public required string Location { get; set; } 20 | 21 | public required string Description { get; set; } 22 | 23 | public ServerStatus Status { get; set; } = ServerStatus.SERVER_STATUS_IDLE; 24 | 25 | public string Cookie { get; set; } = Guid.CreateVersion7().ToString(); 26 | 27 | public DateTimeOffset TimestampRegistered { get; set; } = DateTimeOffset.UtcNow; 28 | } 29 | 30 | /// 31 | /// This enumeration is part of the Chat Server Protocol, and needs to match its counterpart in order for servers in the distributed cache to be handled correctly. 32 | /// 33 | public enum ServerStatus 34 | { 35 | SERVER_STATUS_SLEEPING, 36 | SERVER_STATUS_IDLE, 37 | SERVER_STATUS_LOADING, 38 | SERVER_STATUS_ACTIVE, 39 | SERVER_STATUS_CRASHED, 40 | SERVER_STATUS_KILLED, 41 | 42 | SERVER_STATUS_UNKNOWN 43 | }; 44 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Channels/SilenceChannelMember.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Channels; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_CHANNEL_SILENCE_USER)] 4 | public class SilenceChannelMember : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | SilenceChannelMemberRequestData requestData = new (buffer); 9 | 10 | ChatChannel channel = ChatChannel.Get(session, requestData.ChannelID); 11 | 12 | // Find The Target Account ID By Name 13 | ChatSession? targetSession = Context.ChatSessions.Values 14 | .SingleOrDefault(chatSession => chatSession.Account.Name.Equals(requestData.TargetName, StringComparison.OrdinalIgnoreCase)); 15 | 16 | if (targetSession is null) 17 | { 18 | // TODO: Notify Requester That Target User Was Not Found 19 | 20 | return; 21 | } 22 | 23 | channel.Silence(session, targetSession.Account.ID, requestData.DurationMilliseconds); 24 | } 25 | } 26 | 27 | public class SilenceChannelMemberRequestData(ChatBuffer buffer) 28 | { 29 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 30 | 31 | public int ChannelID = buffer.ReadInt32(); 32 | 33 | public string TargetName = buffer.ReadString(); 34 | 35 | public int DurationMilliseconds = buffer.ReadInt32(); 36 | } 37 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Internals/UsingDirectives.cs: -------------------------------------------------------------------------------- 1 | global using ASPIRE.Common.Communication; 2 | global using ASPIRE.Common.ServiceDefaults; 3 | 4 | global using MERRICK.DatabaseContext.Constants; 5 | global using MERRICK.DatabaseContext.Data; 6 | global using MERRICK.DatabaseContext.Entities.Core; 7 | global using MERRICK.DatabaseContext.Entities.Game; 8 | global using MERRICK.DatabaseContext.Entities.Relational; 9 | global using MERRICK.DatabaseContext.Entities.Statistics; 10 | global using MERRICK.DatabaseContext.Entities.Utility; 11 | global using MERRICK.DatabaseContext.Enumerations; 12 | global using MERRICK.DatabaseContext.Extensions; 13 | global using MERRICK.DatabaseContext.Handlers; 14 | global using MERRICK.DatabaseContext.Persistence; 15 | global using MERRICK.DatabaseContext.Services; 16 | 17 | global using Microsoft.EntityFrameworkCore; 18 | global using Microsoft.EntityFrameworkCore.ChangeTracking; 19 | global using Microsoft.EntityFrameworkCore.Diagnostics; 20 | global using Microsoft.EntityFrameworkCore.Metadata.Builders; 21 | global using Microsoft.EntityFrameworkCore.Storage; 22 | global using Microsoft.Extensions.Diagnostics.HealthChecks; 23 | 24 | global using System.ComponentModel.DataAnnotations; 25 | global using System.ComponentModel.DataAnnotations.Schema; 26 | global using System.Diagnostics; 27 | global using System.Reflection; 28 | global using System.Security.Claims; 29 | global using System.Linq.Expressions; 30 | global using System.Text.Json; 31 | -------------------------------------------------------------------------------- /ASPIRE.Tests/ASPIRE.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | exe 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Domain/Core/ChatSessionMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Domain.Core; 2 | 3 | public class ChatSessionMetadata(ClientHandshakeRequestData data) 4 | { 5 | public string RemoteIP = data.RemoteIP; 6 | 7 | public string SessionCookie = data.SessionCookie; 8 | public string SessionAuthenticationHash = data.SessionAuthenticationHash; 9 | 10 | public int ChatProtocolVersion = data.ChatProtocolVersion; 11 | 12 | public byte OperatingSystemIdentifier = data.OperatingSystemIdentifier; 13 | public byte OperatingSystemVersionMajor = data.OperatingSystemVersionMajor; 14 | public byte OperatingSystemVersionMinor = data.OperatingSystemVersionMinor; 15 | public byte OperatingSystemVersionPatch = data.OperatingSystemVersionPatch; 16 | public string OperatingSystemBuildCode = data.OperatingSystemBuildCode; 17 | public string OperatingSystemArchitecture = data.OperatingSystemArchitecture; 18 | 19 | public byte ClientVersionMajor = data.ClientVersionMajor; 20 | public byte ClientVersionMinor = data.ClientVersionMinor; 21 | public byte ClientVersionPatch = data.ClientVersionPatch; 22 | public byte ClientVersionRevision = data.ClientVersionRevision; 23 | 24 | public ChatProtocol.ChatClientStatus LastKnownClientState = data.LastKnownClientState; 25 | public ChatProtocol.ChatModeType ClientChatModeState = data.ClientChatModeState; 26 | 27 | public string ClientRegion = data.ClientRegion; 28 | public string ClientLanguage = data.ClientLanguage; 29 | } 30 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Game/HeroGuide.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Game; 2 | 3 | public class HeroGuide 4 | { 5 | [Key] 6 | public int ID { get; set; } 7 | 8 | [MaxLength(50)] 9 | public required string Name { get; set; } 10 | 11 | [MaxLength(20)] 12 | public required string HeroName { get; set; } 13 | 14 | [MaxLength(25)] 15 | public required string HeroIdentifier { get; set; } 16 | 17 | [MaxLength(500)] 18 | public required string Intro { get; set; } 19 | 20 | [MaxLength(1500)] 21 | public required string Content { get; set; } 22 | 23 | [MaxLength(150)] 24 | public required string StartingItems { get; set; } 25 | 26 | [MaxLength(150)] 27 | public required string EarlyGameItems { get; set; } 28 | 29 | [MaxLength(150)] 30 | public required string CoreItems { get; set; } 31 | 32 | [MaxLength(150)] 33 | public required string LuxuryItems { get; set; } 34 | 35 | [MaxLength(750)] 36 | public required string AbilityQueue { get; set; } 37 | 38 | public required Account Author { get; set; } 39 | 40 | public required float Rating { get; set; } 41 | 42 | public required int UpVotes { get; set; } 43 | 44 | public required int DownVotes { get; set; } 45 | 46 | public required bool Public { get; set; } 47 | 48 | public required bool Featured { get; set; } 49 | 50 | public DateTimeOffset TimestampCreated { get; set; } = DateTimeOffset.UtcNow; 51 | 52 | public DateTimeOffset? TimestampLastUpdated { get; set; } 53 | } 54 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupRejectInvite.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_REJECT_INVITE)] 4 | public class GroupRejectInvite : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | GroupRejectInviteRequestData requestData = new (buffer); 9 | 10 | // Find The Inviter's Group 11 | MatchmakingGroup? group = MatchmakingService.GetMatchmakingGroup(requestData.InviterName); 12 | 13 | if (group is null) 14 | return; 15 | 16 | // Broadcast Rejection To All Group Members 17 | ChatBuffer rejectBroadcast = new (); 18 | 19 | rejectBroadcast.WriteCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_REJECT_INVITE); 20 | rejectBroadcast.WriteString(session.Account.Name); // Rejecting Player Name 21 | rejectBroadcast.WriteString(requestData.InviterName); // Inviter Name 22 | 23 | foreach (MatchmakingGroupMember member in group.Members) 24 | member.Session.Send(rejectBroadcast); 25 | 26 | // Send Partial Group Update To All Members 27 | group.MulticastUpdate(session.Account.ID, ChatProtocol.TMMUpdateType.TMM_PARTIAL_GROUP_UPDATE); 28 | } 29 | } 30 | 31 | public class GroupRejectInviteRequestData(ChatBuffer buffer) 32 | { 33 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 34 | 35 | public string InviterName = buffer.ReadString(); 36 | } 37 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/SRP/SRPAuthenticationResponseStageOne.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.SRP; 2 | 3 | public class SRPAuthenticationResponseStageOne(SRPAuthenticationSessionDataStageOne stageOneData) 4 | { 5 | /// 6 | /// The SRP session salt, used by the client during authentication. 7 | /// Not sent in the event of an invalid account name. 8 | /// 9 | [PhpProperty("salt")] 10 | public string Salt { get; init; } = stageOneData.SessionSalt; 11 | 12 | /// 13 | /// HoN's specific salt, used by the client in the password hashing algorithm. 14 | /// Not sent in the event of an invalid account name. 15 | /// 16 | [PhpProperty("salt2")] 17 | public string Salt2 { get; init; } = stageOneData.PasswordSalt; 18 | 19 | /// 20 | /// The ephemeral SRP value "B", created by the server and sent to the client for use during SRP authentication. 21 | /// 22 | [PhpProperty("B")] 23 | public string B { get; init; } = stageOneData.ServerPublicEphemeral; 24 | 25 | /// 26 | /// Unknown property which seems to often be set to "5", for some reason. 27 | /// 28 | [PhpProperty("vested_threshold")] 29 | public int VestedThreshold => 5; 30 | 31 | /// 32 | /// Unknown property which seems to be set to "true" on a successful response or to "false" if an error occurs. 33 | /// If an error occurred, use "SRPAuthenticationFailureResponse" instead. 34 | /// 35 | [PhpProperty(0)] 36 | public bool Zero => true; 37 | } 38 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupJoinQueue.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | /// 4 | /// Handles explicit queue join requests from the group leader. 5 | /// This path complements which auto-joins the queue when all players reach 100% loading status. 6 | /// Both paths validate the same conditions. 7 | /// 8 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_JOIN_QUEUE)] 9 | public class GroupJoinQueue : ISynchronousCommandProcessor 10 | { 11 | public void Process(ChatSession session, ChatBuffer buffer) 12 | { 13 | GroupJoinQueueRequestData requestData = new (buffer); 14 | 15 | MatchmakingGroup? group = MatchmakingService.GetMatchmakingGroup(session.Account.ID); 16 | 17 | if (group is null) 18 | { 19 | Log.Error("[BUG] No Matchmaking Group Found For Account ID {AccountID}", session.Account.ID); 20 | 21 | return; 22 | } 23 | 24 | // Validate That The Requesting Player Is The Group Leader 25 | if (group.Leader.Account.ID != session.Account.ID) 26 | { 27 | Log.Error("[BUG] Account ID {AccountID} Attempted To Join Queue For Group {GroupGUID} But Is Not The Leader", session.Account.ID, group.GUID); 28 | 29 | return; 30 | } 31 | 32 | // Call The Group's Queue Join Method Which Validates All Conditions 33 | group.JoinQueue(); 34 | } 35 | } 36 | 37 | public class GroupJoinQueueRequestData(ChatBuffer buffer) 38 | { 39 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 40 | } 41 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Internals/UsingDirectives.cs: -------------------------------------------------------------------------------- 1 | global using ASPIRE.Common.ServiceDefaults; 2 | 3 | global using FluentValidation; 4 | global using FluentValidation.Results; 5 | 6 | global using MERRICK.DatabaseContext.Constants; 7 | global using MERRICK.DatabaseContext.Entities.Core; 8 | global using MERRICK.DatabaseContext.Entities.Utility; 9 | global using MERRICK.DatabaseContext.Persistence; 10 | 11 | global using Microsoft.AspNetCore.Authentication.JwtBearer; 12 | global using Microsoft.AspNetCore.Authorization; 13 | global using Microsoft.AspNetCore.HttpLogging; 14 | global using Microsoft.AspNetCore.Identity; 15 | global using Microsoft.AspNetCore.Mvc; 16 | global using Microsoft.AspNetCore.RateLimiting; 17 | global using Microsoft.EntityFrameworkCore; 18 | global using Microsoft.EntityFrameworkCore.Diagnostics; 19 | global using Microsoft.Extensions.FileProviders; 20 | global using Microsoft.Extensions.Options; 21 | global using Microsoft.IdentityModel.Tokens; 22 | global using Microsoft.OpenApi; 23 | 24 | global using SecureRemotePassword; 25 | 26 | global using System.IdentityModel.Tokens.Jwt; 27 | global using System.Security.Claims; 28 | global using System.Security.Cryptography; 29 | global using System.Text; 30 | global using System.Text.RegularExpressions; 31 | global using System.Threading.RateLimiting; 32 | 33 | global using ZORGATH.WebPortal.API.Constants; 34 | global using ZORGATH.WebPortal.API.Contracts; 35 | global using ZORGATH.WebPortal.API.Extensions; 36 | global using ZORGATH.WebPortal.API.Handlers; 37 | global using ZORGATH.WebPortal.API.Helpers; 38 | global using ZORGATH.WebPortal.API.Models.Configuration; 39 | global using ZORGATH.WebPortal.API.Services.Email; 40 | global using ZORGATH.WebPortal.API.Validators; 41 | -------------------------------------------------------------------------------- /ASPIRE.ApplicationHost/ASPIRE.ApplicationHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | exe 12 | true 13 | 00005072-6f6a-6563-7420-4b4f4e474f52 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Constants/Claims.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Constants; 2 | 3 | /// 4 | /// 5 | /// Used for setting and getting claims for JwtSecurityToken.Claims or for User.Claims (inside a controller action). 6 | /// Compared to JwtSecurityToken.Claims, some of these claims will be different when getting them from User.Claims. 7 | /// 8 | /// 9 | /// "email" in JwtSecurityToken.Claims is "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" in User.Claims. 10 | ///
11 | /// "sub" in JwtSecurityToken.Claims is "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" in User.Claims. 12 | ///
13 | ///
14 | public static class Claims 15 | { 16 | public const string AccountID = "account_id"; 17 | public const string AccountIsMain = "account_is_main"; 18 | public const string Audience = "aud"; 19 | public const string AuthenticatedAtTime = "auth_time"; 20 | public const string ClanName = "clan_name"; 21 | public const string ClanTag = "clan_tag"; 22 | public const string Email = "email"; 23 | public const string EmailAddress = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; 24 | public const string ExpiresAtTime = "exp"; 25 | public const string IssuedAtTime = "iat"; 26 | public const string Issuer = "iss"; 27 | public const string JWTIdentifier = "jti"; 28 | public const string NameIdentifier = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; 29 | public const string Nonce = "nonce"; 30 | public const string Subject = "sub"; 31 | public const string UserID = "user_id"; 32 | public const string UserRole = "user_role"; 33 | } 34 | -------------------------------------------------------------------------------- /ASPIRE.Common/Extensions/Cryptography/CryptographyExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Common.Extensions.Cryptography; 2 | 3 | public static class CryptographyExtensions 4 | { 5 | /// 6 | /// Generates a hash code with a numeric value between 0 and 4,294,967,295 (unsigned integer). 7 | /// Unlike .NET's GetHashCode(), this method will always generate the same numeric hash code for the same input value. 8 | /// 9 | public static uint GetDeterministicUInt32Hash(this object hashable) 10 | { 11 | byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(hashable.ToString() ?? string.Empty)); 12 | 13 | uint first = BitConverter.ToUInt32(hash, 0); 14 | uint second = BitConverter.ToUInt32(hash, 4); 15 | uint third = BitConverter.ToUInt32(hash, 8); 16 | uint fourth = BitConverter.ToUInt32(hash, 12); 17 | 18 | return first ^ second ^ third ^ fourth; 19 | } 20 | 21 | /// 22 | /// Generates a hash code with a numeric value between 0 and 2,147,483,647 (positive signed integer). 23 | /// Unlike .NET's GetHashCode(), this method will always generate the same numeric hash code for the same input value. 24 | /// 25 | public static int GetDeterministicInt32Hash(this object hashable, bool absolute = true) 26 | { 27 | byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(hashable.ToString() ?? string.Empty)); 28 | 29 | int first = BitConverter.ToInt32(hash, 0); 30 | int second = BitConverter.ToInt32(hash, 4); 31 | int third = BitConverter.ToInt32(hash, 8); 32 | int fourth = BitConverter.ToInt32(hash, 12); 33 | 34 | return absolute ? Math.Abs(first ^ second ^ third ^ fourth) : first ^ second ^ third ^ fourth; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Social/RemoveFriend.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Social; 2 | 3 | /// 4 | /// Handles friend removal notification. 5 | /// 6 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_NOTIFY_BUDDY_REMOVE)] 7 | public class RemoveFriend : ISynchronousCommandProcessor 8 | { 9 | public void Process(ChatSession session, ChatBuffer buffer) 10 | { 11 | RemoveFriendNotificationData notificationData = new (buffer); 12 | 13 | /* 14 | This is a NOOP (no operation) as per the implementation of the chat protocol on the side of the game client. 15 | The intention is to avoid notifying players when they have been removed from another player's friend list. 16 | The requesting player will still appear in the friend list of the removed player until they perform a logout/login cycle. 17 | */ 18 | return; 19 | } 20 | } 21 | 22 | public class RemoveFriendNotificationData(ChatBuffer buffer) 23 | { 24 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 25 | 26 | public int RemovedFriendAccountID = buffer.ReadInt32(); 27 | 28 | /// 29 | /// The ID of the notification for removing another player from the client's friend list. 30 | /// Used for managing notifications while the client is offline; to be received on next login. 31 | /// 32 | public int RequesterNotificationID = buffer.ReadInt32(); 33 | 34 | /// 35 | /// The ID of the notification for being removed from another player's friend list. 36 | /// Used for managing notifications while the client is offline; to be received on next login. 37 | /// 38 | public int RemovedFriendNotificationID = buffer.ReadInt32(); 39 | } 40 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Domain/Matchmaking/MatchmakingGroupInformation.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Domain.Matchmaking; 2 | 3 | public class MatchmakingGroupInformation 4 | { 5 | public required string ClientVersion { get; set; } 6 | 7 | public required ChatProtocol.TMMType GroupType { get; set; } 8 | 9 | public required ChatProtocol.TMMGameType GameType { get; set; } 10 | 11 | public required string MapName { get; set; } 12 | 13 | public required string[] GameModes { get; set; } 14 | 15 | public required string[] GameRegions { get; set; } 16 | 17 | public required bool Ranked { get; set; } 18 | 19 | public required byte MatchFidelity { get; set; } 20 | 21 | public required byte BotDifficulty { get; set; } 22 | 23 | public required bool RandomizeBots { get; set; } 24 | 25 | public byte TeamSize => GameType switch 26 | { 27 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_NORMAL => 5, // caldavar 28 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_CASUAL => 5, // caldavar_old 29 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_MIDWARS => 5, // midwars 30 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_RIFTWARS => 5, // riftwars 31 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_CAMPAIGN_NORMAL => 5, // caldavar 32 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_CAMPAIGN_CASUAL => 5, // caldavar_old 33 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_REBORN_NORMAL => 5, // caldavar_reborn 34 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_REBORN_CASUAL => 5, // caldavar_reborn 35 | ChatProtocol.TMMGameType.TMM_GAME_TYPE_MIDWARS_REBORN => 5, // midwars 36 | _ => throw new ArgumentOutOfRangeException(nameof(GameType), $@"Unsupported Game Type: ""{GameType}""") 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupLeaveQueue.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_LEAVE_QUEUE)] 4 | public class GroupLeaveQueue : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | GroupLeaveQueueRequestData requestData = new (buffer); 9 | 10 | MatchmakingGroup? group = MatchmakingService.GetMatchmakingGroup(session.Account.ID); 11 | 12 | if (group is null) 13 | return; 14 | 15 | // Validate That The Group Is Actually Queued 16 | if (group.QueueStartTime is null) 17 | return; 18 | 19 | // Remove Group From Queue 20 | group.QueueStartTime = null; 21 | 22 | // Unready The Group Leader And Unload All Members 23 | // Non-Leader Members Should Always Be Ready So That Group Readiness Is Determined Solely By The Leader 24 | foreach (MatchmakingGroupMember member in group.Members) 25 | { 26 | member.IsReady = member.IsLeader is false; 27 | member.LoadingPercent = 0; 28 | } 29 | 30 | // Broadcast Leave Queue To All Group Members 31 | ChatBuffer leaveQueueBroadcast = new (); 32 | 33 | leaveQueueBroadcast.WriteCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_LEAVE_QUEUE); 34 | 35 | foreach (MatchmakingGroupMember member in group.Members) 36 | member.Session.Send(leaveQueueBroadcast); 37 | 38 | // Send Full Group Update To Reflect New Player States 39 | group.MulticastUpdate(session.Account.ID, ChatProtocol.TMMUpdateType.TMM_FULL_GROUP_UPDATE); 40 | } 41 | } 42 | 43 | public class GroupLeaveQueueRequestData(ChatBuffer buffer) 44 | { 45 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 46 | } 47 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Controllers/ServerRequesterController/ServerRequesterController.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Controllers.ServerRequesterController; 2 | 3 | [ApiController] 4 | [Route("server_requester.php")] 5 | [Consumes("application/x-www-form-urlencoded")] 6 | public partial class ServerRequesterController(MerrickContext databaseContext, IDatabase distributedCache, ILogger logger) : ControllerBase 7 | { 8 | private MerrickContext MerrickContext { get; } = databaseContext; 9 | private IDatabase DistributedCache { get; } = distributedCache; 10 | private ILogger Logger { get; } = logger; 11 | 12 | [HttpPost(Name = "Server Requester All-In-One")] 13 | public async Task ServerRequester() 14 | { 15 | // TODO: Implement Server Requester Controller Cookie Validation 16 | 17 | //if (Cache.ValidateAccountSessionCookie(form.Cookie, out string? _).Equals(false)) 18 | //{ 19 | // Logger.LogWarning($@"IP Address ""{Request.HttpContext.Connection.RemoteIpAddress?.MapToIPv4().ToString() ?? "UNKNOWN"}"" Has Made A Server Controller Request With Forged Cookie ""{form.Cookie}"""); 20 | 21 | // return Unauthorized($@"Unrecognized Cookie ""{form.Cookie}"""); 22 | //} 23 | 24 | return Request.Query["f"].SingleOrDefault() switch 25 | { 26 | // server manager 27 | "replay_auth" => await HandleReplayAuthentication(), 28 | 29 | // server 30 | "accept_key" => await HandleAcceptKey(), 31 | "c_conn" => await HandleConnectClient(), 32 | "new_session" => await HandleNewSession(), 33 | "set_online" => await HandleSetOnline(), 34 | 35 | _ => throw new NotImplementedException($"Unsupported Server Requester Controller Query String Parameter: f={Request.Query["f"].Single()}") 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Internals/UsingDirectives.cs: -------------------------------------------------------------------------------- 1 | global using ASPIRE.Common.Extensions.Cryptography; 2 | global using ASPIRE.Common.ServiceDefaults; 3 | 4 | // TODO: Move These To A Shared Project And Remove Inter-Project Dependencies 5 | global using KONGOR.MasterServer.Extensions.Cache; 6 | global using KONGOR.MasterServer.Models.ServerManagement; 7 | global using KONGOR.MasterServer.Handlers.SRP; 8 | 9 | global using MERRICK.DatabaseContext.Entities.Core; 10 | global using MERRICK.DatabaseContext.Entities.Relational; 11 | global using MERRICK.DatabaseContext.Enumerations; 12 | global using MERRICK.DatabaseContext.Persistence; 13 | 14 | global using Microsoft.EntityFrameworkCore; 15 | global using Microsoft.EntityFrameworkCore.Diagnostics; 16 | global using Microsoft.Extensions.Diagnostics.HealthChecks; 17 | 18 | global using OneOf; 19 | 20 | global using StackExchange.Redis; 21 | 22 | global using System.Collections.Concurrent; 23 | global using System.Diagnostics; 24 | global using System.Net; 25 | global using System.Net.Sockets; 26 | global using System.Reflection; 27 | global using System.Text; 28 | 29 | global using TRANSMUTANSTEIN.ChatServer.Attributes; 30 | global using TRANSMUTANSTEIN.ChatServer.CommandProcessors.Channels; 31 | global using TRANSMUTANSTEIN.ChatServer.CommandProcessors.Connection; 32 | global using TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 33 | global using TRANSMUTANSTEIN.ChatServer.Contracts; 34 | global using TRANSMUTANSTEIN.ChatServer.Core; 35 | global using TRANSMUTANSTEIN.ChatServer.Domain.Communication; 36 | global using TRANSMUTANSTEIN.ChatServer.Domain.Core; 37 | global using TRANSMUTANSTEIN.ChatServer.Domain.Matchmaking; 38 | global using TRANSMUTANSTEIN.ChatServer.Domain.Social; 39 | global using TRANSMUTANSTEIN.ChatServer.Extensions.Protocol; 40 | global using TRANSMUTANSTEIN.ChatServer.Internals; 41 | global using TRANSMUTANSTEIN.ChatServer.Services; 42 | global using TRANSMUTANSTEIN.ChatServer.Utilities; 43 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Services/DatabaseInitializer.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Services; 2 | 3 | public class DatabaseInitializer(IServiceProvider serviceProvider, ILogger logger) : BackgroundService 4 | { 5 | public const string ActivitySourceName = "Migrations"; 6 | 7 | private readonly ActivitySource _activitySource = new (ActivitySourceName); 8 | 9 | protected override async Task ExecuteAsync(CancellationToken cancellationToken) 10 | { 11 | using IServiceScope scope = serviceProvider.CreateScope(); 12 | 13 | MerrickContext context = scope.ServiceProvider.GetRequiredService(); 14 | 15 | await InitializeDatabaseAsync(context, cancellationToken); 16 | } 17 | 18 | private async Task InitializeDatabaseAsync(MerrickContext context, CancellationToken cancellationToken) 19 | { 20 | const string activityName = "Initializing MERRICK Database"; 21 | 22 | using Activity? activity = _activitySource.StartActivity(activityName, ActivityKind.Client); 23 | 24 | Stopwatch stopwatch = Stopwatch.StartNew(); 25 | 26 | IExecutionStrategy strategy = context.Database.CreateExecutionStrategy(); 27 | 28 | await strategy.ExecuteAsync(context.Database.MigrateAsync, cancellationToken); 29 | 30 | await SeedAsync(context, cancellationToken); 31 | 32 | logger.LogInformation("Database Initialization Completed After {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds); 33 | } 34 | 35 | private async Task SeedAsync(MerrickContext context, CancellationToken cancellationToken) 36 | { 37 | logger.LogInformation("Seeding Database"); 38 | 39 | await SeedDataHandlers.SeedUsers(context, cancellationToken, logger); 40 | await SeedDataHandlers.SeedClans(context, cancellationToken, logger); 41 | await SeedDataHandlers.SeedAccounts(context, cancellationToken, logger); 42 | await SeedDataHandlers.SeedHeroGuides(context, cancellationToken, logger); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Matchmaking/GroupCreate.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Matchmaking; 2 | 3 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_CREATE)] 4 | public class GroupCreate : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | GroupCreateRequestData requestData = new (buffer); 9 | 10 | MatchmakingGroup 11 | .Create(session, requestData); 12 | } 13 | } 14 | 15 | public class GroupCreateRequestData(ChatBuffer buffer) 16 | { 17 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 18 | 19 | public string ClientVersion = buffer.ReadString(); 20 | 21 | public ChatProtocol.TMMType GroupType = (ChatProtocol.TMMType) buffer.ReadInt8(); 22 | 23 | public ChatProtocol.TMMGameType GameType = (ChatProtocol.TMMGameType) buffer.ReadInt8(); 24 | 25 | public string MapName = buffer.ReadString(); 26 | 27 | public string[] GameModes = buffer.ReadString().Split('|', StringSplitOptions.RemoveEmptyEntries); 28 | 29 | public string[] GameRegions = buffer.ReadString().Split('|', StringSplitOptions.RemoveEmptyEntries); 30 | 31 | public bool Ranked = buffer.ReadBool(); 32 | 33 | /// 34 | /// 0: Skill Disparity Will Be Higher But The Matchmaking Queue Time Will Be Shorter 35 | ///
36 | /// 1: Skill Disparity Will Be Lower But The Matchmaking Queue Time Will Be Longer 37 | ///
38 | public byte MatchFidelity = buffer.ReadInt8(); 39 | 40 | /// 41 | /// 1: Easy, 2: Medium, 3: Hard 42 | /// 43 | /// 44 | /// Only Used For Bot Matches, But Sent With Every Request To Create A Group 45 | /// 46 | public byte BotDifficulty = buffer.ReadInt8(); 47 | 48 | /// 49 | /// Only Used For Bot Matches, But Sent With Every Request To Create A Group 50 | /// 51 | public bool RandomizeBots = buffer.ReadBool(); 52 | } 53 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Validators/PasswordValidator.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Validators; 2 | 3 | public class PasswordValidator : AbstractValidator 4 | { 5 | private const int MinimumPasswordLength = 8; 6 | private const int MinimumAlphabeticCharactersCount = 2; 7 | private const int MinimumNumericCharactersCount = 2; 8 | private const int MinimumSpecialCharactersCount = 2; 9 | 10 | public PasswordValidator() 11 | { 12 | RuleFor(password => password).NotEmpty().WithMessage("Password Must Not Be Empty"); 13 | 14 | RuleFor(password => password).MinimumLength(MinimumPasswordLength).WithMessage($"Password Must To Be At Least {MinimumPasswordLength} Characters Long"); 15 | 16 | RuleFor(password => password).Must(password => MeetsMinimumAlphabeticCharactersCountRequirement(password, MinimumAlphabeticCharactersCount)) 17 | .WithMessage($"Password Must Contain At Least {MinimumAlphabeticCharactersCount} Alphabetic Characters"); 18 | 19 | RuleFor(password => password).Must(password => MeetsMinimumNumericCharactersCountRequirement(password, MinimumNumericCharactersCount)) 20 | .WithMessage($"Password Must Contain At Least {MinimumNumericCharactersCount} Numeric Characters"); 21 | 22 | RuleFor(password => password).Must(password => MeetsMinimumSpecialCharactersCountRequirement(password, MinimumSpecialCharactersCount)) 23 | .WithMessage($"Password Must Contain At Least {MinimumSpecialCharactersCount} Special Characters"); 24 | } 25 | 26 | private static bool MeetsMinimumAlphabeticCharactersCountRequirement(string password, int minimum) 27 | => password.Count(char.IsLetter) >= minimum; 28 | 29 | private static bool MeetsMinimumNumericCharactersCountRequirement(string password, int minimum) 30 | => password.Count(char.IsNumber) >= minimum; 31 | 32 | private static bool MeetsMinimumSpecialCharactersCountRequirement(string password, int minimum) 33 | => password.Count(character => char.IsLetterOrDigit(character) is false) >= minimum; 34 | } 35 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/Patch/LatestPatchRequestForm.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.Patch; 2 | 3 | public class LatestPatchRequestForm 4 | { 5 | /// 6 | /// Unknown. 7 | /// This value appears to always be "1", perhaps indicating that a patch request was issued. 8 | /// 9 | [FromForm(Name = "update")] 10 | public required string Update { get; set; } 11 | 12 | /// 13 | /// Unknown. 14 | /// This value appears to always be "0.0.0.0". 15 | /// 16 | [FromForm(Name = "version")] 17 | public required string PatchVersion { get; set; } 18 | 19 | /// 20 | /// The HoN client's version, in the format "1.2.3(.4)", sent by the HoN client making the request. 21 | /// 22 | [FromForm(Name = "current_version")] 23 | public required string CurrentPatchVersion { get; set; } 24 | 25 | /// 26 | /// The HoN client's operating system, sent by the HoN client making the request. 27 | /// Generally, (ignoring RCT/SBT/etc.) this will be one of three values: "wac" (Windows client), "lac" (Linux client), or "mac" (macOS client). 28 | /// 29 | [FromForm(Name = "os")] 30 | public required string OperatingSystem { get; set; } 31 | 32 | /// 33 | /// The HoN client's operating system architecture, sent by the HoN client making the request. 34 | /// Generally, this will be one of three values: "x86_64" (Windows client), "x86-biarch" (Linux client), or "universal-64" (macOS client). 35 | /// The 32-bit versions of Windows ("i686") and macOS ("universal"), alongside other legacy architectures, are not supported by Project KONGOR. 36 | /// 37 | [FromForm(Name = "arch")] 38 | public required string Architecture { get; set; } 39 | 40 | /// 41 | /// The HoN client's cookie, used to authorize the session. 42 | /// 43 | [FromForm(Name = "cookie")] 44 | public required string Cookie { get; set; } 45 | } 46 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/GameData/GuideListResponse.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.GameData; 2 | 3 | public class GuideListResponse 4 | { 5 | public GuideListResponse(IEnumerable guides, Account requestingAccount, int hostTime) 6 | { 7 | IEnumerable guidesForResponse = from guide in guides 8 | 9 | // The Featured Guide Is Considered The Default Guide, So It Is Displayed Without An Author Name 10 | let author = guide.Featured ? string.Empty : guide.Author.Clan is null ? guide.Author.Name : $"[{guide.Author.Clan.Tag}]" + guide.Author.Name 11 | 12 | select new StringBuilder().Append($"{guide.ID}").Append('|') 13 | .Append(guide.TimestampCreated.ToString("dd MMMM yyyy HH:mm:ss")).Append('|') 14 | .Append(author).Append('|') 15 | .Append(guide.Name).Append('|') 16 | .Append(new List { "not_def", "is_def" }.First()).Append('|') // TODO: Implement Support For Default Guide 17 | .Append(new List { "not_fav", "is_fav" }.First()).Append('|') // TODO: Implement Support For Favourite Guide 18 | .Append($"{guide.Rating}").Append('|') 19 | .Append(guide.Author?.ID.Equals(requestingAccount.ID) ?? false ? "yours" : "not_yours").Append('|') 20 | .Append(guide.Featured ? 1.ToString() : 0.ToString()) 21 | into guide 22 | 23 | select guide.ToString(); 24 | 25 | GuideList = string.Join('`', guidesForResponse); 26 | 27 | HostTime = hostTime; 28 | } 29 | 30 | [PhpProperty("errors")] 31 | public string Errors => string.Empty; 32 | 33 | [PhpProperty("success")] 34 | public int Success => 1; 35 | 36 | [PhpProperty("guide_list")] 37 | public string GuideList { get; set; } 38 | 39 | [PhpProperty("hosttime")] 40 | public int HostTime { get; set; } 41 | 42 | [PhpProperty("vested_threshold")] 43 | public int VestedThreshold => 5; 44 | 45 | [PhpProperty(0)] 46 | public bool Zero => true; 47 | } 48 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Extensions; 2 | 3 | // TODO: Move To Shared Project To Remove Duplication 4 | 5 | public static class EnumerableExtensions 6 | { 7 | /// 8 | /// Returns TRUE if the collection is empty. Otherwise, returns FALSE. 9 | /// 10 | public static bool None(this IEnumerable collection) 11 | => !collection.Any(); 12 | 13 | /// 14 | /// Returns TRUE if no element in the collection satisfies the condition specified by a predicate. Otherwise, returns FALSE. 15 | /// 16 | public static bool None(this IEnumerable collection, Func predicate) 17 | => !collection.Any(predicate); 18 | 19 | /// 20 | /// Returns TRUE if the collection is empty. Otherwise, returns FALSE. 21 | /// 22 | public static async Task NoneAsync(this IQueryable collection, CancellationToken cancellationToken = default) 23 | => !await collection.AnyAsync(cancellationToken); 24 | 25 | /// 26 | /// Returns TRUE if no element in the collection satisfies the condition specified by a predicate. Otherwise, returns FALSE. 27 | /// 28 | public static async Task NoneAsync(this IQueryable collection, Expression> predicate, CancellationToken cancellationToken = default) 29 | => !await collection.AnyAsync(predicate, cancellationToken); 30 | 31 | /// 32 | /// Returns a new collection composed of the input collection's elements in a random order. 33 | /// 34 | public static IEnumerable Shuffle(this IEnumerable collection) 35 | => collection.OrderBy(element => Random.Shared.Next()); 36 | 37 | /// 38 | /// Returns a random element from the collection. 39 | /// 40 | public static TSource RandomElement(this IEnumerable collection) 41 | => collection.Shuffle().First(); 42 | } 43 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Extensions/Collections/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Extensions.Collections; 2 | 3 | // TODO: Move To Shared Project To Remove Duplication 4 | 5 | public static class EnumerableExtensions 6 | { 7 | /// 8 | /// Returns TRUE if the collection is empty. Otherwise, returns FALSE. 9 | /// 10 | public static bool None(this IEnumerable collection) 11 | => !collection.Any(); 12 | 13 | /// 14 | /// Returns TRUE if no element in the collection satisfies the condition specified by a predicate. Otherwise, returns FALSE. 15 | /// 16 | public static bool None(this IEnumerable collection, Func predicate) 17 | => !collection.Any(predicate); 18 | 19 | /// 20 | /// Returns TRUE if the collection is empty. Otherwise, returns FALSE. 21 | /// 22 | public static async Task NoneAsync(this IQueryable collection, CancellationToken cancellationToken = default) 23 | => !await collection.AnyAsync(cancellationToken); 24 | 25 | /// 26 | /// Returns TRUE if no element in the collection satisfies the condition specified by a predicate. Otherwise, returns FALSE. 27 | /// 28 | public static async Task NoneAsync(this IQueryable collection, Expression> predicate, CancellationToken cancellationToken = default) 29 | => !await collection.AnyAsync(predicate, cancellationToken); 30 | 31 | /// 32 | /// Returns a new collection composed of the input collection's elements in a random order. 33 | /// 34 | public static IEnumerable Shuffle(this IEnumerable collection) 35 | => collection.OrderBy(element => Random.Shared.Next()); 36 | 37 | /// 38 | /// Returns a random element from the collection. 39 | /// 40 | public static TSource RandomElement(this IEnumerable collection) 41 | => collection.Shuffle().First(); 42 | } 43 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Statistics/SeasonStats.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Statistics; 2 | 3 | [ChatCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_CAMPAIGN_STATS)] 4 | public class SeasonStats : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | SeasonStatsRequestData requestData = new (buffer); 9 | 10 | ChatBuffer response = new (); 11 | 12 | response.WriteCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_CAMPAIGN_STATS); 13 | response.WriteFloat32(1850.55f); // TMM Rating 14 | response.WriteInt32(15); // TMM Rank 15 | response.WriteInt32(6661); // TMM Wins 16 | response.WriteInt32(123); // TMM Losses 17 | response.WriteInt32(6662); // Ranked Win Streak 18 | response.WriteInt32(6663); // Ranked Matches Played 19 | response.WriteInt32(5); // Placement Matches Played 20 | response.WriteString("11011"); // Placement Status 21 | response.WriteFloat32(1950.55f); // Casual TMM Rating 22 | response.WriteInt32(10); // Casual TMM Rank 23 | response.WriteInt32(4441); // Casual TMM Wins 24 | response.WriteInt32(321); // Casual TMM Losses 25 | response.WriteInt32(4442); // Casual Ranked Win Streak 26 | response.WriteInt32(4443); // Casual Ranked Matches Played 27 | response.WriteInt32(6); // Casual Placement Matches Played 28 | response.WriteString("010101"); // Casual Placement Status 29 | response.WriteInt8(1); // Eligible For TMM 30 | response.WriteInt8(1); // Season End 31 | 32 | // TODO: Send Actual Season Statistics 33 | 34 | session.Send(response); 35 | 36 | // Also Respond With NET_CHAT_CL_TMM_POPULARITY_UPDATE Since The Client Will Not Explicitly Request It 37 | PopularityUpdate.SendMatchmakingPopularity(session); 38 | } 39 | } 40 | 41 | public class SeasonStatsRequestData(ChatBuffer buffer) 42 | { 43 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 44 | } 45 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/Store/StoreItemDiscountCoupon.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.Store; 2 | 3 | public class StoreItemDiscountCoupon 4 | { 5 | // TODO: Implement This As Part Of The Hero Mastery Feature 6 | 7 | //[PhpProperty("product_id")] 8 | //public int Id { get; set; } 9 | 10 | //[PhpIgnore] 11 | //public string Name { get; set; } 12 | 13 | //[PhpIgnore] 14 | //public string Code => $"cp.{Name}"; 15 | 16 | //[PhpIgnore] 17 | //public string Hero { get; set; } 18 | 19 | //[PhpProperty("coupon_id")] 20 | //// The integer value of the "discount" property part of the form data which gets sent when making a purchase in the in-game store. 21 | //// The "discount" property will be set to this value after choosing to use a discount coupon. 22 | //public int DiscountId => Id; 23 | 24 | //[PhpProperty("coupon_products")] 25 | //public string ApplicableProducts => GetApplicableProducts(); 26 | 27 | //[PhpIgnore] 28 | //public IEnumerable ApplicableProductsList => GetApplicableProductsList(); 29 | 30 | //[PhpProperty("discount")] 31 | //public double DiscountGold => 0.75; 32 | 33 | //[PhpProperty("mmp_discount")] 34 | //public double DiscountSilver => 0.75; 35 | 36 | //[PhpProperty("end_time")] 37 | //public string DiscountExpirationDate => DateTimeOffset.UtcNow.AddYears(1000).ToString("dd MMMM yyyy", CultureInfo.InvariantCulture); 38 | 39 | //private string GetApplicableProducts() 40 | //{ 41 | // IEnumerable avatars = DataSeedHelpers.AllUpgrades 42 | // .Where(upgrade => upgrade.UpgradeType is Upgrade.Type.AlternativeAvatar && upgrade.Code.StartsWith(Hero)) 43 | // .Select(upgrade => upgrade.PrefixedCode); 44 | 45 | // return string.Join(',', avatars); 46 | //} 47 | 48 | //private IEnumerable GetApplicableProductsList() 49 | //{ 50 | // IEnumerable avatars = DataSeedHelpers.AllUpgrades 51 | // .Where(upgrade => upgrade.UpgradeType is Upgrade.Type.AlternativeAvatar && upgrade.Code.StartsWith(Hero)) 52 | // .Select(upgrade => upgrade.PrefixedCode); 53 | 54 | // return avatars; 55 | //} 56 | } 57 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Internals/UsingDirectives.cs: -------------------------------------------------------------------------------- 1 | global using ASPIRE.Common.Communication; 2 | global using ASPIRE.Common.Constants; 3 | global using ASPIRE.Common.Extensions.Cryptography; 4 | global using ASPIRE.Common.ServiceDefaults; 5 | 6 | global using KONGOR.MasterServer.Constants; 7 | global using KONGOR.MasterServer.Extensions.Cache; 8 | global using KONGOR.MasterServer.Extensions.Collections; 9 | global using KONGOR.MasterServer.Handlers.Patch; 10 | global using KONGOR.MasterServer.Handlers.SRP; 11 | global using KONGOR.MasterServer.Models.Configuration; 12 | global using KONGOR.MasterServer.Models.RequestResponse.GameData; 13 | global using KONGOR.MasterServer.Models.RequestResponse.Patch; 14 | global using KONGOR.MasterServer.Models.RequestResponse.ServerManagement; 15 | global using KONGOR.MasterServer.Models.RequestResponse.SRP; 16 | global using KONGOR.MasterServer.Models.RequestResponse.Stats; 17 | global using KONGOR.MasterServer.Models.RequestResponse.Store; 18 | global using KONGOR.MasterServer.Models.ServerManagement; 19 | 20 | global using MERRICK.DatabaseContext.Entities.Core; 21 | global using MERRICK.DatabaseContext.Entities.Game; 22 | global using MERRICK.DatabaseContext.Entities.Relational; 23 | global using MERRICK.DatabaseContext.Entities.Statistics; 24 | global using MERRICK.DatabaseContext.Enumerations; 25 | global using MERRICK.DatabaseContext.Persistence; 26 | 27 | global using Microsoft.AspNetCore.HttpLogging; 28 | global using Microsoft.AspNetCore.Mvc; 29 | global using Microsoft.AspNetCore.RateLimiting; 30 | global using Microsoft.EntityFrameworkCore; 31 | global using Microsoft.EntityFrameworkCore.Diagnostics; 32 | global using Microsoft.Extensions.Caching.Memory; 33 | global using Microsoft.Extensions.FileProviders; 34 | global using Microsoft.Extensions.Options; 35 | global using Microsoft.OpenApi; 36 | 37 | global using OneOf; 38 | 39 | global using PhpSerializerNET; 40 | 41 | global using SecureRemotePassword; 42 | 43 | global using StackExchange.Redis; 44 | 45 | global using System.Linq.Expressions; 46 | global using System.Net; 47 | global using System.Security.Cryptography; 48 | global using System.Text; 49 | global using System.Text.Json; 50 | global using System.Text.RegularExpressions; 51 | global using System.Threading.RateLimiting; 52 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Services/ChatService.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Services; 2 | 3 | public class ChatService(IServiceProvider serviceProvider) : IHostedService, IDisposable 4 | { 5 | public static Domain.Core.ChatServer? ChatServer { get; set; } 6 | 7 | public Task StartAsync(CancellationToken cancellationToken) 8 | { 9 | Log.Initialise(serviceProvider.GetRequiredService>()); 10 | 11 | IPAddress address = IPAddress.Any; 12 | 13 | int port = int.Parse(Environment.GetEnvironmentVariable("CHAT_SERVER_PORT") ?? throw new NullReferenceException("Chat Server Port Is NULL")); 14 | 15 | ChatServer = new Domain.Core.ChatServer(address, port, serviceProvider); 16 | 17 | if (ChatServer.Start() is false) 18 | { 19 | // TODO: Log Critical Event 20 | 21 | return Task.FromException(new ApplicationException("Chat Server Was Unable To Start")); 22 | } 23 | 24 | Log.Information("Chat Server Listening On {ChatServer.Endpoint}", ChatServer.Endpoint); 25 | 26 | return Task.CompletedTask; 27 | } 28 | 29 | public Task StopAsync(CancellationToken cancellationToken) 30 | { 31 | if (ChatServer is null) 32 | { 33 | // TODO: Log Bug 34 | 35 | return Task.FromException(new ApplicationException("Chat Server Is NULL")); 36 | } 37 | 38 | if (ChatServer.IsStarted) 39 | { 40 | ChatServer.DisconnectAll(); 41 | ChatServer.Stop(); 42 | } 43 | 44 | else 45 | { 46 | // TODO: Log Bug 47 | 48 | return Task.FromException(new ApplicationException("Chat Server Is Not Running")); 49 | } 50 | 51 | Log.Information("Chat Server Has Stopped"); 52 | 53 | return Task.CompletedTask; 54 | } 55 | 56 | public void Dispose() 57 | { 58 | if (ChatServer is null) 59 | { 60 | // TODO: Log Bug 61 | 62 | throw new ApplicationException("Chat Server Is NULL"); 63 | } 64 | 65 | if (ChatServer.IsDisposed) 66 | { 67 | // TODO: Log Bug 68 | 69 | throw new ApplicationException("Chat Server Is Already Disposed"); 70 | } 71 | 72 | ChatServer.Dispose(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Helpers/EmailAddressHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Helpers; 2 | 3 | public static class EmailAddressHelpers 4 | { 5 | public static IActionResult SanitizeEmailAddress(string email) 6 | { 7 | if (ZORGATH.RunsInDevelopmentMode is false) 8 | { 9 | if (email.Split('@').First().Contains('+')) 10 | return new BadRequestObjectResult(@"Alias Creating Character ""+"" Is Not Allowed"); 11 | 12 | string[] allowedEmailProviders = Enumerable.Empty() 13 | .Concat(new [] { "outlook", "hotmail", "live", "msn" }) // Microsoft Outlook 14 | .Concat(new [] { "protonmail", "proton" }) // Proton Mail 15 | .Concat(new [] { "gmail", "googlemail" }) // Google Mail 16 | .Concat(new [] { "yahoo", "rocketmail", "ymail" }) // Yahoo Mail 17 | .Concat(new [] { "aol", "yandex", "gmx", "mail" }) // AOL Mail, Yandex Mail, GMX Mail, mail.com 18 | .Concat(new [] { "icloud", "me", "mac" }) // iCloud Mail 19 | .ToArray(); 20 | 21 | Regex pattern = new (@"^(?[a-zA-Z0-9_\-.]+)@(?[a-zA-Z]+)\.(?[a-zA-Z]{1,3}|co.uk)$"); 22 | 23 | Match match = pattern.Match(email); 24 | 25 | if (match.Success.Equals(false)) 26 | return new BadRequestObjectResult($@"Email Address ""{email}"" Is Not Valid"); 27 | 28 | string local = match.Groups["local"].Value; 29 | string domain = match.Groups["domain"].Value; 30 | string tld = match.Groups["tld"].Value; 31 | 32 | if (allowedEmailProviders.Contains(domain).Equals(false)) 33 | return new BadRequestObjectResult($@"Email Address Provider ""{domain}"" Is Not Allowed"); 34 | 35 | // These Email Providers Ignore Period Characters 36 | // Users Can Create Aliases With The Same Email Address By Simply Adding Some Period Characters To The Local Part 37 | if (domain is "protonmail" or "proton" or "gmail" or "googlemail") 38 | local = local.Replace(".", string.Empty); 39 | 40 | email = $"{local}@{domain}.{tld}"; 41 | } 42 | 43 | return new ContentResult 44 | { 45 | Content = email 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Handlers/SRPRegistrationHandlers.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Handlers; 2 | 3 | public static class SRPRegistrationHandlers 4 | { 5 | # region Secure Remote Password Magic Strings 6 | 7 | // Thank you, Anton Romanov (aka Theli), for making these values public: https://github.com/theli-ua/pyHoNBot/blob/cabde31b8601c1ca55dc10fcf663ec663ec0eb71/hon/masterserver.py#L37. 8 | // The first magic string is also present in the k2_x64 DLL of the Windows client, between offsets 0xF2F4D0 and 0xF2F4D0. 9 | // It is not clear how the second magic string was obtained. 10 | // Project KONGOR would have not been possible without having these values in the public domain. 11 | 12 | private const string MagicStringOne = "[!~esTo0}"; 13 | private const string MagicStringTwo = "taquzaph_?98phab&junaj=z=kuChusu"; 14 | 15 | # endregion 16 | 17 | /// 18 | /// Generates a 64-character long SHA256 hash of the account's password. 19 | /// The uppercase hashes (C# default) in this method need to be lowercased, to match the lowercase hashes (C++ default) that the game client generates. 20 | ///
21 | /// The expectation is that the password is not hashed for SRP registration, but is hashed for SRP authentication. 22 | ///
23 | public static string ComputeSRPPasswordHash(string password, string salt, bool passwordIsHashed = false) 24 | { 25 | string passwordHash = passwordIsHashed ? password : Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(password))).ToLower(); 26 | 27 | string magickedPasswordHash = passwordHash + salt + MagicStringOne; 28 | 29 | string magickedPasswordHashHash = Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(magickedPasswordHash))).ToLower(); 30 | 31 | string magickedMagickedPasswordHashHash = magickedPasswordHashHash + MagicStringTwo; 32 | 33 | return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(magickedMagickedPasswordHashHash))).ToLower(); 34 | } 35 | 36 | /// 37 | /// Generates a 64-character long SRP password salt. 38 | /// The value needs to be divided by 2, because there are 2 hexadecimal digits per byte. 39 | /// 40 | public static string GenerateSRPPasswordSalt() 41 | => SrpInteger.RandomInteger(64 / 2).ToHex(); 42 | } 43 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/GameData/GuideResponseSuccess.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.GameData; 2 | 3 | public class GuideResponseSuccess(HeroGuide guide, int hostTime) 4 | { 5 | [PhpProperty("errors")] 6 | public string Errors => string.Empty; 7 | 8 | [PhpProperty("success")] 9 | public int Success => 1; 10 | 11 | [PhpProperty("datetime")] 12 | public DateTimeOffset TimestampCreated { get; set; } = guide.TimestampCreated; 13 | 14 | [PhpProperty("author_name")] 15 | public string AuthorName { get; set; } = guide.Author.Name; 16 | 17 | [PhpProperty("hero_cli_name")] 18 | public string HeroIdentifier { get; set; } = guide.HeroIdentifier; 19 | 20 | [PhpProperty("guide_name")] 21 | public string Name { get; set; } = guide.Name; 22 | 23 | [PhpProperty("hero_name")] 24 | public string HeroName { get; set; } = guide.HeroName; 25 | 26 | [PhpProperty("default")] 27 | public int Default => 0; 28 | 29 | [PhpProperty("favorite")] 30 | public int Favorite => 0; 31 | 32 | [PhpProperty("rating")] 33 | public float Rating { get; set; } = guide.Rating; 34 | 35 | [PhpProperty("thumb")] 36 | public string Thumb => guide.UpVotes is 0 ? "noVote" : guide.UpVotes.ToString(); 37 | 38 | [PhpProperty("premium")] 39 | public bool Featured { get; set; } = guide.Featured; 40 | 41 | [PhpProperty("i_start")] 42 | public string StartingItems { get; set; } = guide.StartingItems; 43 | 44 | [PhpProperty("i_laning")] 45 | public string EarlyGameItems { get; set; } = guide.EarlyGameItems; 46 | 47 | [PhpProperty("i_core")] 48 | public string CoreItems { get; set; } = guide.CoreItems; 49 | 50 | [PhpProperty("i_luxury")] 51 | public string LuxuryItems { get; set; } = guide.LuxuryItems; 52 | 53 | [PhpProperty("abilQ")] 54 | public string AbilityQueue { get; set; } = guide.AbilityQueue; 55 | 56 | [PhpProperty("txt_intro")] 57 | public string Intro { get; set; } = guide.Intro; 58 | 59 | [PhpProperty("txt_guide")] 60 | public string Content { get; set; } = guide.Content; 61 | 62 | [PhpProperty("hosttime")] 63 | public int HostTime { get; set; } = hostTime; 64 | 65 | [PhpProperty("vested_threshold")] 66 | public int VestedThreshold => 5; 67 | 68 | [PhpProperty(0)] 69 | public bool Zero => true; 70 | } 71 | -------------------------------------------------------------------------------- /ASPIRE.Tests/Internals/UsingDirectives.cs: -------------------------------------------------------------------------------- 1 | global using Aspire.Hosting.Testing; 2 | global using Aspire.Hosting; 3 | 4 | global using ASPIRE.SourceGenerator.Attributes; 5 | 6 | global using ASPIRE.Tests.KONGOR.MasterServer.Infrastructure; 7 | global using ASPIRE.Tests.KONGOR.MasterServer.Models; 8 | global using ASPIRE.Tests.KONGOR.MasterServer.Services; 9 | 10 | global using ASPIRE.Tests.ZORGATH.WebPortal.API.Infrastructure; 11 | global using ASPIRE.Tests.ZORGATH.WebPortal.API.Models; 12 | global using ASPIRE.Tests.ZORGATH.WebPortal.API.Services; 13 | 14 | global using KONGOR.MasterServer.Handlers.SRP; 15 | global using KONGOR.MasterServer.Internals; 16 | global using KONGOR.MasterServer.Models.RequestResponse.SRP; 17 | 18 | global using MERRICK.DatabaseContext.Constants; 19 | global using MERRICK.DatabaseContext.Entities.Core; 20 | global using MERRICK.DatabaseContext.Entities.Utility; 21 | global using MERRICK.DatabaseContext.Enumerations; 22 | global using MERRICK.DatabaseContext.Helpers; 23 | global using MERRICK.DatabaseContext.Persistence; 24 | 25 | global using Microsoft.AspNetCore.Builder; 26 | global using Microsoft.AspNetCore.Hosting; 27 | global using Microsoft.AspNetCore.Identity; 28 | global using Microsoft.AspNetCore.Mvc.Testing; 29 | global using Microsoft.AspNetCore.Mvc; 30 | global using Microsoft.EntityFrameworkCore; 31 | global using Microsoft.Extensions.Caching.Distributed; 32 | global using Microsoft.Extensions.DependencyInjection; 33 | global using Microsoft.Extensions.Logging; 34 | global using Microsoft.Extensions.Options; 35 | global using Microsoft.IdentityModel.Tokens; 36 | 37 | global using PhpSerializerNET; 38 | 39 | global using SecureRemotePassword; 40 | 41 | global using StackExchange.Redis; 42 | 43 | global using System.Collections.Concurrent; 44 | global using System.IdentityModel.Tokens.Jwt; 45 | global using System.Text.RegularExpressions; 46 | global using System.Text; 47 | 48 | global using TUnit.Assertions.Extensions; 49 | global using TUnit.Assertions; 50 | global using TUnit.Core; 51 | 52 | global using ZORGATH.WebPortal.API.Contracts; 53 | global using ZORGATH.WebPortal.API.Controllers; 54 | global using ZORGATH.WebPortal.API.Extensions; 55 | global using ZORGATH.WebPortal.API.Handlers; 56 | global using ZORGATH.WebPortal.API.Internals; 57 | global using ZORGATH.WebPortal.API.Models.Configuration; 58 | global using ZORGATH.WebPortal.API.Services.Email; 59 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Handlers/Patch/PatchHandlers.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Handlers.Patch; 2 | 3 | public static class PatchHandlers 4 | { 5 | public static PatchDetails GetLatestClientPatchDetails(string distribution) 6 | => DistributionVersions.Single(details => details.DistributionIdentifier.Equals(distribution) && details.Latest.Equals(true)); 7 | 8 | public static PatchDetails GetClientPatchDetails(string distribution, string version) 9 | => DistributionVersions.Single(details => details.DistributionIdentifier.Equals(distribution) && details.Version.Equals(version)); 10 | 11 | private static List DistributionVersions { get; set; } = 12 | [ 13 | new PatchDetails() 14 | { 15 | DistributionIdentifier = "wac", 16 | Version = "4.10.1", 17 | FullVersion = "4.10.1.0", 18 | ManifestArchiveSHA1Hash = "33b5151fca1704aff892cf76e41f3986634d38bb", 19 | ManifestArchiveSizeInBytes = "3628533", 20 | Latest = true 21 | }, 22 | new PatchDetails() 23 | { 24 | DistributionIdentifier = "lac", 25 | Version = "4.10.1", 26 | FullVersion = "4.10.1.0", 27 | ManifestArchiveSHA1Hash = "3977f63f62954e06038c34572a00656a8cb7e311", 28 | ManifestArchiveSizeInBytes = "6122296", 29 | Latest = true 30 | }, 31 | new PatchDetails() 32 | { 33 | DistributionIdentifier = "mac", 34 | Version = "4.10.1", 35 | FullVersion = "4.10.1.0", 36 | ManifestArchiveSHA1Hash = "3933009", 37 | ManifestArchiveSizeInBytes = "ce18408b94a14a968736bb39213daac9a09e4026", 38 | Latest = true 39 | }, 40 | new PatchDetails() 41 | { 42 | DistributionIdentifier = "was-crIac6LASwoafrl8FrOa", 43 | Version = "4.10.1", 44 | FullVersion = "4.10.1.0", 45 | ManifestArchiveSHA1Hash = "37fc03fc781925e00ae633f8855f3bbd2996c5e7", 46 | ManifestArchiveSizeInBytes = "1507915", 47 | Latest = true 48 | }, 49 | new PatchDetails() 50 | { 51 | DistributionIdentifier = "las-crIac6LASwoafrl8FrOa", 52 | Version = "4.10.1", 53 | FullVersion = "4.10.1.0", 54 | ManifestArchiveSHA1Hash = "8ba887c3de95b0b0d33b461bdb4721f29ace952e", 55 | ManifestArchiveSizeInBytes = "2705984", 56 | Latest = true 57 | }, 58 | ]; 59 | } 60 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Channels/SendChannelMessage.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Channels; 2 | 3 | [ChatCommand(ChatProtocol.Command.CHAT_CMD_CHANNEL_MSG)] 4 | public class SendChannelMessage(FloodPreventionService floodPreventionService) : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | SendChannelMessageRequestData requestData = new (buffer); 9 | 10 | ChatChannel channel = ChatChannel.Get(session, requestData.ChannelID); 11 | 12 | // Check Flood Prevention (Service Handles Both Check And Response) 13 | if (floodPreventionService.CheckAndHandleFloodPrevention(session) is false) 14 | { 15 | return; 16 | } 17 | 18 | // Check If The Sender Is Silenced In This Channel 19 | if (channel.IsSilenced(session)) 20 | { 21 | ChatBuffer response = new (); 22 | 23 | response.WriteCommand(ChatProtocol.Command.CHAT_CMD_CHANNEL_SILENCED); 24 | response.WriteInt32(requestData.ChannelID); // Channel ID 25 | 26 | session.Send(response); 27 | 28 | return; 29 | } 30 | 31 | string messageContent = requestData.Message; 32 | 33 | // Enforce Message Content Length Limit 34 | // Staff Accounts Are Exempt From Message Length Restrictions, For Moderation And Administration Purposes 35 | if (session.Account.Type is not AccountType.Staff && messageContent.Length > ChatProtocol.CHAT_MESSAGE_MAX_LENGTH) 36 | { 37 | messageContent = messageContent[.. ChatProtocol.CHAT_MESSAGE_MAX_LENGTH]; 38 | 39 | // TODO: Notify The Sender That Their Message Was Truncated 40 | } 41 | 42 | // Broadcast The Message To All Channel Members 43 | ChatBuffer broadcast = new (); 44 | 45 | broadcast.WriteCommand(ChatProtocol.Command.CHAT_CMD_CHANNEL_MSG); 46 | broadcast.WriteInt32(session.Account.ID); // Sender Account ID 47 | broadcast.WriteInt32(requestData.ChannelID); // Channel ID 48 | broadcast.WriteString(messageContent); // Message Content (Potentially Truncated) 49 | 50 | channel.BroadcastMessage(broadcast); 51 | } 52 | } 53 | 54 | public class SendChannelMessageRequestData(ChatBuffer buffer) 55 | { 56 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 57 | 58 | public string Message = buffer.ReadString(); 59 | 60 | public int ChannelID = buffer.ReadInt32(); 61 | } 62 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Services/Email/EmailService.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Services.Email; 2 | 3 | // TODO: Implement Secret Management Component 4 | // TODO: Implement Real Email Service 5 | 6 | public class EmailService(IOptions configuration, ILogger logger) : IEmailService 7 | { 8 | private OperationalConfiguration Configuration { get; } = configuration.Value; 9 | private ILogger Logger { get; } = logger; 10 | 11 | private string BaseURL { get; } = ZORGATH.RunsInDevelopmentMode is true ? "https://localhost:55510" : "https://portal.api.kongor.net"; 12 | 13 | public async Task SendEmailAddressRegistrationLink(string emailAddress, string token) 14 | { 15 | string link = BaseURL + "/register/" + token; 16 | 17 | const string subject = "Verify Email Address"; 18 | 19 | string body = "You need to verify your email address before you can create your Heroes Of Newerth account." 20 | + Environment.NewLine + "Please follow the link below to continue:" 21 | + Environment.NewLine + Environment.NewLine + link 22 | + Environment.NewLine + Environment.NewLine + "Regards," 23 | + Environment.NewLine + "The Project KONGOR Team"; 24 | 25 | Console.WriteLine(body); 26 | 27 | // TODO: Add "try/catch" Block And Return "false" On Failure 28 | 29 | await Task.Delay(250); return true; 30 | } 31 | 32 | public async Task SendEmailAddressRegistrationConfirmation(string emailAddress, string accountName) 33 | { 34 | const string subject = "Email Address Verified"; 35 | 36 | string body = $"Hi {accountName}," 37 | + Environment.NewLine + Environment.NewLine + "Congratulations on verifying the email address linked to your Heroes Of Newerth account." 38 | + " " + "Please remember to be respectful to your fellow Newerthians, and to maintain your account in good standing." 39 | + " " + "Suspensions carry over across accounts so, if you receive a suspension, you will not be able to log back into the game by creating a new account." 40 | + Environment.NewLine + Environment.NewLine + "Regards," 41 | + Environment.NewLine + "The Project KONGOR Team"; 42 | 43 | Console.WriteLine(body); 44 | 45 | // TODO: Add "try/catch" Block And Return "false" On Failure 46 | 47 | await Task.Delay(250); return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Statistics/MatchStatistics.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Statistics; 2 | 3 | [Index(nameof(MatchID), IsUnique = true)] 4 | public class MatchStatistics 5 | { 6 | [Key] 7 | public int ID { get; set; } 8 | 9 | public required long ServerID { get; set; } 10 | 11 | [MaxLength(15)] 12 | public required string HostAccountName { get; set; } 13 | 14 | public required int MatchID { get; set; } 15 | 16 | public required string Map { get; set; } 17 | 18 | [MaxLength(15)] 19 | public required string MapVersion { get; set; } 20 | 21 | public required int TimePlayed { get; set; } 22 | 23 | public required int FileSize { get; set; } 24 | 25 | public required string FileName { get; set; } 26 | 27 | public required int ConnectionState { get; set; } 28 | 29 | public required string Version { get; set; } 30 | 31 | public required int AveragePSR { get; set; } 32 | 33 | public required int AveragePSRTeamOne { get; set; } 34 | 35 | public required int AveragePSRTeamTwo { get; set; } 36 | 37 | // TODO: MMR And Casual MMR May Need To Also Be Added Here 38 | // TODO: PSR And (Casual) MMR Should Default To The Data Type Default Value If Not Provided 39 | 40 | public required string GameMode { get; set; } 41 | 42 | public required int ScoreTeam1 { get; set; } 43 | 44 | public required int ScoreTeam2 { get; set; } 45 | 46 | public required int TeamScoreGoal { get; set; } 47 | 48 | public required int PlayerScoreGoal { get; set; } 49 | 50 | public required int NumberOfRounds { get; set; } 51 | 52 | public required string ReleaseStage { get; set; } 53 | 54 | public required string BannedHeroes { get; set; } 55 | 56 | public required int AwardMostAnnihilations { get; set; } 57 | 58 | public required int AwardMostQuadKills { get; set; } 59 | 60 | public required int AwardLargestKillStreak { get; set; } 61 | 62 | public required int AwardMostSmackdowns { get; set; } 63 | 64 | public required int AwardMostKills { get; set; } 65 | 66 | public required int AwardMostAssists { get; set; } 67 | 68 | public required int AwardLeastDeaths { get; set; } 69 | 70 | public required int AwardMostBuildingDamage { get; set; } 71 | 72 | public required int AwardMostWardsKilled { get; set; } 73 | 74 | public required int AwardMostHeroDamageDealt { get; set; } 75 | 76 | public required int AwardHighestCreepScore { get; set; } 77 | 78 | public required string SubmissionDebug { get; set; } 79 | } 80 | -------------------------------------------------------------------------------- /ASPIRE.Tests/ZORGATH.WebPortal.API/Infrastructure/ZORGATHServiceProvider.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Tests.ZORGATH.WebPortal.API.Infrastructure; 2 | 3 | /// 4 | /// Provides Test Dependencies For ZORGATH Web Portal API Tests 5 | /// 6 | public static class ZORGATHServiceProvider 7 | { 8 | /// 9 | /// Creates An Instance Of The ZORGATH Web Portal API With In-Memory Dependencies 10 | /// 11 | public static WebApplicationFactory CreateOrchestratedInstance(string? identifier = null) 12 | { 13 | string databaseName = identifier ?? Guid.CreateVersion7().ToString(); 14 | 15 | // Replace Database Context And Distributed Cache With In-Memory Implementations 16 | WebApplicationFactory webApplicationFactory = new WebApplicationFactory().WithWebHostBuilder(builder => builder.ConfigureServices(services => 17 | { 18 | Func databaseContextPredicate = descriptor => 19 | descriptor.ServiceType.FullName?.Contains(nameof(MerrickContext)) is true || descriptor.ImplementationType?.FullName?.Contains(nameof(MerrickContext)) is true; 20 | 21 | // Remove MerrickContext Registration 22 | foreach (ServiceDescriptor? descriptor in services.Where(databaseContextPredicate).ToList()) 23 | services.Remove(descriptor); 24 | 25 | // Register In-Memory MerrickContext 26 | services.AddDbContext(options => options.UseInMemoryDatabase(databaseName).EnableServiceProviderCaching(false), 27 | ServiceLifetime.Singleton, ServiceLifetime.Singleton); 28 | 29 | Func distributedCachePredicate = descriptor => 30 | descriptor.ServiceType == typeof(IConnectionMultiplexer) || descriptor.ServiceType == typeof(IDatabase); 31 | 32 | // Remove IConnectionMultiplexer And IDatabase Registrations 33 | foreach (ServiceDescriptor? descriptor in services.Where(distributedCachePredicate).ToList()) 34 | services.Remove(descriptor); 35 | 36 | // Register In-Process Distributed Cache Database 37 | services.AddSingleton(); 38 | })); 39 | 40 | // Ensure That OnModelCreating From MerrickContext Has Been Called 41 | webApplicationFactory.Services.GetRequiredService().Database.EnsureCreated(); 42 | 43 | return webApplicationFactory; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Connection/ServerHandshake.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Connection; 2 | 3 | [ChatCommand(ChatProtocol.GameServerToChatServer.NET_CHAT_GS_CONNECT)] 4 | public class ServerHandshake : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | ServerHandshakeRequestData requestData = new (buffer); 9 | 10 | // TODO: Check Cookie 11 | 12 | // TODO: Run Other Checks 13 | 14 | /* 15 | // Must update session cookie first so that if we call Disconnect(), remote command would execute properly. 16 | Subject.SessionCookie = SessionCookie; 17 | 18 | if (Server is null) 19 | { 20 | Console.WriteLine("Rejected server chat connection because could not validate the cookie {0}", SessionCookie); 21 | 22 | // Close the server so that it re-obtains a new cookie. 23 | Subject.SendResponse(new RemoteCommandResponse(SessionCookie, "quit")); 24 | Subject.Disconnect("Failed to resolve a server with provided Id and SessionCookie."); 25 | return; 26 | } 27 | 28 | if (ServerAccountType != AccountType.RankedMatchHost && ServerAccountType != AccountType.UnrankedMatchHost) 29 | { 30 | Console.WriteLine("Rejected server chat connection because account doens't have the hosting permissions for cookie {0}", SessionCookie); 31 | Subject.Disconnect("The provided account is not allowed to host games, please contact support if you believe this is wrong."); 32 | return; 33 | } 34 | 35 | ConnectedServer? previousInstance = KongorContext.ConnectedServers.SingleOrDefault(s => s.Address == Server.Address && s.Port == Server.Port); 36 | if (previousInstance != null) 37 | { 38 | previousInstance.Disconnect("A new game server instance with an identical address and port has replaced this instance."); 39 | } 40 | */ 41 | 42 | ChatBuffer response = new (); 43 | 44 | response.WriteCommand(ChatProtocol.ChatServerToGameServer.NET_CHAT_GS_ACCEPT); 45 | 46 | session.Send(response); 47 | } 48 | } 49 | 50 | public class ServerHandshakeRequestData(ChatBuffer buffer) 51 | { 52 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 53 | 54 | public int ServerID = buffer.ReadInt32(); 55 | 56 | public string SessionCookie = buffer.ReadString(); 57 | 58 | public int ChatProtocolVersion = buffer.ReadInt32(); 59 | } 60 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/SRP/SRPAuthenticationSessionDataStageTwo.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.SRP; 2 | 3 | /// 4 | /// Exposes the constants, properties, and methods required for Secure Remote Password protocol authentication, stage two. 5 | /// 6 | public class SRPAuthenticationSessionDataStageTwo 7 | { 8 | public SRPAuthenticationSessionDataStageTwo(SRPAuthenticationSessionDataStageOne stageOneData, string clientProof) 9 | { 10 | ClientProof = clientProof; 11 | 12 | SrpParameters parameters = SrpParameters.Create(SRPAuthenticationSessionDataStageOne.SafePrimeNumber, SRPAuthenticationSessionDataStageOne.MultiplicativeGroupGenerator); 13 | 14 | // HoN SRP requires a padded "g" (multiplicative group generator) value for its final "M2" (server proof) calculation. 15 | // The RFC5054 specification is unclear on whether this should be done or not. 16 | // It is done by default in the Python "cocagne/pysrp" library which Anton Romanov used, but not in the C# "secure-remote-password/srp.net" library which this solution uses. 17 | // cocagne/pysrp : https://github.com/cocagne/pysrp/blob/0414166e9dba63c2677414ace2673ccc24208d23/srp/_pysrp.py#L205-L208 18 | // secure-remote-password/srp.net : https://github.com/secure-remote-password/srp.net/blob/176098e90501659990b12e8ac086018d47f23ccb/src/srp/SrpParameters.cs#L29 19 | parameters.Generator = parameters.Pad(parameters.Generator); 20 | 21 | SrpServer server = new (parameters); 22 | 23 | try 24 | { 25 | SrpSession serverSession = server.DeriveSession 26 | ( 27 | stageOneData.ServerPrivateEphemeral, 28 | stageOneData.ClientPublicEphemeral, 29 | stageOneData.SessionSalt, 30 | stageOneData.LoginIdentifier, 31 | stageOneData.Verifier, 32 | clientProof 33 | ); 34 | 35 | ServerProof = serverSession.Proof; 36 | } 37 | 38 | catch 39 | { 40 | ServerProof = null; 41 | } 42 | } 43 | 44 | // TODO: Add SRP Tests 45 | 46 | /// 47 | /// M1 : the client's proof; the server should verify this value and use it to compute M2 (the server's proof) 48 | /// 49 | public string ClientProof { get; init; } 50 | 51 | /// 52 | /// M2 : the server's proof; the client should verify this value and use it to complete the SRP challenge exchange 53 | /// 54 | public string? ServerProof { get; init; } 55 | } 56 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Controllers/StatsRequesterController/StatsRequesterController.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Controllers.StatsRequesterController; 2 | 3 | [ApiController] 4 | [Route("stats_requester.php")] 5 | [Consumes("application/x-www-form-urlencoded")] 6 | public partial class StatsRequesterController(MerrickContext databaseContext, IDatabase distributedCache, ILogger logger) : ControllerBase 7 | { 8 | private MerrickContext MerrickContext { get; } = databaseContext; 9 | private IDatabase DistributedCache { get; } = distributedCache; 10 | private ILogger Logger { get; } = logger; 11 | 12 | /// 13 | /// 14 | /// The stats resubmission key is the SHA1 hash of the match ID prepended to this salt. 15 | /// 16 | /// 17 | /// StatsResubmissionKey = SHA1.HashData(Encoding.UTF8.GetBytes(matchID + MatchStatsSubmissionSalt)); 18 | /// 19 | /// 20 | /// This key can be found at offset 0x00F03A10 in k2_x64.dll of the WAS distribution. 21 | /// 22 | /// 23 | private static string MatchStatsSubmissionSalt => "s8c7xaduxAbRanaspUf3kadRachecrac9efeyupr8suwrewecrUphayeweqUmana"; 24 | 25 | // For Debugging Purposes, The "GiveGold" And "GiveExp" Commands (Case-Insensitive) Can Be Used From The Server Console To Complete Matches Quickly And Send Stats 26 | // e.g. #1: "givegold 0 65535" To Give 65535 Gold To Player Index 0 (The First Player), Or "givegold KONGOR 65535" To Give 65535 Gold To Player With Name "KONGOR" 27 | // e.g. #2: "giveexp KONGOR 65535" To Give 65535 Experience To Player With Name "KONGOR" (Unlike The "GiveGold" Command, The "GiveExp" Command Does Not Work With A Player Index) 28 | // NOTE #1: 1v1 Matches Are A Good Way To Test The Stats Submission System, As They Are The Quickest To Complete 29 | // NOTE #2: Another Quick Way To Test The Stats Submission System Is To Replay A Fiddler/Requestly/etc. Request Or Make A Postman/Insomnia/etc. Request With The Required Form Data 30 | 31 | [HttpPost(Name = "Stats Requester All-In-One")] 32 | public async Task StatsRequester([FromForm] StatsForSubmissionRequestForm form) 33 | { 34 | return Request.Form["f"].SingleOrDefault() switch 35 | { 36 | "submit_stats" => await HandleStatsSubmission(form), 37 | "resubmit_stats" => await HandleStatsResubmission(form), 38 | 39 | _ => throw new NotImplementedException($"Unsupported Stats Requester Controller Form Parameter: f={Request.Form["f"].Single()}") 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/CommandProcessors/Connection/ServerManagerHandshake.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.CommandProcessors.Connection; 2 | 3 | [ChatCommand(ChatProtocol.ServerManagerToChatServer.NET_CHAT_SM_CONNECT)] 4 | public class ServerManagerHandshake : ISynchronousCommandProcessor 5 | { 6 | public void Process(ChatSession session, ChatBuffer buffer) 7 | { 8 | ServerManagerHandshakeRequestData requestData = new (buffer); 9 | 10 | // TODO: Check Cookie 11 | 12 | // TODO: Run Other Checks 13 | 14 | /* 15 | if (Manager is null) 16 | { 17 | Subject.Disconnect("Failed to resolve a manager with provided Id and SessionCookie."); 18 | return; 19 | } 20 | 21 | lock (SharedContext.ConnectedManagers) 22 | { 23 | // Disconnect previously connected manager if it's ran twice by mistake. 24 | if (SharedContext.ConnectedManagers.TryGetValue(ManagerId, out var conflictingManager)) 25 | { 26 | conflictingManager.Disconnect("Another manager instance has replaced this manager instance."); 27 | } 28 | 29 | SharedContext.ConnectedManagers[ManagerId] = Subject; 30 | } 31 | */ 32 | 33 | ChatBuffer response = new (); 34 | 35 | response.WriteCommand(ChatProtocol.ChatServerToServerManager.NET_CHAT_SM_ACCEPT); 36 | 37 | session.Send(response); 38 | 39 | // TODO: Don't Reuse Chat Buffer Instance 40 | 41 | response = new ChatBuffer(); // Also Respond With NET_CHAT_SM_OPTIONS Since The Server Manager Will Not Explicitly Request It 42 | 43 | response.WriteCommand(ChatProtocol.ChatServerToServerManager.NET_CHAT_SM_OPTIONS); 44 | response.WriteInt8(Convert.ToByte(true)); // Submit Stats Enabled 45 | response.WriteInt8(Convert.ToByte(true)); // Upload Replays Enabled 46 | response.WriteInt8(Convert.ToByte(false)); // Upload To FTP On Demand Enabled 47 | response.WriteInt8(Convert.ToByte(true)); // Upload To HTTP On Demand Enabled 48 | response.WriteInt8(Convert.ToByte(true)); // Resubmit Stats Enabled 49 | // TODO: Investigate What This (Stats Resubmit Match ID Cut-Off) Is 50 | response.WriteInt32(1); // Stats Resubmit Match ID Cut-Off 51 | 52 | session.Send(response); 53 | } 54 | } 55 | 56 | public class ServerManagerHandshakeRequestData(ChatBuffer buffer) 57 | { 58 | public byte[] CommandBytes = buffer.ReadCommandBytes(); 59 | 60 | public int ServerManagerID = buffer.ReadInt32(); 61 | 62 | public string SessionCookie = buffer.ReadString(); 63 | 64 | public int ChatProtocolVersion = buffer.ReadInt32(); 65 | } 66 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Extensions/Cache/DistributedCacheExtensions.Friends.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Extensions.Cache; 2 | 3 | public static partial class DistributedCacheExtensions 4 | { 5 | private static string ConstructFriendRequestKey(int requesterID, int targetID) => $@"FRIEND-REQUEST:[""{requesterID}:{targetID}""]"; 6 | 7 | /// 8 | /// Stores a pending friend request in the distributed cache store. 9 | /// The value of this entry is the requester's and target's notification IDs as a tuple. 10 | /// 11 | public static async Task SetFriendRequest(this IDatabase distributedCacheStore, int requesterID, int targetID, int requesterNotificationID, int targetNotificationID) 12 | { 13 | string notificationIDsPair = $"{requesterNotificationID}:{targetNotificationID}"; 14 | 15 | await distributedCacheStore.StringSetAsync(ConstructFriendRequestKey(requesterID, targetID), notificationIDsPair, TimeSpan.FromHours(24)); 16 | } 17 | 18 | /// 19 | /// Retrieves a pending friend request from the distributed cache store. 20 | /// Returns a tuple containing the requester's and target's notification IDs, or NULL if the request doesn't exist or has expired. 21 | /// 22 | public static async Task<(int RequesterNotificationID, int TargetNotificationID)?> GetFriendRequest(this IDatabase distributedCacheStore, int requesterID, int targetID) 23 | { 24 | RedisValue cachedValue = await distributedCacheStore.StringGetAsync(ConstructFriendRequestKey(requesterID, targetID)); 25 | 26 | if (cachedValue.IsNullOrEmpty) 27 | { 28 | return null; 29 | } 30 | 31 | string[] parts = cachedValue.ToString().Split(':'); 32 | 33 | if (parts.Length is not 2 || int.TryParse(parts.First(), out int requesterNotificationID) is false || int.TryParse(parts.Last(), out int targetNotificationID) is false) 34 | { 35 | return null; 36 | } 37 | 38 | return (requesterNotificationID, targetNotificationID); 39 | } 40 | 41 | /// 42 | /// Removes a friend request from the distributed cache store, usually because it has been accepted or declined. 43 | /// 44 | public static async Task RemoveFriendRequest(this IDatabase distributedCacheStore, int requesterID, int targetID) 45 | { 46 | await distributedCacheStore.KeyDeleteAsync(ConstructFriendRequestKey(requesterID, targetID)); 47 | } 48 | 49 | /// 50 | /// Checks whether a pending friend request already exists. 51 | /// 52 | public static async Task PendingFriendRequestExists(this IDatabase distributedCacheStore, int requesterID, int targetID) 53 | { 54 | bool exists = await distributedCacheStore.KeyExistsAsync(ConstructFriendRequestKey(requesterID, targetID)); 55 | 56 | return exists; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Persistence/MerrickContext.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Persistence; 2 | 3 | public sealed class MerrickContext : DbContext 4 | { 5 | public MerrickContext(DbContextOptions options) : base(options) 6 | { 7 | if (Database.IsInMemory().Equals(false)) 8 | Database.SetCommandTimeout(60); // 1 Minute - Helps Prevent Migrations From Timing Out When Many Records Need To Update 9 | } 10 | 11 | public DbSet Accounts => Set(); 12 | public DbSet Clans => Set(); 13 | public DbSet HeroGuides => Set(); 14 | public DbSet MatchStatistics => Set(); 15 | public DbSet PlayerStatistics => Set(); 16 | public DbSet Roles => Set(); 17 | public DbSet Tokens => Set(); 18 | public DbSet Users => Set(); 19 | 20 | protected override void OnModelCreating(ModelBuilder builder) 21 | { 22 | base.OnModelCreating(builder); 23 | 24 | ConfigureRoles(builder.Entity()); 25 | ConfigureAccounts(builder.Entity()); 26 | ConfigurePlayerStatistics(builder.Entity()); 27 | } 28 | 29 | private static void ConfigureRoles(EntityTypeBuilder builder) 30 | { 31 | builder.HasData 32 | ( 33 | new Role 34 | { 35 | ID = 1, 36 | Name = UserRoles.Administrator 37 | }, 38 | 39 | new Role 40 | { 41 | ID = 2, 42 | Name = UserRoles.User 43 | } 44 | ); 45 | } 46 | 47 | private static void ConfigureAccounts(EntityTypeBuilder builder) 48 | { 49 | builder.OwnsMany(account => account.BannedPeers, ownedNavigationBuilder => { ownedNavigationBuilder.ToJson(); }); 50 | builder.OwnsMany(account => account.FriendedPeers, ownedNavigationBuilder => { ownedNavigationBuilder.ToJson(); }); 51 | builder.OwnsMany(account => account.IgnoredPeers, ownedNavigationBuilder => { ownedNavigationBuilder.ToJson(); }); 52 | } 53 | 54 | private static void ConfigurePlayerStatistics(EntityTypeBuilder builder) 55 | { 56 | builder.Property(statistics => statistics.Inventory).HasConversion 57 | ( 58 | value => JsonSerializer.Serialize(value, new JsonSerializerOptions()), 59 | value => JsonSerializer.Deserialize>(value, new JsonSerializerOptions()) ?? new List(), 60 | new ValueComparer>((first, second) => (first ?? new List()).SequenceEqual(second ?? new List()), 61 | collection => collection.Aggregate(0, (accumulatedHashCode, value) => HashCode.Combine(accumulatedHashCode, value.GetHashCode())), collection => collection.ToList()) 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Extensions/UserClaimsExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Extensions; 2 | 3 | public static class UserClaimsExtensions 4 | { 5 | public static Guid GetAccountID(this IEnumerable claims) 6 | => Guid.Parse(claims.Single(claim => claim.Type.Equals(Claims.AccountID)).Value); 7 | 8 | public static bool GetAccountIsMain(this IEnumerable claims) 9 | => bool.Parse(claims.Single(claim => claim.Type.Equals(Claims.AccountIsMain)).Value); 10 | 11 | public static string GetAccountName(this IEnumerable claims) 12 | => claims.Single(claim => claim.Type.Equals(Claims.Subject) || claim.Type.Equals(Claims.NameIdentifier)).Value; 13 | 14 | public static string GetAudience(this IEnumerable claims) 15 | => claims.Single(claim => claim.Type.Equals(Claims.Audience)).Value; 16 | 17 | public static DateTimeOffset GetAuthenticatedAtTime(this IEnumerable claims) 18 | => EpochTimeToUTCTime(Convert.ToInt64(claims.Single(claim => claim.Type.Equals(Claims.AuthenticatedAtTime)).Value)); 19 | 20 | public static string GetClanName(this IEnumerable claims) 21 | => claims.Single(claim => claim.Type.Equals(Claims.ClanName)).Value; 22 | 23 | public static string GetClanTag(this IEnumerable claims) 24 | => claims.Single(claim => claim.Type.Equals(Claims.ClanTag)).Value; 25 | 26 | public static DateTimeOffset GetExpiresAtTime(this IEnumerable claims) 27 | => EpochTimeToUTCTime(Convert.ToInt64(claims.Single(claim => claim.Type.Equals(Claims.ExpiresAtTime)).Value)); 28 | 29 | public static DateTimeOffset GetIssuedAtTime(this IEnumerable claims) 30 | => EpochTimeToUTCTime(Convert.ToInt64(claims.Single(claim => claim.Type.Equals(Claims.IssuedAtTime)).Value)); 31 | 32 | public static string GetIssuer(this IEnumerable claims) 33 | => claims.Single(claim => claim.Type.Equals(Claims.Issuer)).Value; 34 | 35 | public static Guid GetJWTIdentifier(this IEnumerable claims) 36 | => Guid.Parse(claims.Single(claim => claim.Type.Equals(Claims.JWTIdentifier)).Value); 37 | 38 | public static Guid GetNonce(this IEnumerable claims) 39 | => Guid.Parse(claims.Single(claim => claim.Type.Equals(Claims.Nonce)).Value); 40 | 41 | public static Guid GetUserID(this IEnumerable claims) 42 | => Guid.Parse(claims.Single(claim => claim.Type.Equals(Claims.UserID)).Value); 43 | 44 | public static string GetUserEmailAddress(this IEnumerable claims) 45 | => claims.Single(claim => claim.Type.Equals(Claims.Email) || claim.Type.Equals(Claims.EmailAddress)).Value; 46 | 47 | public static string GetUserRole(this IEnumerable claims) 48 | => claims.Single(claim => claim.Type.Equals(Claims.UserRole)).Value; 49 | 50 | private static DateTimeOffset EpochTimeToUTCTime(long epochSeconds) 51 | => DateTimeOffset.FromUnixTimeSeconds(epochSeconds); 52 | } 53 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Controllers/PatcherController/PatcherController.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Controllers.PatcherController; 2 | 3 | [ApiController] 4 | [Route("patcher/patcher.php")] 5 | [Consumes("application/x-www-form-urlencoded")] 6 | public class PatcherController(ILogger logger, IDatabase distributedCache, IOptions configuration) : ControllerBase 7 | { 8 | private ILogger Logger { get; } = logger; 9 | private IDatabase DistributedCache { get; } = distributedCache; 10 | private OperationalConfiguration Configuration { get; } = configuration.Value; 11 | 12 | [HttpPost(Name = "Patcher")] 13 | public async Task LatestPatch([FromForm] LatestPatchRequestForm form) 14 | { 15 | if ((await DistributedCache.ValidateAccountSessionCookie(form.Cookie)).IsValid.Equals(false)) 16 | { 17 | Logger.LogWarning($@"IP Address ""{Request.HttpContext.Connection.RemoteIpAddress?.MapToIPv4().ToString() ?? "UNKNOWN"}"" Has Made A Patcher Controller Request With Forged Cookie ""{form.Cookie}"""); 18 | 19 | return Unauthorized($@"Unrecognized Cookie ""{form.Cookie}"""); 20 | } 21 | 22 | (string, string)[] supported = [ ("wac", "x86_64"), ("lac", "x86-biarch"), ("mac", "universal-64") ]; 23 | 24 | if (supported.Contains((form.OperatingSystem, form.Architecture)).Equals(false)) 25 | return BadRequest($@"Unsupported Client: Operating System ""{form.OperatingSystem}"", Architecture ""{form.Architecture}"""); 26 | 27 | // The Current Client's Version Number Needs To Include The Revision Number Even If It Is Zero (e.g. "4.10.1.0" rather than just "4.10.1") 28 | PatchDetails currentPatch = PatchHandlers.GetClientPatchDetails(form.OperatingSystem, form.CurrentPatchVersion); 29 | 30 | // Unlike The Current Client's Version, The Revision Number Is Excluded From The Version Number Of The Latest Client If It Is Zero (e.g. "4.10.1.0" becomes just "4.10.1") 31 | PatchDetails latestPatch = PatchHandlers.GetLatestClientPatchDetails(form.OperatingSystem); 32 | 33 | LatestPatchResponse response = new () 34 | { 35 | PatchVersion = currentPatch.FullVersion, 36 | CurrentPatchVersion = currentPatch.FullVersion, 37 | CurrentManifestArchiveSHA1Hash = currentPatch.ManifestArchiveSHA1Hash, 38 | CurrentManifestArchiveSizeInBytes = currentPatch.ManifestArchiveSizeInBytes, 39 | PatchDetails = new PatchDetailsForResponse 40 | { 41 | OperatingSystem = form.OperatingSystem, 42 | Architecture = form.Architecture, 43 | PatchVersion = latestPatch.Version, 44 | LatestPatchVersion = latestPatch.Version, 45 | LatestManifestArchiveSHA1Hash = latestPatch.ManifestArchiveSHA1Hash, 46 | LatestManifestArchiveSizeInBytes = latestPatch.ManifestArchiveSizeInBytes, 47 | PrimaryDownloadURL = Configuration.CDN.PrimaryPatchURL, 48 | SecondaryDownloadURL = Configuration.CDN.SecondaryPatchURL 49 | } 50 | }; 51 | 52 | return Ok(PhpSerialization.Serialize(response)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Controllers/ClientRequesterController/ClientRequesterControllerGuides.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Controllers.ClientRequesterController; 2 | 3 | public partial class ClientRequesterController 4 | { 5 | private async Task GetGuideList() 6 | { 7 | string? accountID = Request.Form["account"]; 8 | 9 | if (accountID is null) 10 | return BadRequest(@"Missing Value For Form Parameter ""account"""); 11 | 12 | string? heroIdentifier = Request.Form["hero"]; 13 | 14 | if (heroIdentifier is null) 15 | return BadRequest(@"Missing Value For Form Parameter ""hero"""); 16 | 17 | string? hostTime = Request.Form["hosttime"]; 18 | 19 | if (hostTime is null) 20 | return BadRequest(@"Missing Value For Form Parameter ""hosttime"""); 21 | 22 | Account? account = await MerrickContext.Accounts 23 | .Include(account => account.Clan) 24 | .SingleOrDefaultAsync(account => account.ID.Equals(int.Parse(accountID))); 25 | 26 | if (account is null) 27 | return NotFound($@"Account With ID ""{accountID}"" Was Not Found"); 28 | 29 | List guides = MerrickContext.HeroGuides 30 | .Include(guide => guide.Author).ThenInclude(record => record.Clan) 31 | .Where(guide => guide.HeroIdentifier.Equals(heroIdentifier)) 32 | .Where(guide => guide.Public.Equals(true) || guide.Public.Equals(false) && guide.Author.ID.Equals(account.ID)) 33 | .ToList(); 34 | 35 | if (guides.None()) 36 | { 37 | logger.LogError($@"No Guides Were Found For Hero Identifier ""{heroIdentifier}"""); 38 | 39 | return NotFound($@"No Guides Were Found For Hero Identifier ""{heroIdentifier}"""); 40 | } 41 | 42 | return Ok(PhpSerialization.Serialize(new GuideListResponse(guides, account, int.Parse(hostTime)))); 43 | } 44 | 45 | private async Task GetGuide() 46 | { 47 | string? accountID = Request.Form["account"]; 48 | 49 | if (accountID is null) 50 | return BadRequest(@"Missing Value For Form Parameter ""account"""); 51 | 52 | string? heroIdentifier = Request.Form["hero"]; 53 | 54 | if (heroIdentifier is null) 55 | return BadRequest(@"Missing Value For Form Parameter ""hero"""); 56 | 57 | string? hostTime = Request.Form["hosttime"]; 58 | 59 | if (hostTime is null) 60 | return BadRequest(@"Missing Value For Form Parameter ""hosttime"""); 61 | 62 | string? guideID = Request.Form["gid"]; 63 | 64 | switch (guideID) 65 | { 66 | case null: return BadRequest(@"Missing Value For Form Parameter ""gid"""); 67 | 68 | // A call for guide 99999 for the currently-selected hero is always made by the client when opening the guides for the first time every session. This call always returns an error response. 69 | case "99999": return Ok(PhpSerialization.Serialize(new GuideResponseError(int.Parse(hostTime)))); 70 | } 71 | 72 | HeroGuide? guide = await MerrickContext.HeroGuides 73 | .Include(guide => guide.Author).ThenInclude(author => author.Clan) 74 | .SingleOrDefaultAsync(guide => guide.ID.Equals(int.Parse(guideID))); 75 | 76 | if (guide is null) 77 | return NotFound($"Guide ID {guideID} Was Not Found"); 78 | 79 | return Ok(PhpSerialization.Serialize(new GuideResponseSuccess(guide, int.Parse(hostTime)))); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/SRP/SRPAuthenticationFailureResponse.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.SRP; 2 | 3 | public class SRPAuthenticationFailureResponse(SRPAuthenticationFailureReason reason, string? accountName = null) 4 | { 5 | /// 6 | /// A string of error output in the event of an authentication failure, e.g. "Invalid Nickname Or Password.". 7 | /// 8 | [PhpProperty("auth")] 9 | public string AuthenticationOutcome { get; set; } = reason switch 10 | { 11 | SRPAuthenticationFailureReason.AccountIsDisabled => "Account" + (accountName is null ? " " : $@" ""{accountName}"" ") + "Is Disabled", 12 | SRPAuthenticationFailureReason.AccountNotFound => "Account Not Found", 13 | SRPAuthenticationFailureReason.IncorrectPassword => "Incorrect Password", 14 | SRPAuthenticationFailureReason.IncorrectSystemInformationFormat => "Incorrect System Information Format", 15 | SRPAuthenticationFailureReason.IsServerHostingAccount => "Is Server Hosting Account", 16 | SRPAuthenticationFailureReason.MissingCachedSRPData => "Missing Cached SRP Data", 17 | SRPAuthenticationFailureReason.MissingClientPublicEphemeral => "Missing Client Public Ephemeral", 18 | SRPAuthenticationFailureReason.MissingIPAddress => "Missing IP Address", 19 | SRPAuthenticationFailureReason.MissingLoginIdentifier => "Missing Login Identifier", 20 | SRPAuthenticationFailureReason.MissingMajorVersion => "Missing Major Version", 21 | SRPAuthenticationFailureReason.MissingMinorVersion => "Missing Minor Version", 22 | SRPAuthenticationFailureReason.MissingMicroVersion => "Missing Micro Version", 23 | SRPAuthenticationFailureReason.MissingOperatingSystemType => "Missing Operating System Type", 24 | SRPAuthenticationFailureReason.MissingSRPClientProof => "Missing SRP Client Proof", 25 | SRPAuthenticationFailureReason.MissingSystemInformation => "Missing System Information", 26 | SRPAuthenticationFailureReason.SRPAuthenticationDisabled => "SRP Authentication Is Disabled" + Environment.NewLine + "1) Open The Console (CTRL + F8)" + Environment.NewLine + @"2) Execute ""SetSave login_useSRP true""", 27 | SRPAuthenticationFailureReason.UnexpectedUserAgent => "Unexpected User Agent", 28 | _ => "Unsupported Authentication Failure Reason" + " " + $@"""{nameof(reason)}""" 29 | }; 30 | 31 | /// 32 | /// Unknown property which seems to be set to "true" on a successful response or "false" if an error occurs. 33 | /// Since this is an error response, set to "false". 34 | /// 35 | [PhpProperty(0)] 36 | public bool Zero => false; 37 | } 38 | 39 | public enum SRPAuthenticationFailureReason 40 | { 41 | AccountIsDisabled, 42 | AccountNotFound, 43 | IncorrectPassword, 44 | IncorrectSystemInformationFormat, 45 | IsServerHostingAccount, 46 | MissingCachedSRPData, 47 | MissingClientPublicEphemeral, 48 | MissingIPAddress, 49 | MissingLoginIdentifier, 50 | MissingMajorVersion, 51 | MissingMinorVersion, 52 | MissingMicroVersion, 53 | MissingOperatingSystemType, 54 | MissingSRPClientProof, 55 | MissingSystemInformation, 56 | SRPAuthenticationDisabled, 57 | UnexpectedUserAgent 58 | } 59 | -------------------------------------------------------------------------------- /ASPIRE.Tests/KONGOR.MasterServer/Infrastructure/KONGORServiceProvider.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Tests.KONGOR.MasterServer.Infrastructure; 2 | 3 | /// 4 | /// Provides Test Dependencies For KONGOR Master Server Tests 5 | /// 6 | public static class KONGORServiceProvider 7 | { 8 | /// 9 | /// Creates An Instance Of The KONGOR Master Server With In-Memory Dependencies 10 | /// 11 | public static WebApplicationFactory CreateOrchestratedInstance(string? identifier = null) 12 | { 13 | string databaseName = identifier ?? Guid.CreateVersion7().ToString(); 14 | 15 | // Set Required Environment Variables 16 | Environment.SetEnvironmentVariable("CHAT_SERVER_HOST", "127.0.0.1"); 17 | Environment.SetEnvironmentVariable("CHAT_SERVER_PORT", "11031"); 18 | Environment.SetEnvironmentVariable("APPLICATION_URL", "http://localhost/"); 19 | 20 | // Replace Database Context And Distributed Cache With In-Memory Implementations 21 | WebApplicationFactory webApplicationFactory = new WebApplicationFactory().WithWebHostBuilder(builder => builder.ConfigureServices(services => 22 | { 23 | Func databaseContextPredicate = descriptor => 24 | descriptor.ServiceType.FullName?.Contains(nameof(MerrickContext)) is true || descriptor.ImplementationType?.FullName?.Contains(nameof(MerrickContext)) is true; 25 | 26 | // Remove MerrickContext Registration 27 | foreach (ServiceDescriptor? descriptor in services.Where(databaseContextPredicate).ToList()) 28 | services.Remove(descriptor); 29 | 30 | // Register In-Memory MerrickContext 31 | services.AddDbContext(options => options.UseInMemoryDatabase(databaseName).EnableServiceProviderCaching(false), 32 | ServiceLifetime.Singleton, ServiceLifetime.Singleton); 33 | 34 | Func distributedCachePredicate = descriptor => 35 | descriptor.ServiceType == typeof(IConnectionMultiplexer) || descriptor.ServiceType == typeof(IDatabase); 36 | 37 | // Remove IConnectionMultiplexer And IDatabase Registrations 38 | foreach (ServiceDescriptor? descriptor in services.Where(distributedCachePredicate).ToList()) 39 | services.Remove(descriptor); 40 | 41 | // Register In-Process Distributed Cache Database 42 | services.AddSingleton(); 43 | 44 | // Add Middleware To Set Fake Remote IP Address 45 | services.AddSingleton(new RemoteIPAddressStartupFilter()); 46 | })); 47 | 48 | // Ensure That OnModelCreating From MerrickContext Has Been Called 49 | webApplicationFactory.Services.GetRequiredService().Database.EnsureCreated(); 50 | 51 | return webApplicationFactory; 52 | } 53 | } 54 | 55 | /// 56 | /// Startup Filter To Set Fake Remote IP Address 57 | /// 58 | file class RemoteIPAddressStartupFilter : IStartupFilter 59 | { 60 | public Action Configure(Action next) 61 | { 62 | return app => 63 | { 64 | app.Use(next => async context => 65 | { 66 | context.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback; 67 | 68 | await next(context); 69 | }); 70 | 71 | next(app); 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ZORGATH.WebPortal.API/Controllers/EmailAddressController.cs: -------------------------------------------------------------------------------- 1 | namespace ZORGATH.WebPortal.API.Controllers; 2 | 3 | [ApiController] 4 | [Route("[controller]")] 5 | [Consumes("application/json")] 6 | [EnableRateLimiting(RateLimiterPolicies.Strict)] 7 | public class EmailAddressController(MerrickContext databaseContext, ILogger logger, IEmailService emailService) : ControllerBase 8 | { 9 | private MerrickContext MerrickContext { get; } = databaseContext; 10 | private ILogger Logger { get; } = logger; 11 | private IEmailService EmailService { get; } = emailService; 12 | 13 | [HttpPost("Register", Name = "Register Email Address")] 14 | [AllowAnonymous] 15 | [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] 16 | [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] 17 | [ProducesResponseType(typeof(string), StatusCodes.Status422UnprocessableEntity)] 18 | [ProducesResponseType(typeof(string), StatusCodes.Status503ServiceUnavailable)] 19 | public async Task RegisterEmailAddress(RegisterEmailAddressDTO payload) 20 | { 21 | if (payload.EmailAddress.Equals(payload.ConfirmEmailAddress).Equals(false)) 22 | return BadRequest($@"Email Address ""{payload.ConfirmEmailAddress}"" Does Not Match ""{payload.EmailAddress}"""); 23 | 24 | Token? token = await MerrickContext.Tokens.SingleOrDefaultAsync(token => token.EmailAddress.Equals(payload.EmailAddress) && token.Purpose.Equals(TokenPurpose.EmailAddressVerification)); 25 | 26 | if (token is null) 27 | { 28 | IActionResult result = EmailAddressHelpers.SanitizeEmailAddress(payload.EmailAddress); 29 | 30 | if (result is not ContentResult contentResult) 31 | { 32 | return result; 33 | } 34 | 35 | if (contentResult.Content is null) 36 | { 37 | Logger.LogError(@"[BUG] Sanitized Email Address ""{Payload.EmailAddress}"" Is NULL", payload.EmailAddress); 38 | 39 | return UnprocessableEntity($@"Unable To Process Email Address ""{payload.EmailAddress}"""); 40 | } 41 | 42 | string sanitizedEmailAddress = contentResult.Content; 43 | 44 | token = new Token() 45 | { 46 | Purpose = TokenPurpose.EmailAddressVerification, 47 | EmailAddress = payload.EmailAddress, 48 | Value = Guid.CreateVersion7(), 49 | Data = sanitizedEmailAddress 50 | }; 51 | 52 | await MerrickContext.Tokens.AddAsync(token); 53 | await MerrickContext.SaveChangesAsync(); 54 | 55 | bool sent = await EmailService.SendEmailAddressRegistrationLink(payload.EmailAddress, token.Value.ToString()); 56 | 57 | if (sent.Equals(false)) 58 | { 59 | MerrickContext.Tokens.Remove(token); 60 | await MerrickContext.SaveChangesAsync(); 61 | 62 | return StatusCode(StatusCodes.Status503ServiceUnavailable, "Failed To Send Email Address Verification Email"); 63 | } 64 | } 65 | 66 | else 67 | { 68 | return token.TimestampConsumed is null 69 | ? BadRequest($@"A Registration Request For Email Address ""{payload.EmailAddress}"" Has Already Been Made (Check Your Email Inbox For A Registration Link)") 70 | : BadRequest($@"Email Address ""{payload.EmailAddress}"" Is Already Registered"); 71 | } 72 | 73 | return Ok($@"Email Address Registration Token Was Successfully Created, And An Email Was Sent To Address ""{payload.EmailAddress}"""); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/Entities/Core/Account.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext.Entities.Core; 2 | 3 | [Index(nameof(Name), IsUnique = true)] 4 | public class Account 5 | { 6 | [Key] 7 | public int ID { get; set; } 8 | 9 | [MaxLength(15)] 10 | public required string Name { get; set; } 11 | 12 | public required User User { get; set; } 13 | 14 | public AccountType Type { get; set; } = AccountType.Legacy; 15 | 16 | public required bool IsMain { get; set; } 17 | 18 | public Clan? Clan { get; set; } = null; 19 | 20 | public ClanTier ClanTier { get; set; } = ClanTier.None; 21 | 22 | public DateTimeOffset? TimestampJoinedClan { get; set; } = null; 23 | 24 | public int AscensionLevel { get; set; } = 0; 25 | 26 | public DateTimeOffset TimestampCreated { get; set; } = DateTimeOffset.UtcNow; 27 | 28 | public DateTimeOffset TimestampLastActive { get; set; } = DateTimeOffset.UtcNow; 29 | 30 | public List AutoConnectChatChannels { get; set; } = []; 31 | 32 | public List BannedPeers { get; set; } = []; 33 | 34 | public List FriendedPeers { get; set; } = []; 35 | 36 | public List IgnoredPeers { get; set; } = []; 37 | 38 | public List SelectedStoreItems { get; set; } = ["ai.Default Icon", "cc.white", "t.Standard"]; 39 | 40 | public List IPAddressCollection { get; set; } = []; 41 | 42 | public List MACAddressCollection { get; set; } = []; 43 | 44 | public List SystemInformationCollection { get; set; } = []; 45 | 46 | public List SystemInformationHashCollection { get; set; } = []; 47 | 48 | [NotMapped] 49 | public string NameWithClanTag => Equals(Clan, null) ? Name : $"[{Clan.Tag}]{Name}"; 50 | 51 | [NotMapped] 52 | public string ClanTierName => ClanTier switch 53 | { 54 | ClanTier.None => "None", 55 | ClanTier.Member => "Member", 56 | ClanTier.Officer => "Officer", 57 | ClanTier.Leader => "Leader", 58 | _ => throw new ArgumentOutOfRangeException(@$"Unsupported Clan Tier ""{ClanTier}""") 59 | }; 60 | 61 | public static (string ClanTag, string AccountName) SeparateClanTagFromAccountName(string accountNameWithClanTag) 62 | { 63 | // If no '[' and ']' characters are found, then the account is not part of a clan and has no clan tag. 64 | if (accountNameWithClanTag.Contains('[').Equals(false) && accountNameWithClanTag.Contains(']').Equals(false)) 65 | return (string.Empty, accountNameWithClanTag); 66 | 67 | // If '[' is not the first character, then the account name contains the '[' and ']' characters, but the account is not part of a clan and has no clan tag. 68 | if (accountNameWithClanTag.StartsWith('[').Equals(false)) 69 | return (string.Empty, accountNameWithClanTag); 70 | 71 | // Remove the leading '[' character and split at the first occurrence of the ']' character. The resulting account name may contain the '[' and ']' characters. 72 | string[] segments = accountNameWithClanTag.TrimStart('[').Split(']', count: 2); 73 | 74 | return (segments.First(), segments.Last()); 75 | } 76 | 77 | public string Icon => SelectedStoreItems.SingleOrDefault(item => item.StartsWith("ai.")) ?? "ai.Default Icon"; 78 | public string IconNoPrefixCode => Icon.Replace("ai.", string.Empty); 79 | 80 | public string ChatSymbol => SelectedStoreItems.SingleOrDefault(item => item.StartsWith("cs.")) ?? string.Empty; 81 | public string ChatSymbolNoPrefixCode => ChatSymbol.Replace("cs.", string.Empty); 82 | 83 | public string NameColour => SelectedStoreItems.SingleOrDefault(item => item.StartsWith("cc.")) ?? "cc.white"; 84 | public string NameColourNoPrefixCode => NameColour.Replace("cc.", string.Empty); 85 | } 86 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Services/MatchmakingService.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Services; 2 | 3 | public class MatchmakingService : IHostedService, IDisposable 4 | { 5 | public static ConcurrentDictionary Groups { get; set; } = []; 6 | 7 | public static MatchmakingGroup? GetMatchmakingGroup(OneOf memberIdentifier) 8 | => memberIdentifier.Match(id => GetMatchmakingGroupByMemberID(id), name => GetMatchmakingGroupByMemberName(name)); 9 | 10 | public static MatchmakingGroup? GetMatchmakingGroupByMemberID(int memberID) 11 | => Groups.Values.SingleOrDefault(group => group.Members.Any(member => member.Account.ID == memberID)); 12 | 13 | public static MatchmakingGroup? GetMatchmakingGroupByMemberName(string memberName) 14 | => Groups.Values.SingleOrDefault(group => group.Members.Any(member => member.Account.Name.Equals(memberName))); 15 | 16 | public static ConcurrentDictionary SoloPlayerGroups 17 | => new (Groups.Where(group => group.Value.Members.Count == 1)); 18 | 19 | public static ConcurrentDictionary TwoPlayerGroups 20 | => new (Groups.Where(group => group.Value.Members.Count == 2)); 21 | 22 | public static ConcurrentDictionary ThreePlayerGroups 23 | => new (Groups.Where(group => group.Value.Members.Count == 3)); 24 | 25 | public static ConcurrentDictionary FourPlayerGroups 26 | => new (Groups.Where(group => group.Value.Members.Count == 4)); 27 | 28 | public static ConcurrentDictionary FivePlayerGroups 29 | => new (Groups.Where(group => group.Value.Members.Count == 5)); 30 | 31 | public async Task StartAsync(CancellationToken cancellationToken) 32 | { 33 | Log.Information("Matchmaking Service Has Started"); 34 | 35 | await RunMatchBroker(cancellationToken); 36 | } 37 | 38 | public async Task StopAsync(CancellationToken cancellationToken) 39 | { 40 | Log.Information("Matchmaking Service Has Stopped"); 41 | 42 | await Task.CompletedTask; 43 | } 44 | 45 | public void Dispose() 46 | { 47 | Groups.Clear(); 48 | 49 | GC.SuppressFinalize(this); 50 | } 51 | 52 | private async Task RunMatchBroker(CancellationToken cancellationToken) 53 | { 54 | while (cancellationToken.IsCancellationRequested is false) 55 | { 56 | // TODO: Implement Match Broker Logic Here 57 | 58 | # region Match Broker Logic Placeholder 59 | if (Groups.Count == 2 && Groups.Values.First().QueueDuration != TimeSpan.Zero && Groups.Values.Last().QueueDuration != TimeSpan.Zero) 60 | { 61 | List team_1 = [Groups.Values.First()]; 62 | List team_2 = [Groups.Values.Last()]; 63 | 64 | List groups = [.. team_1, .. team_2]; 65 | 66 | foreach (MatchmakingGroup group in groups) 67 | group.QueueStartTime = null; 68 | 69 | ChatBuffer found = new (); 70 | 71 | found.WriteCommand(ChatProtocol.Matchmaking.NET_CHAT_CL_TMM_GROUP_QUEUE_UPDATE); 72 | found.WriteInt8(Convert.ToByte(ChatProtocol.TMMUpdateType.TMM_GROUP_FOUND_SERVER)); // Sound The Horn !!! 73 | 74 | // TODO: This Packet Can Be Sent With TMM_GROUP_QUEUE_UPDATE And A 4-Byte Integer To Update The Average Time In Queue (In Seconds) 75 | 76 | foreach (MatchmakingGroup group in groups) 77 | foreach (MatchmakingGroupMember member in group.Members) 78 | member.Session.Send(found); 79 | } 80 | # endregion 81 | } 82 | 83 | await Task.CompletedTask; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Utilities/Log.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Utilities; 2 | 3 | public class Log 4 | { 5 | private static ILogger? MaybeLogger { get; set; } 6 | 7 | public static ILogger Initialise(ILogger logger) 8 | { 9 | MaybeLogger = logger; 10 | 11 | return MaybeLogger; 12 | } 13 | 14 | private static ILogger Get() 15 | => MaybeLogger is null ? throw new InvalidOperationException("Logger Has Not Been Initialised") : MaybeLogger; 16 | 17 | public static void Debug(Exception? exception, string? message, params object?[] args) 18 | { 19 | ILogger logger = Get(); 20 | 21 | if (logger.IsEnabled(LogLevel.Debug)) 22 | logger.Log(LogLevel.Debug, exception, message, args); 23 | } 24 | 25 | public static void Debug(string? message, params object?[] args) 26 | { 27 | ILogger logger = Get(); 28 | 29 | if (logger.IsEnabled(LogLevel.Debug)) 30 | logger.Log(LogLevel.Debug, message, args); 31 | } 32 | 33 | public static void Trace(Exception? exception, string? message, params object?[] args) 34 | { 35 | ILogger logger = Get(); 36 | 37 | if (logger.IsEnabled(LogLevel.Trace)) 38 | logger.Log(LogLevel.Trace, exception, message, args); 39 | } 40 | 41 | public static void Trace(string? message, params object?[] args) 42 | { 43 | ILogger logger = Get(); 44 | 45 | if (logger.IsEnabled(LogLevel.Trace)) 46 | logger.Log(LogLevel.Trace, message, args); 47 | } 48 | 49 | public static void Information(Exception? exception, string? message, params object?[] args) 50 | { 51 | ILogger logger = Get(); 52 | 53 | if (logger.IsEnabled(LogLevel.Information)) 54 | logger.Log(LogLevel.Information, exception, message, args); 55 | } 56 | 57 | public static void Information(string? message, params object?[] args) 58 | { 59 | ILogger logger = Get(); 60 | 61 | if (logger.IsEnabled(LogLevel.Information)) 62 | logger.Log(LogLevel.Information, message, args); 63 | } 64 | 65 | public static void Warning(Exception? exception, string? message, params object?[] args) 66 | { 67 | ILogger logger = Get(); 68 | 69 | if (logger.IsEnabled(LogLevel.Warning)) 70 | logger.Log(LogLevel.Warning, exception, message, args); 71 | } 72 | 73 | public static void Warning(string? message, params object?[] args) 74 | { 75 | ILogger logger = Get(); 76 | 77 | if (logger.IsEnabled(LogLevel.Warning)) 78 | logger.Log(LogLevel.Warning, message, args); 79 | } 80 | 81 | public static void Error(Exception? exception, string? message, params object?[] args) 82 | { 83 | ILogger logger = Get(); 84 | 85 | if (logger.IsEnabled(LogLevel.Error)) 86 | logger.Log(LogLevel.Error, exception, message, args); 87 | } 88 | 89 | public static void Error(string? message, params object?[] args) 90 | { 91 | ILogger logger = Get(); 92 | 93 | if (logger.IsEnabled(LogLevel.Error)) 94 | logger.Log(LogLevel.Error, message, args); 95 | } 96 | 97 | public static void Critical(Exception? exception, string? message, params object?[] args) 98 | { 99 | ILogger logger = Get(); 100 | 101 | if (logger.IsEnabled(LogLevel.Critical)) 102 | logger.Log(LogLevel.Critical, exception, message, args); 103 | } 104 | 105 | public static void Critical(string? message, params object?[] args) 106 | { 107 | ILogger logger = Get(); 108 | 109 | if (logger.IsEnabled(LogLevel.Critical)) 110 | logger.Log(LogLevel.Critical, message, args); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Controllers/ClientRequesterController/ClientRequesterControllerStats.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Controllers.ClientRequesterController; 2 | 3 | public partial class ClientRequesterController 4 | { 5 | private async Task GetSimpleStats() 6 | { 7 | string? accountName = Request.Form["nickname"]; 8 | 9 | if (accountName is null) 10 | return BadRequest(@"Missing Value For Form Parameter ""nickname"""); 11 | 12 | Account? account = await MerrickContext.Accounts 13 | .Include(account => account.User) 14 | .Include(account => account.Clan) 15 | .SingleOrDefaultAsync(account => account.Name.Equals(accountName)); 16 | 17 | if (account is null) 18 | return NotFound($@"Account With Name ""{accountName}"" Was Not Found"); 19 | 20 | ShowSimpleStatsResponse response = new () 21 | { 22 | NameWithClanTag = account.NameWithClanTag, 23 | ID = account.ID.ToString(), 24 | Level = account.User.TotalLevel, 25 | LevelExperience = account.User.TotalExperience, 26 | NumberOfAvatarsOwned = account.User.OwnedStoreItems.Count(item => item.StartsWith("aa.")), 27 | TotalMatchesPlayed = 5555, // TODO: Implement Matches Played 28 | CurrentSeason = 12, // TODO: Set Season 29 | SimpleSeasonStats = new SimpleSeasonStats() // TODO: Implement Stats 30 | { 31 | RankedMatchesWon = 1001 /* ranked */ + 1001 /* ranked casual */, 32 | RankedMatchesLost = 1002 /* ranked */ + 1002 /* ranked casual */, 33 | WinStreak = Math.Max(1003 /* ranked */, 1003 /* ranked casual */), 34 | InPlacementPhase = 0, // TODO: Implement Placement Matches 35 | LevelsGainedThisSeason = account.User.TotalLevel 36 | }, 37 | SimpleCasualSeasonStats = new SimpleSeasonStats() // TODO: Implement Stats 38 | { 39 | RankedMatchesWon = 1001 /* ranked */ + 1001 /* ranked casual */, 40 | RankedMatchesLost = 1002 /* ranked */ + 1002 /* ranked casual */, 41 | WinStreak = Math.Max(1003 /* ranked */, 1003 /* ranked casual */), 42 | InPlacementPhase = 0, // TODO: Implement Placement Matches 43 | LevelsGainedThisSeason = account.User.TotalLevel 44 | }, 45 | MVPAwardsCount = 1004, 46 | Top4AwardNames = [ "awd_masst", "awd_mhdd", "awd_mbdmg", "awd_lgks" ], // TODO: Implement Awards 47 | Top4AwardCounts = [ 1005, 1006, 1007, 1008 ], // TODO: Implement Awards 48 | CustomIconSlotID = SetCustomIconSlotID(account), 49 | OwnedStoreItems = account.User.OwnedStoreItems, 50 | SelectedStoreItems = account.SelectedStoreItems, 51 | OwnedStoreItemsData = SetOwnedStoreItemsData(account) 52 | }; 53 | 54 | return Ok(PhpSerialization.Serialize(response)); 55 | } 56 | 57 | private static string SetCustomIconSlotID(Account account) 58 | => account.SelectedStoreItems.Any(item => item.StartsWith("ai.custom_icon")) 59 | ? account.SelectedStoreItems.Single(item => item.StartsWith("ai.custom_icon")).Replace("ai.custom_icon:", string.Empty) : "0"; 60 | 61 | private static Dictionary> SetOwnedStoreItemsData(Account account) 62 | { 63 | Dictionary> items = account.User.OwnedStoreItems 64 | .Where(item => item.StartsWith("ma.").Equals(false) && item.StartsWith("cp.").Equals(false)) 65 | .ToDictionary>(upgrade => upgrade, upgrade => new StoreItemData()); 66 | 67 | // TODO: Add Mastery Boosts And Coupons 68 | 69 | return items; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MERRICK.DatabaseContext/MERRICK.cs: -------------------------------------------------------------------------------- 1 | namespace MERRICK.DatabaseContext; 2 | 3 | /// 4 | /// My name is Merrick, and I manage the store ... the data store. 5 | /// 6 | public class MERRICK 7 | { 8 | public static async Task Main(string[] args) 9 | { 10 | // Create The Application Builder 11 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 12 | 13 | // Add Aspire Service Defaults 14 | builder.AddServiceDefaults(); 15 | 16 | // Add The Database Context 17 | builder.AddSqlServerDbContext("MERRICK", configureSettings: null, configureDbContextOptions: options => 18 | { 19 | // Enable Detailed Error Messages In Development Environment 20 | options.EnableDetailedErrors(builder.Environment.IsDevelopment()); 21 | 22 | // Suppress Warning Regarding Enabled Sensitive Data Logging, Since It Is Only Enabled In The Development Environment 23 | // https://github.com/dotnet/efcore/blob/main/src/EFCore/Properties/CoreStrings.resx (LogSensitiveDataLoggingEnabled) 24 | options.EnableSensitiveDataLogging(builder.Environment.IsDevelopment()) 25 | .ConfigureWarnings(warnings => warnings.Log((Id: CoreEventId.SensitiveDataLoggingEnabledWarning, Level: LogLevel.Trace))); 26 | 27 | // Enable Thread Safety Checks For Entity Framework 28 | options.EnableThreadSafetyChecks(); 29 | }); 30 | 31 | // Add Database Initializer Telemetry 32 | builder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddSource(DatabaseInitializer.ActivitySourceName)); 33 | 34 | // Register Database Initializer As Singleton Service For Dependency Injection 35 | builder.Services.AddSingleton(); 36 | 37 | // Register Database Initializer As Hosted Service For Background Execution At Application Startup 38 | builder.Services.AddHostedService(provider => provider.GetRequiredService()); 39 | 40 | // Add Database Health Check 41 | builder.Services.AddHealthChecks().AddCheck("MERRICK Database Health Check"); 42 | 43 | // Build The Application 44 | WebApplication application = builder.Build(); 45 | 46 | // Configure Development-Specific Middleware 47 | if (application.Environment.IsDevelopment()) 48 | { 49 | // Show Detailed Error Pages In Development 50 | application.UseDeveloperExceptionPage(); 51 | } 52 | 53 | else 54 | { 55 | // Use Global Exception Handler In Production 56 | application.UseExceptionHandler("/error"); 57 | } 58 | 59 | // Enforce HTTPS With Strict Transport Security 60 | application.UseHsts(); 61 | 62 | // Automatically Redirect HTTP Requests To HTTPS 63 | application.UseHttpsRedirection(); 64 | 65 | // Add Security Headers Middleware 66 | application.Use(async (context, next) => 67 | { 68 | IHeaderDictionary headers = context.Response.Headers; 69 | 70 | // Prevent MIME Type Sniffing 71 | headers["X-Content-Type-Options"] = "nosniff"; 72 | 73 | // Control Referrer Information 74 | headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; 75 | 76 | // Apply Restrictive CSP Only To API Endpoints 77 | if (context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)) 78 | { 79 | headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none';"; 80 | } 81 | 82 | await next(); 83 | }); 84 | 85 | // Map Aspire Default Health Check Endpoints 86 | application.MapDefaultEndpoints(); 87 | 88 | // Run The Application 89 | await application.RunAsync(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /TRANSMUTANSTEIN.ChatServer/Domain/Communication/Whisper.cs: -------------------------------------------------------------------------------- 1 | namespace TRANSMUTANSTEIN.ChatServer.Domain.Communication; 2 | 3 | public class Whisper 4 | { 5 | public required string Message { get; init; } 6 | 7 | /// 8 | /// Hidden Constructor Which Enforces As The Primary Mechanism For Creating Whispers 9 | /// 10 | private Whisper() { } 11 | 12 | public static Whisper Create(string message) 13 | => new () { Message = message }; 14 | 15 | public Whisper Send(ChatSession senderSession, string recipientName) 16 | { 17 | ChatSession recipientSession = Context.ChatSessions.Values 18 | .Single(chatSession => chatSession.Account.Name.Equals(recipientName, StringComparison.OrdinalIgnoreCase)); 19 | 20 | // Check Recipient's Chat Mode 21 | switch (recipientSession.Metadata.ClientChatModeState) 22 | { 23 | // DND: Block Whisper And Send Auto-Response 24 | case ChatProtocol.ChatModeType.CHAT_MODE_DND: 25 | SendWhisperFailure(senderSession, recipientName) 26 | .SendAutomaticResponse(senderSession, recipientSession, "Do Not Disturb"); 27 | 28 | return this; 29 | 30 | // Invisible: Treat As Offline 31 | case ChatProtocol.ChatModeType.CHAT_MODE_INVISIBLE: 32 | SendWhisperFailure(senderSession, recipientName); 33 | 34 | return this; 35 | 36 | // AFK: Deliver Message But Send Auto-Response 37 | case ChatProtocol.ChatModeType.CHAT_MODE_AFK: 38 | SendWhisperSuccess(senderSession.Account.Name, recipientSession) 39 | .SendAutomaticResponse(senderSession, recipientSession, "Away From Keyboard"); 40 | 41 | return this; 42 | 43 | // Available: Normal Delivery 44 | case ChatProtocol.ChatModeType.CHAT_MODE_AVAILABLE: 45 | SendWhisperSuccess(senderSession.Account.Name, recipientSession); 46 | 47 | return this; 48 | 49 | default: 50 | throw new ArgumentOutOfRangeException(nameof(recipientSession.Metadata.ClientChatModeState), recipientSession.Metadata.ClientChatModeState, 51 | $@"Unknown Chat Mode State ""{recipientSession.Metadata.ClientChatModeState}"" For Recipient ""{recipientSession.Account.Name}"""); 52 | } 53 | } 54 | 55 | private Whisper SendWhisperSuccess(string senderName, ChatSession recipientSession) 56 | { 57 | ChatBuffer whisperSuccess = new (); 58 | 59 | whisperSuccess.WriteCommand(ChatProtocol.Command.CHAT_CMD_WHISPER); 60 | whisperSuccess.WriteString(senderName); // Sender Name 61 | whisperSuccess.WriteString(Message); // Message Content 62 | 63 | recipientSession.Send(whisperSuccess); 64 | 65 | return this; 66 | } 67 | 68 | 69 | private Whisper SendWhisperFailure(ChatSession senderSession, string recipientName) 70 | { 71 | ChatBuffer whisperFailed = new (); 72 | 73 | whisperFailed.WriteCommand(ChatProtocol.Command.CHAT_CMD_WHISPER_FAILED); 74 | whisperFailed.WriteString(recipientName); // Recipient's Account Name 75 | whisperFailed.WriteString(Message); // Message Content 76 | 77 | senderSession.Send(whisperFailed); 78 | 79 | return this; 80 | } 81 | 82 | private Whisper SendAutomaticResponse(ChatSession senderSession, ChatSession recipientSession, string messageAutomaticResponse) 83 | { 84 | ChatBuffer automaticResponse = new (); 85 | 86 | automaticResponse.WriteCommand(ChatProtocol.Command.CHAT_CMD_CHAT_MODE_AUTO_RESPONSE); 87 | automaticResponse.WriteInt8(Convert.ToByte(recipientSession.Metadata.ClientChatModeState)); // Recipient's Chat Mode Type 88 | automaticResponse.WriteString(recipientSession.Account.Name); // Recipient's Account Name 89 | automaticResponse.WriteString(messageAutomaticResponse); // Message Content 90 | 91 | senderSession.Send(automaticResponse); 92 | 93 | return this; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ASPIRE.Tests/ZORGATH.WebPortal.API/Tests/EmailAddressRegistrationTests.cs: -------------------------------------------------------------------------------- 1 | namespace ASPIRE.Tests.ZORGATH.WebPortal.API.Tests; 2 | 3 | /// 4 | /// Tests For Email Address Registration Functionality 5 | /// 6 | public sealed class EmailAddressRegistrationTests 7 | { 8 | [Test] 9 | [Arguments("test@kongor.com")] 10 | [Arguments("user@kongor.net")] 11 | public async Task RegisterEmailAddress_WithValidEmailAddress_ReturnsOKAndCreatesToken(string emailAddress) 12 | { 13 | await using WebApplicationFactory webApplicationFactory = ZORGATHServiceProvider.CreateOrchestratedInstance(); 14 | 15 | ILogger logger = webApplicationFactory.Services.GetRequiredService>(); 16 | IEmailService emailService = webApplicationFactory.Services.GetRequiredService(); 17 | 18 | MerrickContext databaseContext = webApplicationFactory.Services.GetRequiredService(); 19 | 20 | EmailAddressController controller = new (databaseContext, logger, emailService); 21 | 22 | IActionResult response = await controller.RegisterEmailAddress(new RegisterEmailAddressDTO(emailAddress, emailAddress)); 23 | 24 | await Assert.That(response).IsTypeOf(); 25 | 26 | Token? token = await databaseContext.Tokens.SingleOrDefaultAsync(token => 27 | token.EmailAddress.Equals(emailAddress) && token.Purpose.Equals(TokenPurpose.EmailAddressVerification)); 28 | 29 | await Assert.That(token).IsNotNull(); 30 | 31 | using (Assert.Multiple()) 32 | { 33 | await Assert.That(token.EmailAddress).IsEqualTo(emailAddress); 34 | await Assert.That(token.Purpose).IsEqualTo(TokenPurpose.EmailAddressVerification); 35 | await Assert.That(token.TimestampConsumed).IsNull(); 36 | } 37 | } 38 | 39 | [Test] 40 | [Arguments("test@kongor.com", "different@kongor.com")] 41 | [Arguments("user@kongor.net", "typo@kongor.net")] 42 | public async Task RegisterEmailAddress_WithMismatchedConfirmation_ReturnsBadRequest(string emailAddress, string confirmEmailAddress) 43 | { 44 | await using WebApplicationFactory webApplicationFactory = ZORGATHServiceProvider.CreateOrchestratedInstance(); 45 | 46 | ILogger logger = webApplicationFactory.Services.GetRequiredService>(); 47 | IEmailService emailService = webApplicationFactory.Services.GetRequiredService(); 48 | 49 | MerrickContext databaseContext = webApplicationFactory.Services.GetRequiredService(); 50 | 51 | EmailAddressController controller = new (databaseContext, logger, emailService); 52 | 53 | IActionResult response = await controller.RegisterEmailAddress(new RegisterEmailAddressDTO(emailAddress, confirmEmailAddress)); 54 | 55 | await Assert.That(response).IsTypeOf(); 56 | } 57 | 58 | [Test] 59 | [Arguments("duplicate@kongor.com")] 60 | [Arguments("existing@kongor.net")] 61 | public async Task RegisterEmailAddress_WhenAlreadyRegistered_ReturnsBadRequest(string emailAddress) 62 | { 63 | await using WebApplicationFactory webApplicationFactory = ZORGATHServiceProvider.CreateOrchestratedInstance(); 64 | 65 | ILogger logger = webApplicationFactory.Services.GetRequiredService>(); 66 | IEmailService emailService = webApplicationFactory.Services.GetRequiredService(); 67 | 68 | MerrickContext databaseContext = webApplicationFactory.Services.GetRequiredService(); 69 | 70 | EmailAddressController controller = new (databaseContext, logger, emailService); 71 | 72 | await controller.RegisterEmailAddress(new RegisterEmailAddressDTO(emailAddress, emailAddress)); 73 | 74 | IActionResult response = await controller.RegisterEmailAddress(new RegisterEmailAddressDTO(emailAddress, emailAddress)); 75 | 76 | await Assert.That(response).IsTypeOf(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /KONGOR.MasterServer/Models/RequestResponse/Patch/LatestPatchResponse.cs: -------------------------------------------------------------------------------- 1 | namespace KONGOR.MasterServer.Models.RequestResponse.Patch; 2 | 3 | public class LatestPatchResponse 4 | { 5 | /// 6 | /// Details on the latest HoN client patch version. 7 | /// 8 | [PhpProperty(0)] 9 | public required PatchDetailsForResponse PatchDetails { get; set; } 10 | 11 | /// 12 | /// The HoN client's current version. 13 | /// This should match what is sent by the HoN client making the request. 14 | /// It is unknown why this duplicates the "current_version" property of the PHP object. 15 | /// 16 | [PhpProperty("version")] 17 | public required string PatchVersion { get; set; } 18 | 19 | /// 20 | /// The HoN client's current version. 21 | /// This should match what is sent by the HoN client making the request. 22 | /// 23 | [PhpProperty("current_version")] 24 | public required string CurrentPatchVersion { get; set; } 25 | 26 | /// 27 | /// The SHA-1 hash of the zipped manifest file of the client making the request. 28 | /// 29 | [PhpProperty("current_manifest_checksum")] 30 | public required string CurrentManifestArchiveSHA1Hash { get; set; } 31 | 32 | /// 33 | /// The size in bytes of the zipped manifest file of the client making the request. 34 | /// 35 | [PhpProperty("current_manifest_size")] 36 | public required string CurrentManifestArchiveSizeInBytes { get; set; } 37 | } 38 | 39 | public class PatchDetailsForResponse 40 | { 41 | /// 42 | /// The latest HoN client version available. 43 | /// It is unknown why this duplicates the "latest_version" property of the PHP object. 44 | /// 45 | [PhpProperty("version")] 46 | public required string PatchVersion { get; set; } 47 | 48 | /// 49 | /// The latest HoN client version available. 50 | /// 51 | [PhpProperty("latest_version")] 52 | public required string LatestPatchVersion { get; set; } 53 | 54 | /// 55 | /// The HoN client's operating system. 56 | /// Generally, (ignoring RCT/SBT/etc.) this will be one of three values: "wac" (Windows client), "lac" (Linux client), or "mac" (macOS client). 57 | /// 58 | [PhpProperty("os")] 59 | public required string OperatingSystem { get; set; } 60 | 61 | /// 62 | /// The HoN client's operating system architecture. 63 | /// Generally, this will be one of three values: "x86_64" (Windows client), "x86-biarch" (Linux client), or "universal-64" (macOS client). 64 | /// The 32-bit versions of Windows ("i686") and macOS ("universal"), alongside other legacy architectures, are not supported by Project KONGOR. 65 | /// 66 | [PhpProperty("arch")] 67 | public required string Architecture { get; set; } 68 | 69 | /// 70 | /// The primary download URL for the patch files. 71 | /// This was originally set to "http://cdn.naeu.patch.heroesofnewerth.com/" for the international client. 72 | /// 73 | [PhpProperty("url")] 74 | public required string PrimaryDownloadURL { get; set; } 75 | 76 | /// 77 | /// The secondary download URL for the patch files. 78 | /// If no fallback option exists, this should have the same value as the "url" property of the PHP object. 79 | /// For the 32-bit HoN client, this used to be a backup FTP server. 80 | /// For the 64-bit HoN client, the same CDN URL was used. 81 | /// 82 | [PhpProperty("url2")] 83 | public required string SecondaryDownloadURL { get; set; } 84 | 85 | /// 86 | /// The SHA-1 hash of the zipped manifest file of the latest HoN client version. 87 | /// 88 | [PhpProperty("latest_manifest_checksum")] 89 | public required string LatestManifestArchiveSHA1Hash { get; set; } 90 | 91 | /// 92 | /// The size in bytes of the zipped manifest file of the latest HoN client version. 93 | /// 94 | [PhpProperty("latest_manifest_size")] 95 | public required string LatestManifestArchiveSizeInBytes { get; set; } 96 | } 97 | --------------------------------------------------------------------------------