├── .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 |
--------------------------------------------------------------------------------