From d170f315f911e7ba8b929ade897d494f3d2d3a3f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 00:51:43 +0000 Subject: [PATCH 01/11] fix: eliminate critical refund bug and harden startup/persistence config - SEC-01: refund compensation now uses CancellationToken.None and logs before attempting it, so a cancelled purchase can no longer leave a player permanently debited without their item. - RES-01: enable EnableRetryOnFailure on MySQL; adapt the three manual transaction sites (PlayerWalletGrain, GroupDirectoryGrain, RoomGrain.Trading) to run inside a retryable execution strategy with a fresh DbContext per attempt. - CFG-02: remove the dead DatabaseConfig.LoggingEnabled option; add an optional MySqlServerVersion so ServerVersion.Parse can replace AutoDetect (which opens a connection during DI configuration). - DATA-01: remove the duplicate WriteToDatabaseAsync call in PlayerGrain.SetFigureAsync. - HC-08/HC-09: shared appsettings.json no longer ships a Windows asset path or Debug-level logging as the cross-platform/production default. - CODE-02: fix exceptions that passed a message as paramName (PackageHandler, Rc4Engine); fix two lingering nullability warnings (RentableSpaceGrain, DashboardEndpoints) so the build is warning-free. - NET-01/NET-02: PackageHandler's IVortexContextAccessor/IErrorGroupingSink are now required dependencies wired through both network transports instead of silently-null optionals; add a Vortex.packet.dropped metric so packets that vanish during encoding are now visible. - RevisionManager: use a ConcurrentDictionary and throw on a null revision instead of failing silently. - IRoomGrain: mark pure-read methods [AlwaysInterleave] so they don't queue behind the 50ms room tick. - Remove a commented-out UsePingPong() call and switch a bootstrap log line to structured logging. All 564 existing tests pass; build is clean at 0 warnings. --- Vortex.Crypto/Rc4Engine.cs | 9 +- .../Hosting/DashboardEndpoints.cs | 5 +- .../Configuration/DatabaseConfig.cs | 8 +- .../Extensions/ServiceCollectionExtensions.cs | 16 +- Vortex.Main/Program.cs | 2 +- Vortex.Networking/NetworkManager.cs | 24 ++- Vortex.Networking/Package/PackageEncoder.cs | 19 ++- Vortex.Networking/Package/PackageHandler.cs | 37 +++-- .../Revisions/RevisionManager.cs | 11 +- Vortex.Observability/Metrics/VortexMetrics.cs | 14 ++ Vortex.Players/Grains/GroupDirectoryGrain.cs | 150 +++++++++++------- Vortex.Players/Grains/PlayerGrain.cs | 2 - Vortex.Players/Grains/PlayerWalletGrain.cs | 119 ++++++++------ Vortex.Players/Grains/RentableSpaceGrain.cs | 2 +- .../Observability/IVortexMetrics.cs | 2 + .../Wallet/WalletPurchaseExtensions.cs | 20 ++- Vortex.Primitives/Rooms/Grains/IRoomGrain.cs | 9 ++ Vortex.Rooms/Grains/RoomGrain.Trading.cs | 83 +++++----- appsettings.json | 7 +- 19 files changed, 352 insertions(+), 187 deletions(-) diff --git a/Vortex.Crypto/Rc4Engine.cs b/Vortex.Crypto/Rc4Engine.cs index 416ec858..481f1c57 100644 --- a/Vortex.Crypto/Rc4Engine.cs +++ b/Vortex.Crypto/Rc4Engine.cs @@ -47,9 +47,14 @@ int outputOffset ArgumentNullException.ThrowIfNull(outputData); ArgumentOutOfRangeException.ThrowIfNegative(length); - if (inputOffset < 0 || outputOffset < 0) + if (inputOffset < 0) { - throw new ArgumentOutOfRangeException("Offsets must be non-negative."); + throw new ArgumentOutOfRangeException(nameof(inputOffset), "Offset must be non-negative."); + } + + if (outputOffset < 0) + { + throw new ArgumentOutOfRangeException(nameof(outputOffset), "Offset must be non-negative."); } if (inputData.Length - inputOffset < length) diff --git a/Vortex.Dashboard.API/Hosting/DashboardEndpoints.cs b/Vortex.Dashboard.API/Hosting/DashboardEndpoints.cs index 92ef389f..20c7ef19 100644 --- a/Vortex.Dashboard.API/Hosting/DashboardEndpoints.cs +++ b/Vortex.Dashboard.API/Hosting/DashboardEndpoints.cs @@ -200,9 +200,12 @@ public static void MapMeta(WebApplication app) methods = ["GET"]; } - string?[] capabilities = endpoint + // OfType() drops null policies *and* yields a non-nullable element + // type, matching the ApiRouteDescriptor.Capabilities: string[] parameter. + string[] capabilities = endpoint .Metadata.OfType() .Select(auth => auth.Policy) + .OfType() .Where(policy => !string.IsNullOrWhiteSpace(policy)) .Distinct() .OrderBy(policy => policy) diff --git a/Vortex.Database/Configuration/DatabaseConfig.cs b/Vortex.Database/Configuration/DatabaseConfig.cs index b14d70e6..194e135e 100644 --- a/Vortex.Database/Configuration/DatabaseConfig.cs +++ b/Vortex.Database/Configuration/DatabaseConfig.cs @@ -5,5 +5,11 @@ public class DatabaseConfig public const string SECTION_NAME = "Vortex:Database"; public string ConnectionString { get; init; } = string.Empty; - public bool LoggingEnabled { get; init; } = false; + + /// + /// Explicit MySQL server version (e.g. "8.0.36-mysql"), so ServerVersion.Parse can be used + /// instead of ServerVersion.AutoDetect, which opens a connection during DI configuration + /// itself. Left unset, the driver falls back to auto-detection. + /// + public string? MySqlServerVersion { get; init; } } diff --git a/Vortex.Database/Extensions/ServiceCollectionExtensions.cs b/Vortex.Database/Extensions/ServiceCollectionExtensions.cs index 596210b8..1eed67bc 100644 --- a/Vortex.Database/Extensions/ServiceCollectionExtensions.cs +++ b/Vortex.Database/Extensions/ServiceCollectionExtensions.cs @@ -31,14 +31,14 @@ HostApplicationBuilder builder { DatabaseConfig dbConfig = sp.GetRequiredService>().Value; string connectionString = dbConfig.ConnectionString; - bool loggingEnabled = dbConfig.LoggingEnabled; options.UseMySql( connectionString, - ServerVersion.AutoDetect(connectionString), + ResolveServerVersion(dbConfig, connectionString), options => { options.MigrationsAssembly("Vortex.Database"); + options.EnableRetryOnFailure(maxRetryCount: 3); } ); } @@ -47,6 +47,14 @@ HostApplicationBuilder builder return services; } + private static ServerVersion ResolveServerVersion( + DatabaseConfig dbConfig, + string connectionString + ) => + string.IsNullOrWhiteSpace(dbConfig.MySqlServerVersion) + ? ServerVersion.AutoDetect(connectionString) + : ServerVersion.Parse(dbConfig.MySqlServerVersion); + public static IServiceCollection AddPluginTablePrefix( this IServiceCollection services ) @@ -100,16 +108,16 @@ this IServiceCollection services IHostServices host = sp.GetRequiredService(); DatabaseConfig dbConfig = host.GetRequiredService>().Value; string connectionString = dbConfig.ConnectionString; - bool loggingEnabled = dbConfig.LoggingEnabled; options.UseMySql( connectionString, - ServerVersion.AutoDetect(connectionString), + ResolveServerVersion(dbConfig, connectionString), builder => { builder.MigrationsHistoryTable( $"__EFMigrationsHistory_{prefix().TrimEnd('_')}" ); + builder.EnableRetryOnFailure(maxRetryCount: 3); } ); } diff --git a/Vortex.Main/Program.cs b/Vortex.Main/Program.cs index 5d8b7cf4..98659a4e 100644 --- a/Vortex.Main/Program.cs +++ b/Vortex.Main/Program.cs @@ -88,7 +88,7 @@ IConfigurationProvider p in ((IConfigurationRoot)builder.Configuration).Provider IFileInfo? fi = fileProvider?.GetFileInfo(path); string physical = fi?.PhysicalPath ?? ""; - bootstrapLogger.LogInformation($"Json: '{path}' -> {physical}"); + bootstrapLogger.LogInformation("Json: '{Path}' -> {Physical}", path, physical); } } } diff --git a/Vortex.Networking/NetworkManager.cs b/Vortex.Networking/NetworkManager.cs index db49da55..3aede1ad 100644 --- a/Vortex.Networking/NetworkManager.cs +++ b/Vortex.Networking/NetworkManager.cs @@ -22,6 +22,7 @@ using Vortex.Primitives.Messages.Outgoing.Handshake; using Vortex.Primitives.Networking; using Vortex.Primitives.Networking.Revisions; +using Vortex.Primitives.Observability; using Vortex.Primitives.Packets; namespace Vortex.Networking; @@ -32,7 +33,10 @@ public sealed class NetworkManager( IRevisionManager revisionManager, MessageSystem messageSystem, ILoggerFactory loggerFactory, - IGrainFactory grainFactory + IGrainFactory grainFactory, + IVortexContextAccessor contextAccessor, + IErrorGroupingSink errorSink, + IVortexMetrics metrics ) : INetworkManager { private readonly NetworkingConfig _config = config.Value; @@ -40,9 +44,13 @@ IGrainFactory grainFactory private readonly ILogger _logger = loggerFactory.CreateLogger(); private readonly ILoggerFactory _loggerFactory = loggerFactory; private readonly MessageSystem _messageSystem = messageSystem; + private readonly IVortexContextAccessor _contextAccessor = contextAccessor; + private readonly IErrorGroupingSink _errorSink = errorSink; + private readonly IVortexMetrics _metrics = metrics; private readonly IPackageEncoder _packageEncoder = new PackageEncoder( revisionManager, - loggerFactory.CreateLogger() + loggerFactory.CreateLogger(), + metrics ); private readonly IRevisionManager _revisionManager = revisionManager; private readonly ISessionGateway _sessionGateway = sessionGateway; @@ -123,7 +131,6 @@ private void CreateTcpSocket() builder.UseSession(); builder.UsePipelineFilter(); builder.UseSessionGateway(); - //builder.UsePingPong(); _tcpHost = builder.Build(); } @@ -140,7 +147,9 @@ private void CreateWsSocket() PackageHandler packageHandler = new( _revisionManager, _messageSystem, - _loggerFactory.CreateLogger() + _loggerFactory.CreateLogger(), + _contextAccessor, + _errorSink ); WsPackageHandler wsPackageHandler = new( decoder, @@ -302,6 +311,13 @@ private void ConfigureCommonServices(IServiceCollection services) services.AddSingleton(_messageSystem); services.AddSingleton(_loggerFactory); services.AddSingleton(_grainFactory); + // Forwarded from the parent container so PackageHandler/PackageEncoder resolve these as + // required dependencies in the TCP host's own child container too, instead of silently + // getting null (see NET-01): a missing observability sink must be a startup error, not an + // invisible gap in the dashboard's error grouping. + services.AddSingleton(_contextAccessor); + services.AddSingleton(_errorSink); + services.AddSingleton(_metrics); services.AddSingleton, PackageEncoder>(); services.AddSingleton, PackageHandler>(); services.AddSingleton(_ => new ClientPacketDecoder( diff --git a/Vortex.Networking/Package/PackageEncoder.cs b/Vortex.Networking/Package/PackageEncoder.cs index 1727a7c2..64b2cc71 100644 --- a/Vortex.Networking/Package/PackageEncoder.cs +++ b/Vortex.Networking/Package/PackageEncoder.cs @@ -4,15 +4,20 @@ using SuperSocket.ProtoBase; using Vortex.Primitives.Networking; using Vortex.Primitives.Networking.Revisions; +using Vortex.Primitives.Observability; using Vortex.Primitives.Packets; namespace Vortex.Networking.Package; -public sealed class PackageEncoder(IRevisionManager revisionManager, ILogger logger) - : IPackageEncoder +public sealed class PackageEncoder( + IRevisionManager revisionManager, + ILogger logger, + IVortexMetrics metrics +) : IPackageEncoder { private readonly IRevisionManager _revisionManager = revisionManager; private readonly ILogger _logger = logger; + private readonly IVortexMetrics _metrics = metrics; public int Encode(IBufferWriter writer, OutgoingPackage pack) { @@ -46,8 +51,16 @@ public int Encode(IBufferWriter writer, OutgoingPackage pack) composerType.Name, pack.Session.SessionKey ); + + // The packet silently vanishes here: SuperSocket writes nothing when Encode + // returns 0, so this counter is the only trace that it ever happened. + _metrics.PacketDropped("serializer_not_found"); } } + else + { + _metrics.PacketDropped("revision_not_found"); + } } catch (Exception ex) { @@ -57,6 +70,8 @@ public int Encode(IBufferWriter writer, OutgoingPackage pack) pack.Composer.GetType().Name, pack.Session.SessionKey ); + + _metrics.PacketDropped("serialize_exception"); } return 0; diff --git a/Vortex.Networking/Package/PackageHandler.cs b/Vortex.Networking/Package/PackageHandler.cs index 98d1c1f1..d12c342e 100644 --- a/Vortex.Networking/Package/PackageHandler.cs +++ b/Vortex.Networking/Package/PackageHandler.cs @@ -16,12 +16,12 @@ public sealed class PackageHandler( IRevisionManager revisionManager, MessageSystem messageSystem, ILogger logger, - IVortexContextAccessor? contextAccessor = null, - IErrorGroupingSink? errorSink = null + IVortexContextAccessor contextAccessor, + IErrorGroupingSink errorSink ) : IPackageHandler { - private readonly IVortexContextAccessor? _contextAccessor = contextAccessor; - private readonly IErrorGroupingSink? _errorSink = errorSink; + private readonly IVortexContextAccessor _contextAccessor = contextAccessor; + private readonly IErrorGroupingSink _errorSink = errorSink; private readonly ILogger _logger = logger; private readonly MessageSystem _messageSystem = messageSystem; private readonly IRevisionManager _revisionManager = revisionManager; @@ -51,7 +51,9 @@ CancellationToken ct { IRevision revision = _revisionManager.GetRevision(ctx.RevisionId) - ?? throw new ArgumentNullException("No revision set"); + ?? throw new InvalidOperationException( + $"No revision registered for revision id '{ctx.RevisionId}'." + ); if (revision.Parsers.TryGetValue(packet.Header, out IParser? parser)) { @@ -79,21 +81,18 @@ CancellationToken ct ctx.SessionKey ); - IVortexContext? context = _contextAccessor?.Current; + IVortexContext? context = _contextAccessor.Current; - if (_errorSink is not null) - { - _errorSink.Record( - ex, - "package-handler", - $"packet:{packet.Header}", - context?.PlayerId, - context?.RoomId, - context?.CorrelationId.Value, - context?.SessionKey, - ctx.RemoteIpAddress - ); - } + _errorSink.Record( + ex, + "package-handler", + $"packet:{packet.Header}", + context?.PlayerId, + context?.RoomId, + context?.CorrelationId.Value, + context?.SessionKey, + ctx.RemoteIpAddress + ); } } } diff --git a/Vortex.Networking/Revisions/RevisionManager.cs b/Vortex.Networking/Revisions/RevisionManager.cs index 4077a03a..060dd574 100644 --- a/Vortex.Networking/Revisions/RevisionManager.cs +++ b/Vortex.Networking/Revisions/RevisionManager.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using Microsoft.Extensions.Logging; using Vortex.Primitives.Networking.Revisions; @@ -8,7 +10,9 @@ public sealed class RevisionManager(ILogger logger) : IRevision { private readonly ILogger _logger = logger; - public IDictionary Revisions { get; } = new Dictionary(); + public IDictionary Revisions { get; } = + new ConcurrentDictionary(); + public string DefaultRevisionId { get; private set; } = string.Empty; public IRevision? GetRevision(string revisionId) => @@ -16,10 +20,7 @@ public sealed class RevisionManager(ILogger logger) : IRevision public void RegisterRevision(IRevision revision) { - if (revision is null) - { - return; - } + ArgumentNullException.ThrowIfNull(revision); _logger.LogInformation("Revision Registered: {Revision}", revision.Revision); diff --git a/Vortex.Observability/Metrics/VortexMetrics.cs b/Vortex.Observability/Metrics/VortexMetrics.cs index 019179c4..5b84037d 100644 --- a/Vortex.Observability/Metrics/VortexMetrics.cs +++ b/Vortex.Observability/Metrics/VortexMetrics.cs @@ -22,6 +22,7 @@ public sealed class VortexMetrics : IVortexMetrics, IDisposable private readonly Counter _packetReceived; private readonly Histogram _packetDuration; private readonly Counter _packetFailed; + private readonly Counter _packetDropped; private readonly ILiveStatsAggregator _liveStats; public VortexMetrics( @@ -49,6 +50,11 @@ IOptions options unit: "{packet}", description: "Packets whose dispatch threw an exception." ); + _packetDropped = _meter.CreateCounter( + "Vortex.packet.dropped", + unit: "{packet}", + description: "Outgoing packets that could not be encoded and were never sent." + ); } public void PacketReceived(string operation, long? actorId = null, int? roomId = null) @@ -86,6 +92,14 @@ public void PacketFailed(string operation, long? actorId = null, int? roomId = n _liveStats.RecordPacketFailed(operation, actorId, roomId); } + public void PacketDropped(string reason) + { + if (_enabled) + { + _packetDropped.Add(1, new KeyValuePair("reason", reason)); + } + } + private static KeyValuePair Tag(string operation) => new("operation", operation); diff --git a/Vortex.Players/Grains/GroupDirectoryGrain.cs b/Vortex.Players/Grains/GroupDirectoryGrain.cs index 9f187d4b..e3d88ffd 100644 --- a/Vortex.Players/Grains/GroupDirectoryGrain.cs +++ b/Vortex.Players/Grains/GroupDirectoryGrain.cs @@ -94,8 +94,6 @@ CancellationToken ct { int ownerId = owner.Value; - await using VortexDbContext dbCtx = await dbCtxFactory.CreateDbContextAsync(ct); - int creationCost = await grainFactory .GetServerConfigGrain() .GetIntAsync( @@ -104,21 +102,32 @@ CancellationToken ct ) .ConfigureAwait(true); - RoomEntity? room = await dbCtx.Rooms.FirstOrDefaultAsync( - r => r.Id == baseRoomId && r.DeletedAt == null, - ct - ); - - // Must own the room, and the room must not already be a guild base. - if (room is null || room.PlayerEntityId != ownerId || room.GroupEntityId != null) + // Preflight check on its own short-lived context, purely to fail fast (and avoid debiting + // the wallet) before ever publishing the cancellable event below. The authoritative check + // happens again inside the retryable transaction further down. + await using ( + VortexDbContext preflightCtx = await dbCtxFactory.CreateDbContextAsync(ct) + ) { - return null; - } + RoomEntity? preflightRoom = await preflightCtx + .Rooms.AsNoTracking() + .FirstOrDefaultAsync(r => r.Id == baseRoomId && r.DeletedAt == null, ct); + + // Must own the room, and the room must not already be a guild base. + if ( + preflightRoom is null + || preflightRoom.PlayerEntityId != ownerId + || preflightRoom.GroupEntityId != null + ) + { + return null; + } - PlayerEntity? ownerEntity = await dbCtx.Players.FindAsync([ownerId], ct); - if (ownerEntity is null) - { - return null; + bool ownerExists = await preflightCtx.Players.AnyAsync(p => p.Id == ownerId, ct); + if (!ownerExists) + { + return null; + } } EventContext creatingContext = await cancellableEvents @@ -140,6 +149,12 @@ CancellationToken ct return null; } + // A fresh DbContext is created per attempt (rather than shared across retries) so that a + // transient failure retried by the MySQL execution strategy never resubmits entities left + // half-tracked by a rolled-back attempt. + await using VortexDbContext strategyProbe = await dbCtxFactory.CreateDbContextAsync(ct); + IExecutionStrategy strategy = strategyProbe.Database.CreateExecutionStrategy(); + // Charge the creation cost through the shared debit-then-grant executor, exactly like every // other catalog purchase: the wallet writes the economy ledger entry, emits // CurrencyChangedEvent and pushes the new balance, and if the insert below throws (most @@ -154,48 +169,77 @@ CancellationToken ct Amount = creationCost, }, ], - async token => - { - // The group row and the room back-link must land together: a committed group - // whose room never got linked would permanently burn that room as a guild base. - await using IDbContextTransaction tx = await dbCtx - .Database.BeginTransactionAsync(token) - .ConfigureAwait(true); - - GroupEntity created = GroupFactory.Create( - name, - BuildBadgeCode(badgeParts), - baseRoomId, - ownerId, - GroupType.Open, - primaryColorId.ToString(), - secondaryColorId.ToString(), - string.IsNullOrEmpty(description) ? null : description - ); - created.RoomEntity = room; - created.OwnerPlayerEntity = ownerEntity; - - dbCtx.Groups.Add(created); - await dbCtx.SaveChangesAsync(token).ConfigureAwait(true); - - // Link the room back to the group and enrol the owner as an admin member. - room.GroupEntityId = created.Id; - dbCtx.GroupMembers.Add( - new GroupMemberEntity + token => + strategy.ExecuteAsync(async () => + { + await using VortexDbContext dbCtx = await dbCtxFactory.CreateDbContextAsync( + token + ); + + RoomEntity? room = await dbCtx.Rooms.FirstOrDefaultAsync( + r => r.Id == baseRoomId && r.DeletedAt == null, + token + ); + + if (room is null || room.PlayerEntityId != ownerId || room.GroupEntityId != null) { - GroupEntityId = created.Id, - PlayerEntityId = ownerId, - Rank = GroupMemberRank.Admin, - GroupEntity = created, - PlayerEntity = ownerEntity, + throw new InvalidOperationException( + $"Room {baseRoomId} is no longer a valid guild base for player {ownerId}." + ); } - ); - await dbCtx.SaveChangesAsync(token).ConfigureAwait(true); - await tx.CommitAsync(token).ConfigureAwait(true); + PlayerEntity? ownerEntity = await dbCtx.Players.FindAsync( + [ownerId], + token + ); + + if (ownerEntity is null) + { + throw new InvalidOperationException( + $"Player {ownerId} no longer exists." + ); + } - return created.Id; - }, + // The group row and the room back-link must land together: a committed group + // whose room never got linked would permanently burn that room as a guild base. + await using IDbContextTransaction tx = await dbCtx + .Database.BeginTransactionAsync(token) + .ConfigureAwait(true); + + GroupEntity created = GroupFactory.Create( + name, + BuildBadgeCode(badgeParts), + baseRoomId, + ownerId, + GroupType.Open, + primaryColorId.ToString(), + secondaryColorId.ToString(), + string.IsNullOrEmpty(description) ? null : description + ); + created.RoomEntity = room; + created.OwnerPlayerEntity = ownerEntity; + + dbCtx.Groups.Add(created); + await dbCtx.SaveChangesAsync(token).ConfigureAwait(true); + + // Link the room back to the group and enrol the owner as an admin member. + room.GroupEntityId = created.Id; + dbCtx.GroupMembers.Add( + new GroupMemberEntity + { + GroupEntityId = created.Id, + PlayerEntityId = ownerId, + Rank = GroupMemberRank.Admin, + GroupEntity = created, + PlayerEntity = ownerEntity, + } + ); + + await dbCtx.SaveChangesAsync(token).ConfigureAwait(true); + await tx.CommitAsync(token).ConfigureAwait(true); + + return created.Id; + }), logger, ct ) diff --git a/Vortex.Players/Grains/PlayerGrain.cs b/Vortex.Players/Grains/PlayerGrain.cs index 5aa7e489..e612f4a8 100644 --- a/Vortex.Players/Grains/PlayerGrain.cs +++ b/Vortex.Players/Grains/PlayerGrain.cs @@ -67,8 +67,6 @@ public async Task SetFigureAsync(string figure, AvatarGenderType gender, Cancell await playerPresence.OnFigureUpdatedAsync(await GetSummaryAsync(ct), ct); - await WriteToDatabaseAsync(ct); - await _events .PublishAsync(new PlayerFigureChangedEvent(_state.PlayerId, figure), ct) .ConfigureAwait(true); diff --git a/Vortex.Players/Grains/PlayerWalletGrain.cs b/Vortex.Players/Grains/PlayerWalletGrain.cs index 2f030934..46ae9b3a 100644 --- a/Vortex.Players/Grains/PlayerWalletGrain.cs +++ b/Vortex.Players/Grains/PlayerWalletGrain.cs @@ -62,65 +62,90 @@ CancellationToken ct && normalizedRequests.Count > 0 ) { - await using VortexDbContext dbCtx = await _dbCtxFactory + // A fresh DbContext is created per attempt (rather than shared across retries) so that a + // transient failure retried by the MySQL execution strategy never resubmits state left + // half-applied by a rolled-back attempt. + await using VortexDbContext strategyProbe = await _dbCtxFactory .CreateDbContextAsync(ct) .ConfigureAwait(true); - await using IDbContextTransaction tx = await dbCtx - .Database.BeginTransactionAsync(ct) - .ConfigureAwait(true); - - List updates = new List( - normalizedRequests.Count - ); + IExecutionStrategy strategy = strategyProbe.Database.CreateExecutionStrategy(); - foreach (WalletDebitRequest request in normalizedRequests) - { - try + ( + WalletDebitFailure? failure, + List updates + ) = await strategy + .ExecuteAsync(async () => { - WalletCurrencyUpdateSnapshot update = await ProcessDebitRequestAsync( - dbCtx, - request, - ct + await using VortexDbContext dbCtx = await _dbCtxFactory + .CreateDbContextAsync(ct) + .ConfigureAwait(true); + await using IDbContextTransaction tx = await dbCtx + .Database.BeginTransactionAsync(ct) + .ConfigureAwait(true); + + List attemptUpdates = new List( + normalizedRequests.Count ); - if (update.ChangedBy != request.Amount) + foreach (WalletDebitRequest request in normalizedRequests) { - // Specific type (CA2201): a bare Exception cannot be caught selectively, and - // this is a wallet invariant breach — the amount actually debited did not - // match what was asked for — not an arbitrary failure. - throw new InvalidOperationException( - $"Wallet debit changed {update.ChangedBy} but {request.Amount} was " - + $"requested for {request.CurrencyKind.CurrencyType}." - ); + try + { + WalletCurrencyUpdateSnapshot update = await ProcessDebitRequestAsync( + dbCtx, + request, + ct + ); + + if (update.ChangedBy != request.Amount) + { + // Specific type (CA2201): a bare Exception cannot be caught selectively, and + // this is a wallet invariant breach — the amount actually debited did not + // match what was asked for — not an arbitrary failure. + throw new InvalidOperationException( + $"Wallet debit changed {update.ChangedBy} but {request.Amount} was " + + $"requested for {request.CurrencyKind.CurrencyType}." + ); + } + + attemptUpdates.Add(update); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Wallet debit failed for player {PlayerId} ({Currency} x{Amount})", + this.GetPrimaryKeyLong(), + request.CurrencyKind.CurrencyType, + request.Amount + ); + + await tx.RollbackAsync(ct); + + return ( + new WalletDebitFailure + { + CurrencyKind = request.CurrencyKind, + Amount = request.Amount, + }, + attemptUpdates + ); + } } - updates.Add(update); - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Wallet debit failed for player {PlayerId} ({Currency} x{Amount})", - this.GetPrimaryKeyLong(), - request.CurrencyKind.CurrencyType, - request.Amount - ); + await dbCtx.SaveChangesAsync(ct); + await tx.CommitAsync(ct); - await tx.RollbackAsync(ct); - await RollbackUpdatesAsync(updates, ct); + return ((WalletDebitFailure?)null, attemptUpdates); + }) + .ConfigureAwait(true); - return WalletDebitResult.InsufficientBalance( - new WalletDebitFailure - { - CurrencyKind = request.CurrencyKind, - Amount = request.Amount, - } - ); - } - } + if (failure is not null) + { + await RollbackUpdatesAsync(updates, ct); - await dbCtx.SaveChangesAsync(ct); - await tx.CommitAsync(ct); + return WalletDebitResult.InsufficientBalance(failure); + } IPlayerPresenceGrain playerPresence = _grainFactory.GetPlayerPresenceGrain( (int)this.GetPrimaryKeyLong() diff --git a/Vortex.Players/Grains/RentableSpaceGrain.cs b/Vortex.Players/Grains/RentableSpaceGrain.cs index 1ee51007..25138578 100644 --- a/Vortex.Players/Grains/RentableSpaceGrain.cs +++ b/Vortex.Players/Grains/RentableSpaceGrain.cs @@ -335,7 +335,7 @@ public async Task GetConfigAsync(CancellationToken List currencies = await db .CurrencyTypes.Where(c => c.Enabled && c.DeletedAt == null) .OrderBy(c => c.Id) - .Select(c => new AvailableCurrencySnapshot { Id = c.Id, Name = c.Name }) + .Select(c => new AvailableCurrencySnapshot { Id = c.Id, Name = c.Name ?? string.Empty }) .ToListAsync(ct); return new RentableSpaceConfigSnapshot diff --git a/Vortex.Primitives/Observability/IVortexMetrics.cs b/Vortex.Primitives/Observability/IVortexMetrics.cs index 82476f92..3d137fc6 100644 --- a/Vortex.Primitives/Observability/IVortexMetrics.cs +++ b/Vortex.Primitives/Observability/IVortexMetrics.cs @@ -17,4 +17,6 @@ void PacketCompleted( ); void PacketFailed(string operation, long? actorId = null, int? roomId = null); + + void PacketDropped(string reason); } diff --git a/Vortex.Primitives/Players/Wallet/WalletPurchaseExtensions.cs b/Vortex.Primitives/Players/Wallet/WalletPurchaseExtensions.cs index 7d18b5a8..5d2c7743 100644 --- a/Vortex.Primitives/Players/Wallet/WalletPurchaseExtensions.cs +++ b/Vortex.Primitives/Players/Wallet/WalletPurchaseExtensions.cs @@ -56,13 +56,27 @@ CancellationToken ct { if (debitRequests.Count > 0) { - await wallet.CreditBackAsync(debitRequests, ct).ConfigureAwait(false); - logger.LogError( ex, - "Purchase grant failed after wallet debit; refunded {RequestCount} debited amount(s).", + "Purchase grant failed after wallet debit; refunding {RequestCount} debited amount(s).", debitRequests.Count ); + + // The compensation must not be subject to the token of the operation it is + // compensating for: cancellation is the most common reason grantAsync throws + // (client disconnect, host shutdown, timeout), and using `ct` here would make + // the refund fail in exactly that case, leaving the player permanently debited. + try + { + await wallet.CreditBackAsync(debitRequests, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception refundEx) + { + logger.LogCritical( + refundEx, + "REFUND FAILED after a failed grant - player may be permanently debited." + ); + } } throw; diff --git a/Vortex.Primitives/Rooms/Grains/IRoomGrain.cs b/Vortex.Primitives/Rooms/Grains/IRoomGrain.cs index 78185af1..7a1730c8 100644 --- a/Vortex.Primitives/Rooms/Grains/IRoomGrain.cs +++ b/Vortex.Primitives/Rooms/Grains/IRoomGrain.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Orleans; +using Orleans.Concurrency; using Vortex.Primitives.Networking; using Vortex.Primitives.Orleans.Snapshots.Room; using Vortex.Primitives.Rooms.Events; @@ -14,9 +15,17 @@ public partial interface IRoomGrain : IGrainWithIntegerKey public Task DeactivateRoomAsync(); public Task DelayRoomDeactivationAsync(); public Task EnsureRoomActiveAsync(CancellationToken ct); + + // Pure reads: they never mutate _state, so letting them interleave with the room tick (and each + // other) keeps them off the queue behind a 50ms tick instead of serializing on it. + [AlwaysInterleave] public Task GetSnapshotAsync(); + + [AlwaysInterleave] public Task GetSummaryAsync(); public Task GetRoomPopulationAsync(); + + [AlwaysInterleave] public Task>> GetRoomPropertiesAsync(); public Task PublishRoomEventAsync(RoomEvent evt, CancellationToken ct); public Task SendComposerToRoomAsync(IComposer composer); diff --git a/Vortex.Rooms/Grains/RoomGrain.Trading.cs b/Vortex.Rooms/Grains/RoomGrain.Trading.cs index abe47888..a8fec83d 100644 --- a/Vortex.Rooms/Grains/RoomGrain.Trading.cs +++ b/Vortex.Rooms/Grains/RoomGrain.Trading.cs @@ -425,50 +425,57 @@ CancellationToken ct int oneOwner = session.UserOneId.Value; int twoOwner = session.UserTwoId.Value; - VortexDbContext dbCtx = await _dbCtxFactory.CreateDbContextAsync(ct).ConfigureAwait(true); - - try - { - await using IDbContextTransaction tx = await dbCtx - .Database.BeginTransactionAsync(ct) - .ConfigureAwait(true); - - List oneRows = await dbCtx - .Furnitures.Where(f => oneIds.Contains(f.Id)) - .ToListAsync(ct) - .ConfigureAwait(true); - List twoRows = await dbCtx - .Furnitures.Where(f => twoIds.Contains(f.Id)) - .ToListAsync(ct) - .ConfigureAwait(true); + // A fresh DbContext is created per attempt (rather than shared across retries) so that a + // transient failure retried by the MySQL execution strategy never resubmits entities left + // half-tracked by a rolled-back attempt. + await using VortexDbContext strategyProbe = await _dbCtxFactory + .CreateDbContextAsync(ct) + .ConfigureAwait(true); + IExecutionStrategy strategy = strategyProbe.Database.CreateExecutionStrategy(); - if ( - !IsOfferPersistable(oneRows, oneIds.Count, oneOwner) - || !IsOfferPersistable(twoRows, twoIds.Count, twoOwner) - ) + return await strategy + .ExecuteAsync(async () => { - return false; - } + await using VortexDbContext dbCtx = await _dbCtxFactory + .CreateDbContextAsync(ct) + .ConfigureAwait(true); + await using IDbContextTransaction tx = await dbCtx + .Database.BeginTransactionAsync(ct) + .ConfigureAwait(true); + + List oneRows = await dbCtx + .Furnitures.Where(f => oneIds.Contains(f.Id)) + .ToListAsync(ct) + .ConfigureAwait(true); + List twoRows = await dbCtx + .Furnitures.Where(f => twoIds.Contains(f.Id)) + .ToListAsync(ct) + .ConfigureAwait(true); + + if ( + !IsOfferPersistable(oneRows, oneIds.Count, oneOwner) + || !IsOfferPersistable(twoRows, twoIds.Count, twoOwner) + ) + { + return false; + } - foreach (FurnitureEntity row in oneRows) - { - row.PlayerEntityId = twoOwner; - } + foreach (FurnitureEntity row in oneRows) + { + row.PlayerEntityId = twoOwner; + } - foreach (FurnitureEntity row in twoRows) - { - row.PlayerEntityId = oneOwner; - } + foreach (FurnitureEntity row in twoRows) + { + row.PlayerEntityId = oneOwner; + } - await dbCtx.SaveChangesAsync(ct).ConfigureAwait(true); - await tx.CommitAsync(ct).ConfigureAwait(true); + await dbCtx.SaveChangesAsync(ct).ConfigureAwait(true); + await tx.CommitAsync(ct).ConfigureAwait(true); - return true; - } - finally - { - await dbCtx.DisposeAsync().ConfigureAwait(true); - } + return true; + }) + .ConfigureAwait(true); } private static bool IsOfferPersistable( diff --git a/appsettings.json b/appsettings.json index 5c25d149..9d5cb305 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,7 +6,7 @@ "Microsoft.Hosting.Lifetime": "Warning", "SuperSocket": "Warning", "Orleans": "Warning", - "Vortex": "Debug", + "Vortex": "Information", "Vortex.Networking.Package.PackageHandler": "Warning", "Vortex.Networking.Package.PackageEncoder": "Warning" }, @@ -50,8 +50,7 @@ "GatewayPort": 3000 }, "Database": { - "ConnectionString": "CHANGE_ME__set-via-VORTEX__Vortex__Database__ConnectionString-env-var-or-user-secrets", - "LoggingEnabled": true + "ConnectionString": "CHANGE_ME__set-via-VORTEX__Vortex__Database__ConnectionString-env-var-or-user-secrets" }, "Game": {}, "Authentication": { @@ -73,7 +72,7 @@ "TargetedOfferImageUrlTemplate": "http://vortex-assets.local/c_images/targetedoffers/{file}", "AvatarImageUrlTemplate": "http://vortex-assets.local/habbo-imaging/avatarimage?figure={figure}&headonly=1&size=m&direction=2&head_direction=2", "GroupBadgeUrlTemplate": "http://vortex-assets.local/c_images/Badgeparts/generated/{badge}.gif", - "AssetsLocalRoot": "C:\\Laragon\\www\\vortex-assets" + "AssetsLocalRoot": "./assets" }, "WebApi": { "Enabled": true, From 07ff1e5c2e528804fc13b89020362b5d8bf53d9a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 01:03:36 +0000 Subject: [PATCH 02/11] fix: replace fire-and-forget async calls, remove dead Orleans storage, enable build phase 2 - ASY-01: replace ~28 unsupervised `_ = ...Async()` call sites with either `await` (message handler responses to the client) or the existing LogAndForget helper (room broadcasts and grain-turn side effects, where awaiting would cost a tick). The two self-supervising heartbeat loops and the LogAndForget helper's own internal fire-and-forget are intentionally left untouched. - ORL-01: remove the PLAYER_STORE/ROOM_STORE grain storage registrations and their constants - no grain ever used [PersistentState] against them; every grain persists through EF Core. Only PubSubStore (used by the stream providers) remains. - Flip VortexAIPolicyPhase to 2 by default now that the build is warning-free, promoting CS8602/CS8604/CS8618/CA2000/CA2012/CA2201/ IDE0058 to build errors. IDE0005 is excluded: it only runs at build time with GenerateDocumentationFile enabled, which would force XML doc comments repo-wide against this project's own style. All 564 tests still pass; build is clean at 0 warnings under phase 2. --- Directory.Build.props | 10 +++-- .../HostApplicationBuilderExtensions.cs | 5 ++- .../OpenMessageHandler.cs | 2 +- .../UpdateActionMessageHandler.cs | 2 +- .../UpdateAddonMessageHandler.cs | 2 +- .../UpdateConditionMessageHandler.cs | 2 +- .../UpdateSelectorMessageHandler.cs | 2 +- .../UpdateTriggerMessageHandler.cs | 4 +- .../UpdateVariableMessageHandler.cs | 2 +- .../WiredClearErrorLogsMessageHandler.cs | 2 +- ...iredGetAllVariableHoldersMessageHandler.cs | 2 +- ...WiredGetAllVariablesDiffsMessageHandler.cs | 2 +- .../WiredGetAllVariablesHashMessageHandler.cs | 2 +- .../WiredGetErrorLogsMessageHandler.cs | 2 +- .../WiredGetRoomLogsMessageHandler.cs | 2 +- .../WiredGetRoomSettingsMessageHandler.cs | 2 +- .../WiredGetRoomStatsMessageHandler.cs | 2 +- ...GetUserPermanentVariablesMessageHandler.cs | 2 +- ...iredGetVariableOwnersPageMessageHandler.cs | 2 +- ...iredGetVariablesForObjectMessageHandler.cs | 2 +- ...redSetObjectVariableValueMessageHandler.cs | 2 +- .../WiredSetRoomSettingsMessageHandler.cs | 2 +- .../Orleans/OrleansStorageNames.cs | 2 - .../Grains/Modules/RoomObjectModule.cs | 9 ++-- .../Grains/Systems/RoomWiredSystem.cs | 44 +++++++++++-------- .../Floor/FurnitureGameTimerLogic.cs | 4 +- .../Wired/Actions/WiredActionChaseHabbo.cs | 26 ++++++----- .../Object/Logic/Furniture/FurnitureLogic.cs | 4 +- 28 files changed, 83 insertions(+), 63 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 38588533..edb1c55d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,13 @@ latest true false - - 1 - CS8602;CS8604;CS8618;CA2000;CA2012;CA2201;IDE0005;IDE0058 + + 2 + + CS8602;CS8604;CS8618;CA2000;CA2012;CA2201;IDE0058 $(WarningsAsErrors);ORLEANS0012;ORLEANS0013 entries = await room.GetWiredErrorLogsAsync(ct) .ConfigureAwait(false); - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredErrorLogsEventMessageComposer() { Entries = entries }, ct ) diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariableHoldersMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariableHoldersMessageHandler.cs index 4de145b3..9d477e57 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariableHoldersMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariableHoldersMessageHandler.cs @@ -42,7 +42,7 @@ await _grainFactory (new RoomObjectId(h.ObjectId), h.Value) ); - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredAllVariableHoldersEventMessageComposer { VariableSnapshot = result.Value.Variable, diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariablesDiffsMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariablesDiffsMessageHandler.cs index d3adec19..fde70322 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariablesDiffsMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariablesDiffsMessageHandler.cs @@ -68,7 +68,7 @@ CancellationToken ct diffs.Add(variable); } - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredAllVariablesDiffsEventMessageComposer() { AllVariablesHash = variables.AllVariablesHash, diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariablesHashMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariablesHashMessageHandler.cs index 8ab7667c..4cd3c676 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariablesHashMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetAllVariablesHashMessageHandler.cs @@ -30,7 +30,7 @@ CancellationToken ct .GetWiredVariablesSnapshotAsync(ct) .ConfigureAwait(false); - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredAllVariablesHashEventMessageComposer() { AllVariablesHash = variables.AllVariablesHash, diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetErrorLogsMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetErrorLogsMessageHandler.cs index 2b81846a..de6e5fbb 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetErrorLogsMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetErrorLogsMessageHandler.cs @@ -30,7 +30,7 @@ CancellationToken ct .GetWiredErrorLogsAsync(ct) .ConfigureAwait(false); - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredErrorLogsEventMessageComposer() { Entries = entries }, ct ) diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomLogsMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomLogsMessageHandler.cs index 745c2938..7fdf5394 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomLogsMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomLogsMessageHandler.cs @@ -36,6 +36,6 @@ CancellationToken ct ) .ConfigureAwait(false); - _ = ctx.SendComposerAsync(page, ct).ConfigureAwait(false); + await ctx.SendComposerAsync(page, ct).ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomSettingsMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomSettingsMessageHandler.cs index 62c94ad8..bb0e8d51 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomSettingsMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomSettingsMessageHandler.cs @@ -30,6 +30,6 @@ CancellationToken ct .GetWiredRoomSettingsAsync(new PlayerId(ctx.PlayerId), ct) .ConfigureAwait(false); - _ = ctx.SendComposerAsync(settings, ct).ConfigureAwait(false); + await ctx.SendComposerAsync(settings, ct).ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomStatsMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomStatsMessageHandler.cs index 23aabd06..7ce356bd 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomStatsMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetRoomStatsMessageHandler.cs @@ -29,6 +29,6 @@ CancellationToken ct .GetWiredRoomStatsAsync(ct) .ConfigureAwait(false); - _ = ctx.SendComposerAsync(stats, ct).ConfigureAwait(false); + await ctx.SendComposerAsync(stats, ct).ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetUserPermanentVariablesMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetUserPermanentVariablesMessageHandler.cs index 9981abd9..6ea7ec7e 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetUserPermanentVariablesMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetUserPermanentVariablesMessageHandler.cs @@ -35,7 +35,7 @@ CancellationToken ct ) .ConfigureAwait(false); - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredUserPermanentVariablesComposer() { Snapshot = snapshot }, ct ) diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetVariableOwnersPageMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetVariableOwnersPageMessageHandler.cs index 9fbafe84..674558f8 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetVariableOwnersPageMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetVariableOwnersPageMessageHandler.cs @@ -37,7 +37,7 @@ CancellationToken ct ) .ConfigureAwait(false); - _ = ctx.SendComposerAsync(new WiredUserVariablesListComposer() { Page = page }, ct) + await ctx.SendComposerAsync(new WiredUserVariablesListComposer() { Page = page }, ct) .ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetVariablesForObjectMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetVariablesForObjectMessageHandler.cs index 5c474d16..ac40d9c0 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetVariablesForObjectMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredGetVariablesForObjectMessageHandler.cs @@ -40,7 +40,7 @@ CancellationToken ct ) .ConfigureAwait(false); - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredVariablesForObjectEventMessageComposer() { TargetType = (WiredVariableTargetType)message.SourceType, diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredSetObjectVariableValueMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredSetObjectVariableValueMessageHandler.cs index 74ebbfdb..f9f589d3 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredSetObjectVariableValueMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredSetObjectVariableValueMessageHandler.cs @@ -37,7 +37,7 @@ CancellationToken ct ) .ConfigureAwait(false); - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredSetUserPermanentVariableResultComposer() { Success = success }, ct ) diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredSetRoomSettingsMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredSetRoomSettingsMessageHandler.cs index 675577a9..16b6edfd 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredSetRoomSettingsMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredSetRoomSettingsMessageHandler.cs @@ -42,6 +42,6 @@ CancellationToken ct return; } - _ = ctx.SendComposerAsync(settings, ct).ConfigureAwait(false); + await ctx.SendComposerAsync(settings, ct).ConfigureAwait(false); } } diff --git a/Vortex.Primitives/Orleans/OrleansStorageNames.cs b/Vortex.Primitives/Orleans/OrleansStorageNames.cs index a58435a6..caf36f73 100644 --- a/Vortex.Primitives/Orleans/OrleansStorageNames.cs +++ b/Vortex.Primitives/Orleans/OrleansStorageNames.cs @@ -3,6 +3,4 @@ namespace Vortex.Primitives.Orleans; public static class OrleansStorageNames { public const string PUB_SUB_STORE = "PubSubStore"; - public const string PLAYER_STORE = "PlayerStore"; - public const string ROOM_STORE = "RoomStore"; } diff --git a/Vortex.Rooms/Grains/Modules/RoomObjectModule.cs b/Vortex.Rooms/Grains/Modules/RoomObjectModule.cs index fd1910c8..465d2e15 100644 --- a/Vortex.Rooms/Grains/Modules/RoomObjectModule.cs +++ b/Vortex.Rooms/Grains/Modules/RoomObjectModule.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Vortex.Logging; +using Vortex.Logging.Extensions; using Vortex.Primitives; using Vortex.Primitives.Action; using Vortex.Primitives.Messages.Outgoing.Room.Engine; @@ -68,9 +69,11 @@ public async Task AttatchObjectAsync(IRoomObject roomObject, CancellationT await AttatchLogicAsync(avatar, ct); await _roomGrain.AvatarModule.ProcessNextAvatarStepAsync(avatar, ct); - _ = _roomGrain.SendComposerToRoomAsync( - new UsersMessageComposer { Avatars = [avatar.GetSnapshot()] } - ); + _roomGrain + .SendComposerToRoomAsync( + new UsersMessageComposer { Avatars = [avatar.GetSnapshot()] } + ) + .LogAndForget(_roomGrain._logger, "Failed to broadcast avatar attach."); break; } default: diff --git a/Vortex.Rooms/Grains/Systems/RoomWiredSystem.cs b/Vortex.Rooms/Grains/Systems/RoomWiredSystem.cs index 7d7a9754..4cb9072a 100644 --- a/Vortex.Rooms/Grains/Systems/RoomWiredSystem.cs +++ b/Vortex.Rooms/Grains/Systems/RoomWiredSystem.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Vortex.Logging.Extensions; using Vortex.Primitives.Action; using Vortex.Primitives.Messages.Outgoing.Room.Engine; using Vortex.Primitives.Rooms; @@ -360,7 +361,8 @@ trigger is null return; } - _ = ctx.Trigger.FlashActivationStateAsync(ct); + ctx.Trigger.FlashActivationStateAsync(ct) + .LogAndForget(_roomGrain._logger, "Failed to flash activation state for trigger."); // Before/AfterEffects addon hooks run in ExecuteStackChainAsync, around the chain's actual // execution — which can be ticks later than this scheduling when actions carry delays. @@ -533,11 +535,14 @@ action is FurnitureWiredLogic actionBox Signal = new WiredSelectionSet().UnionWith(pending.Signal), }; - _ = action.FlashActivationStateAsync(ct); + action + .FlashActivationStateAsync(ct) + .LogAndForget(_roomGrain._logger, "Failed to flash activation state for action."); await action.ExecuteAsync(ctx, ct); - _ = FlushWiredContextAsync(ctx); + FlushWiredContextAsync(ctx) + .LogAndForget(_roomGrain._logger, "Failed to flush wired execution context."); WriteWiredRoomLog( WiredLogLevel.Info, @@ -725,29 +730,32 @@ private Task FlushWiredContextAsync(WiredExecutionContext ctx) || ctx.WallItemMoves.Count > 0 ) { - _ = ctx.SendComposerToRoomAsync( - new WiredMovementsMessageComposer - { - Users = ctx.UserMoves, - FloorItems = ctx.FloorItemMoves, - WallItems = ctx.WallItemMoves, - UserDirections = ctx.UserDirections, - } - ); + ctx.SendComposerToRoomAsync( + new WiredMovementsMessageComposer + { + Users = ctx.UserMoves, + FloorItems = ctx.FloorItemMoves, + WallItems = ctx.WallItemMoves, + UserDirections = ctx.UserDirections, + } + ) + .LogAndForget(_roomGrain._logger, "Failed to broadcast wired movements."); } if (ctx.FloorItemStateUpdates.Count > 0) { - _ = ctx.SendComposerToRoomAsync( - new ObjectsDataUpdateMessageComposer { StuffDatas = ctx.FloorItemStateUpdates } - ); + ctx.SendComposerToRoomAsync( + new ObjectsDataUpdateMessageComposer { StuffDatas = ctx.FloorItemStateUpdates } + ) + .LogAndForget(_roomGrain._logger, "Failed to broadcast floor item state updates."); } if (ctx.WallItemStateUpdates.Count > 0) { - _ = ctx.SendComposerToRoomAsync( - new ItemsStateUpdateMessageComposer { ObjectStates = ctx.WallItemStateUpdates } - ); + ctx.SendComposerToRoomAsync( + new ItemsStateUpdateMessageComposer { ObjectStates = ctx.WallItemStateUpdates } + ) + .LogAndForget(_roomGrain._logger, "Failed to broadcast wall item state updates."); } return Task.CompletedTask; diff --git a/Vortex.Rooms/Object/Logic/Furniture/Floor/FurnitureGameTimerLogic.cs b/Vortex.Rooms/Object/Logic/Furniture/Floor/FurnitureGameTimerLogic.cs index b2cd803a..8dbe4323 100644 --- a/Vortex.Rooms/Object/Logic/Furniture/Floor/FurnitureGameTimerLogic.cs +++ b/Vortex.Rooms/Object/Logic/Furniture/Floor/FurnitureGameTimerLogic.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Vortex.Logging.Extensions; using Vortex.Primitives.Action; using Vortex.Primitives.Furniture.Enums; using Vortex.Primitives.Furniture.Providers; @@ -287,7 +288,8 @@ private void ApplyDisplay() { StuffData.SetState(_clock.RemainingSeconds.ToString(CultureInfo.InvariantCulture)); - _ = _ctx.RefreshStuffDataAsync(); + _ctx.RefreshStuffDataAsync() + .LogAndForget(_roomGrain._logger, "Failed to refresh game timer stuff data."); } private static int[] ParseSteps(string? customParams) diff --git a/Vortex.Rooms/Object/Logic/Furniture/Floor/Wired/Actions/WiredActionChaseHabbo.cs b/Vortex.Rooms/Object/Logic/Furniture/Floor/Wired/Actions/WiredActionChaseHabbo.cs index c4e0b10e..08ab50d1 100644 --- a/Vortex.Rooms/Object/Logic/Furniture/Floor/Wired/Actions/WiredActionChaseHabbo.cs +++ b/Vortex.Rooms/Object/Logic/Furniture/Floor/Wired/Actions/WiredActionChaseHabbo.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Orleans; +using Vortex.Logging.Extensions; using Vortex.Primitives.Action; using Vortex.Primitives.Furniture.Providers; using Vortex.Primitives.Rooms.Enums; @@ -71,18 +72,19 @@ public override async Task ExecuteAsync(IWiredExecutionContext ctx, Cancel { didCollide = true; - _ = _ctx.PublishRoomEventAsync( - new RoomItemCollisionEvent() - { - ObjectId = floorItem.ObjectId, - CausedBy = ActionContext.CreateForPlayer( - player.PlayerId, - _roomGrain.RoomId - ), - RoomId = _roomGrain.RoomId, - }, - ct - ); + _ctx.PublishRoomEventAsync( + new RoomItemCollisionEvent() + { + ObjectId = floorItem.ObjectId, + CausedBy = ActionContext.CreateForPlayer( + player.PlayerId, + _roomGrain.RoomId + ), + RoomId = _roomGrain.RoomId, + }, + ct + ) + .LogAndForget(_roomGrain._logger, "Failed to publish room item collision event."); break; } diff --git a/Vortex.Rooms/Object/Logic/Furniture/FurnitureLogic.cs b/Vortex.Rooms/Object/Logic/Furniture/FurnitureLogic.cs index 0696719a..8b24c41c 100644 --- a/Vortex.Rooms/Object/Logic/Furniture/FurnitureLogic.cs +++ b/Vortex.Rooms/Object/Logic/Furniture/FurnitureLogic.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Vortex.Logging.Extensions; using Vortex.Primitives.Action; using Vortex.Primitives.Furniture.Enums; using Vortex.Primitives.Furniture.Providers; @@ -103,7 +104,8 @@ public virtual async Task SetStateAsync(int state, bool refresh = true) if (refresh) { - _ = _ctx.RefreshStuffDataAsync(); + _ctx.RefreshStuffDataAsync() + .LogAndForget(_roomGrain._logger, "Failed to refresh stuff data."); } await OnStateChangedAsync(CancellationToken.None); From b645426d4fb4d2210b3b75156d0b6b1b1d762508 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 01:17:07 +0000 Subject: [PATCH 03/11] refactor: decouple VortexEmulator startup, merge duplicate room-id grain call - ARCH-01: introduce IReferenceDataProvider (LoadStage + ReloadAsync). All 14 reference-data providers (furniture, catalog x2, club offers/ gifts, currencies, badge parts, marketplace settings, pet palettes/ commands/levels/vocals, navigator, room models) now implement it and are additionally registered under the new interface, resolving to the same singleton instances. VortexEmulator goes from 18 constructor dependencies to 3 (logger, IEnumerable, INetworkManager), loads providers in parallel by stage instead of one giant sequential await chain, and adding a new reference-data source no longer touches Vortex.Main at all. Catalog snapshot providers and the club gift provider stay in stage 1 since they read furniture definitions during reload, so furniture (stage 0) must finish first; confirmed no other cross-provider dependencies exist. - REV-01 (registration half): revisions are now registered by contract (IRevision) and discovered via IEnumerable from a small dedicated RevisionRegistrationService, instead of VortexEmulator taking a concrete Revision20260701 constructor parameter and calling RegisterRevision directly. Added IRevisionManager.SetDefault and an optional Vortex:Revisions:DefaultRevisionId config key so the default revision no longer depends on registration order by accident. The larger REV-01 items (RevisionBase inheritance, per-domain revision maps) are still open. - PERF-01: MessageRegistry.CreateContextAsync no longer makes its own GetActiveRoomAsync grain call. MessageSystem.PublishAsync already resolves the active room for tracing/metrics and opens an ambient IVortexContextAccessor scope carrying it before invoking the pipeline; CreateContextAsync now reads that instead of repeating the grain round trip, cutting inbound-packet grain calls from 2 to 1. All 564 tests pass; build stays clean at 0 warnings. --- Vortex.Catalog/CatalogModule.cs | 14 ++++ .../Providers/CatalogClubGiftProvider.cs | 6 +- .../Providers/CatalogClubOfferProvider.cs | 5 +- .../Providers/CatalogSnapshotProvider.cs | 7 +- Vortex.Furniture/FurnitureModule.cs | 4 + .../Providers/FurnitureDefinitionProvider.cs | 7 +- Vortex.Main/VortexEmulator.cs | 78 ++++--------------- Vortex.Marketplace/MarketplaceModule.cs | 4 + .../Providers/MarketplaceSettingsProvider.cs | 5 +- Vortex.Messages/Registry/MessageRegistry.cs | 26 ++----- Vortex.Navigator/NavigatorModule.cs | 4 + Vortex.Navigator/NavigatorProvider.cs | 5 +- .../Revisions/RevisionManager.cs | 14 ++++ Vortex.Players/PlayerModule.cs | 19 +++++ .../Providers/CurrencyTypeProvider.cs | 5 +- .../Providers/GroupBadgePartProvider.cs | 5 +- .../Providers/PetCommandProvider.cs | 5 +- Vortex.Players/Providers/PetLevelProvider.cs | 5 +- .../Providers/PetPaletteProvider.cs | 5 +- Vortex.Players/Providers/PetVocalProvider.cs | 5 +- .../Hosting/IReferenceDataProvider.cs | 23 ++++++ .../Networking/Revisions/IRevisionManager.cs | 4 + .../Configuration/RevisionConfig.cs | 13 ++++ .../Extensions/ServiceCollectionExtensions.cs | 10 ++- .../RevisionRegistrationService.cs | 43 ++++++++++ Vortex.Rooms/Providers/RoomModelProvider.cs | 5 +- Vortex.Rooms/RoomModule.cs | 4 + 27 files changed, 236 insertions(+), 94 deletions(-) create mode 100644 Vortex.Primitives/Hosting/IReferenceDataProvider.cs create mode 100644 Vortex.Revisions/Configuration/RevisionConfig.cs create mode 100644 Vortex.Revisions/RevisionRegistrationService.cs diff --git a/Vortex.Catalog/CatalogModule.cs b/Vortex.Catalog/CatalogModule.cs index 756265bc..738968b2 100644 --- a/Vortex.Catalog/CatalogModule.cs +++ b/Vortex.Catalog/CatalogModule.cs @@ -9,6 +9,7 @@ using Vortex.Primitives.Catalog.Providers; using Vortex.Primitives.Catalog.Tags; using Vortex.Primitives.Furniture.Providers; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Plugins; namespace Vortex.Catalog; @@ -24,7 +25,13 @@ public void ConfigureServices(IServiceCollection services, HostApplicationBuilde services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton>( sp => new CatalogSnapshotProvider( sp.GetRequiredService>(), @@ -33,6 +40,9 @@ public void ConfigureServices(IServiceCollection services, HostApplicationBuilde CatalogType.Normal ) ); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService>() + ); services.AddSingleton>( sp => new CatalogSnapshotProvider( sp.GetRequiredService>(), @@ -41,5 +51,9 @@ public void ConfigureServices(IServiceCollection services, HostApplicationBuilde CatalogType.BuildersClub ) ); + services.AddSingleton(sp => + (IReferenceDataProvider) + sp.GetRequiredService>() + ); } } diff --git a/Vortex.Catalog/Providers/CatalogClubGiftProvider.cs b/Vortex.Catalog/Providers/CatalogClubGiftProvider.cs index adf5df85..555dc6b1 100644 --- a/Vortex.Catalog/Providers/CatalogClubGiftProvider.cs +++ b/Vortex.Catalog/Providers/CatalogClubGiftProvider.cs @@ -11,6 +11,7 @@ using Vortex.Primitives.Catalog.Snapshots; using Vortex.Primitives.Furniture.Enums; using Vortex.Primitives.Furniture.Providers; +using Vortex.Primitives.Hosting; namespace Vortex.Catalog.Providers; @@ -18,12 +19,15 @@ public sealed class CatalogClubGiftProvider( IDbContextFactory dbCtxFactory, ILogger logger, IFurnitureDefinitionProvider furnitureProvider -) : ICatalogClubGiftProvider +) : ICatalogClubGiftProvider, IReferenceDataProvider { private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly ILogger _logger = logger; private readonly IFurnitureDefinitionProvider _furnitureProvider = furnitureProvider; + // Stage 1: reads furniture definitions, must reload after IFurnitureDefinitionProvider. + public int LoadStage => 1; + private IReadOnlyList _offers = []; public IReadOnlyList GetAll() => _offers; diff --git a/Vortex.Catalog/Providers/CatalogClubOfferProvider.cs b/Vortex.Catalog/Providers/CatalogClubOfferProvider.cs index 2d937566..0f010f74 100644 --- a/Vortex.Catalog/Providers/CatalogClubOfferProvider.cs +++ b/Vortex.Catalog/Providers/CatalogClubOfferProvider.cs @@ -8,17 +8,20 @@ using Vortex.Database.Entities.Catalog; using Vortex.Primitives.Catalog; using Vortex.Primitives.Catalog.Providers; +using Vortex.Primitives.Hosting; namespace Vortex.Catalog.Providers; public sealed class CatalogClubOfferProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : ICatalogClubOfferProvider +) : ICatalogClubOfferProvider, IReferenceDataProvider { private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly ILogger _logger = logger; + public int LoadStage => 0; + private IReadOnlyList _offers = []; public IReadOnlyList GetAll() diff --git a/Vortex.Catalog/Providers/CatalogSnapshotProvider.cs b/Vortex.Catalog/Providers/CatalogSnapshotProvider.cs index 4ad1de11..bb9c827a 100644 --- a/Vortex.Catalog/Providers/CatalogSnapshotProvider.cs +++ b/Vortex.Catalog/Providers/CatalogSnapshotProvider.cs @@ -12,6 +12,7 @@ using Vortex.Primitives.Catalog.Providers; using Vortex.Primitives.Catalog.Snapshots; using Vortex.Primitives.Furniture.Providers; +using Vortex.Primitives.Hosting; namespace Vortex.Catalog.Providers; @@ -20,7 +21,7 @@ public sealed class CatalogSnapshotProvider( ILogger> logger, IFurnitureDefinitionProvider furnitureProvider, CatalogType catalogType -) : ICatalogSnapshotProvider +) : ICatalogSnapshotProvider, IReferenceDataProvider where TTag : ICatalogTag { private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; @@ -28,6 +29,10 @@ CatalogType catalogType private readonly IFurnitureDefinitionProvider _furnitureProvider = furnitureProvider; private CatalogSnapshot _current = CatalogSnapshot.Empty; + // Stage 1: reads furniture definitions while building snapshots, so it must reload after + // IFurnitureDefinitionProvider (stage 0). + public int LoadStage => 1; + public CatalogType CatalogType => catalogType; public CatalogSnapshot Current => _current; diff --git a/Vortex.Furniture/FurnitureModule.cs b/Vortex.Furniture/FurnitureModule.cs index d26d8eb7..b57ac8fa 100644 --- a/Vortex.Furniture/FurnitureModule.cs +++ b/Vortex.Furniture/FurnitureModule.cs @@ -4,6 +4,7 @@ using Vortex.Furniture.Providers; using Vortex.Primitives.Furniture; using Vortex.Primitives.Furniture.Providers; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Plugins; namespace Vortex.Furniture; @@ -19,6 +20,9 @@ public void ConfigureServices(IServiceCollection services, HostApplicationBuilde ); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); services.AddSingleton(); } diff --git a/Vortex.Furniture/Providers/FurnitureDefinitionProvider.cs b/Vortex.Furniture/Providers/FurnitureDefinitionProvider.cs index 8ce57fbd..8a8ffc10 100644 --- a/Vortex.Furniture/Providers/FurnitureDefinitionProvider.cs +++ b/Vortex.Furniture/Providers/FurnitureDefinitionProvider.cs @@ -12,6 +12,7 @@ using Vortex.Furniture.Configuration; using Vortex.Primitives.Furniture.Providers; using Vortex.Primitives.Furniture.Snapshots; +using Vortex.Primitives.Hosting; namespace Vortex.Furniture.Providers; @@ -19,12 +20,16 @@ public sealed class FurnitureDefinitionProvider( IOptions config, IDbContextFactory dbCtxFactory, ILogger logger -) : IFurnitureDefinitionProvider +) : IFurnitureDefinitionProvider, IReferenceDataProvider { private readonly FurnitureConfig _config = config.Value; private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly ILogger _logger = logger; + // Stage 0: CatalogSnapshotProvider and CatalogClubGiftProvider read furniture definitions + // during their own reload (see stage 1), so this must finish first. + public int LoadStage => 0; + private ImmutableDictionary _definitionsById = ImmutableDictionary.Empty; diff --git a/Vortex.Main/VortexEmulator.cs b/Vortex.Main/VortexEmulator.cs index 18ea72a4..b42e8e89 100644 --- a/Vortex.Main/VortexEmulator.cs +++ b/Vortex.Main/VortexEmulator.cs @@ -1,85 +1,39 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Vortex.Primitives.Catalog.Providers; -using Vortex.Primitives.Catalog.Tags; -using Vortex.Primitives.Furniture.Providers; -using Vortex.Primitives.Groups.Providers; -using Vortex.Primitives.Marketplace.Providers; -using Vortex.Primitives.Navigator; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Networking; -using Vortex.Primitives.Networking.Revisions; -using Vortex.Primitives.Pets.Providers; -using Vortex.Primitives.Players.Providers; -using Vortex.Primitives.Rooms.Providers; -using Vortex.Revisions.Revision20260701; namespace Vortex.Main; public class VortexEmulator( ILogger logger, - IFurnitureDefinitionProvider furnitureProvider, - ICatalogSnapshotProvider catalogProvider, - ICatalogSnapshotProvider buildersClubCatalogProvider, - ICatalogClubOfferProvider clubOfferProvider, - ICatalogClubGiftProvider clubGiftProvider, - ICurrencyTypeProvider currencyTypeProvider, - IGroupBadgePartProvider guildBadgePartProvider, - IMarketplaceSettingsProvider marketplaceSettingsProvider, - IPetPaletteProvider petPaletteProvider, - IPetCommandProvider petCommandProvider, - IPetLevelProvider petLevelProvider, - IPetVocalProvider petVocalProvider, - INavigatorProvider topLevelContextProvider, - IRoomModelProvider roomModelProvider, - INetworkManager networkManager, - IRevisionManager revisionManager, - Revision20260701 defaultRevision + IEnumerable referenceDataProviders, + INetworkManager networkManager ) : IHostedService { - private readonly ICatalogSnapshotProvider _buildersClubCatalogProvider = - buildersClubCatalogProvider; - - private readonly ICatalogSnapshotProvider _catalogProvider = catalogProvider; - private readonly ICatalogClubGiftProvider _clubGiftProvider = clubGiftProvider; - private readonly ICatalogClubOfferProvider _clubOfferProvider = clubOfferProvider; - private readonly ICurrencyTypeProvider _currencyTypeProvider = currencyTypeProvider; - private readonly Revision20260701 _defaultRevision = defaultRevision; - private readonly IFurnitureDefinitionProvider _furnitureProvider = furnitureProvider; - private readonly IGroupBadgePartProvider _guildBadgePartProvider = guildBadgePartProvider; private readonly ILogger _logger = logger; - private readonly IMarketplaceSettingsProvider _marketplaceSettingsProvider = - marketplaceSettingsProvider; + private readonly IEnumerable _referenceDataProviders = + referenceDataProviders; private readonly INetworkManager _networkManager = networkManager; - private readonly IPetCommandProvider _petCommandProvider = petCommandProvider; - private readonly IPetLevelProvider _petLevelProvider = petLevelProvider; - private readonly IPetVocalProvider _petVocalProvider = petVocalProvider; - private readonly IPetPaletteProvider _petPaletteProvider = petPaletteProvider; - private readonly IRevisionManager _revisionManager = revisionManager; - private readonly IRoomModelProvider _roomModelProvider = roomModelProvider; - private readonly INavigatorProvider _topLevelContextProvider = topLevelContextProvider; public async Task StartAsync(CancellationToken ct) { try { - _revisionManager.RegisterRevision(_defaultRevision); - await _furnitureProvider.ReloadAsync(ct).ConfigureAwait(false); - await _catalogProvider.ReloadAsync(ct).ConfigureAwait(false); - await _buildersClubCatalogProvider.ReloadAsync(ct).ConfigureAwait(false); - await _clubOfferProvider.ReloadAsync(ct).ConfigureAwait(false); - await _clubGiftProvider.ReloadAsync(ct).ConfigureAwait(false); - await _currencyTypeProvider.ReloadAsync(ct).ConfigureAwait(false); - await _marketplaceSettingsProvider.ReloadAsync(ct).ConfigureAwait(false); - await _guildBadgePartProvider.ReloadAsync(ct).ConfigureAwait(false); - await _petPaletteProvider.ReloadAsync(ct).ConfigureAwait(false); - await _petCommandProvider.ReloadAsync(ct).ConfigureAwait(false); - await _petLevelProvider.ReloadAsync(ct).ConfigureAwait(false); - await _petVocalProvider.ReloadAsync(ct).ConfigureAwait(false); - await _topLevelContextProvider.ReloadAsync(ct).ConfigureAwait(false); - await _roomModelProvider.ReloadAsync(ct).ConfigureAwait(false); + IEnumerable> stages = _referenceDataProviders + .GroupBy(p => p.LoadStage) + .OrderBy(stage => stage.Key); + + foreach (IGrouping stage in stages) + { + await Task.WhenAll(stage.Select(p => p.ReloadAsync(ct))).ConfigureAwait(false); + } + await _networkManager.StartAsync(ct).ConfigureAwait(false); } catch (OperationCanceledException) diff --git a/Vortex.Marketplace/MarketplaceModule.cs b/Vortex.Marketplace/MarketplaceModule.cs index 9c9f1c36..42d2df2e 100644 --- a/Vortex.Marketplace/MarketplaceModule.cs +++ b/Vortex.Marketplace/MarketplaceModule.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Vortex.Marketplace.Providers; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Marketplace.Providers; using Vortex.Primitives.Plugins; @@ -13,5 +14,8 @@ public sealed class MarketplaceModule : IHostPluginModule public void ConfigureServices(IServiceCollection services, HostApplicationBuilder builder) { services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); } } diff --git a/Vortex.Marketplace/Providers/MarketplaceSettingsProvider.cs b/Vortex.Marketplace/Providers/MarketplaceSettingsProvider.cs index 6ab3f82c..19f886da 100644 --- a/Vortex.Marketplace/Providers/MarketplaceSettingsProvider.cs +++ b/Vortex.Marketplace/Providers/MarketplaceSettingsProvider.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Vortex.Database.Context; using Vortex.Database.Entities.Marketplace; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Marketplace.Providers; using Vortex.Primitives.Marketplace.Snapshots; @@ -13,7 +14,7 @@ namespace Vortex.Marketplace.Providers; public sealed class MarketplaceSettingsProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : IMarketplaceSettingsProvider +) : IMarketplaceSettingsProvider, IReferenceDataProvider { private static readonly MarketplaceSettingsSnapshot Defaults = new() { @@ -21,6 +22,8 @@ ILogger logger OfferDurationSeconds = 259200, }; + public int LoadStage => 0; + private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly ILogger _logger = logger; diff --git a/Vortex.Messages/Registry/MessageRegistry.cs b/Vortex.Messages/Registry/MessageRegistry.cs index 73b77cb1..a592a3e3 100644 --- a/Vortex.Messages/Registry/MessageRegistry.cs +++ b/Vortex.Messages/Registry/MessageRegistry.cs @@ -1,16 +1,14 @@ using System; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Orleans; using Vortex.Logging; using Vortex.Pipeline; using Vortex.Primitives; using Vortex.Primitives.Networking; using Vortex.Primitives.Observability; using Vortex.Primitives.Orleans; -using Vortex.Primitives.Orleans.Snapshots.Room; using Vortex.Primitives.Players; -using Vortex.Primitives.Players.Grains; namespace Vortex.Messages.Registry; @@ -38,32 +36,24 @@ ILogger logger { return new EnvelopeHostOptions { - CreateContextAsync = async (env, data) => + CreateContextAsync = (env, data) => { if (data is null) { throw new VortexException(VortexErrorCodeEnum.InvalidSession); } - IGrainFactory grainFactory = serviceProvider.GetRequiredService(); ISessionGateway sessionGateway = serviceProvider.GetRequiredService(); PlayerId playerId = sessionGateway.GetPlayerId(data.SessionKey); - int roomId = -1; - if (playerId > 0) - { - IPlayerPresenceGrain playerPresence = grainFactory.GetPlayerPresenceGrain( - playerId - ); - RoomPointerSnapshot activeRoom = await playerPresence - .GetActiveRoomAsync() - .ConfigureAwait(false); - - roomId = activeRoom.RoomId; - } + // MessageSystem.PublishAsync already resolved the active room for this exact packet + // (for tracing/metrics) and opened the ambient scope carrying it before invoking this + // pipeline, so this reuses that value instead of a second GetActiveRoomAsync grain + // round trip per packet (PERF-01). + int roomId = playerId > 0 ? (contextAccessor.Current?.RoomId ?? -1) : -1; - return new(data, playerId, roomId); + return Task.FromResult(new MessageContext(data, playerId, roomId)); }, EnableInheritanceDispatch = true, HandlerMode = HandlerExecutionMode.Parallel, diff --git a/Vortex.Navigator/NavigatorModule.cs b/Vortex.Navigator/NavigatorModule.cs index 70982c93..ec75a258 100644 --- a/Vortex.Navigator/NavigatorModule.cs +++ b/Vortex.Navigator/NavigatorModule.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Navigator; using Vortex.Primitives.Plugins; @@ -13,5 +14,8 @@ public void ConfigureServices(IServiceCollection services, HostApplicationBuilde { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); } } diff --git a/Vortex.Navigator/NavigatorProvider.cs b/Vortex.Navigator/NavigatorProvider.cs index 089e01b7..b085a56c 100644 --- a/Vortex.Navigator/NavigatorProvider.cs +++ b/Vortex.Navigator/NavigatorProvider.cs @@ -11,6 +11,7 @@ using Vortex.Database.Entities.Room; using Vortex.Primitives.Navigator; using Vortex.Primitives.Navigator.Enums; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Orleans.Snapshots.Navigator; using Vortex.Primitives.Orleans.Snapshots.Room; using Vortex.Primitives.Players; @@ -20,8 +21,10 @@ namespace Vortex.Navigator; public sealed class NavigatorProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : INavigatorProvider +) : INavigatorProvider, IReferenceDataProvider { + public int LoadStage => 0; + private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly ILogger _logger = logger; diff --git a/Vortex.Networking/Revisions/RevisionManager.cs b/Vortex.Networking/Revisions/RevisionManager.cs index 060dd574..345a71fe 100644 --- a/Vortex.Networking/Revisions/RevisionManager.cs +++ b/Vortex.Networking/Revisions/RevisionManager.cs @@ -31,4 +31,18 @@ public void RegisterRevision(IRevision revision) DefaultRevisionId = revision.Revision; } } + + public void SetDefault(string revisionId) + { + ArgumentException.ThrowIfNullOrEmpty(revisionId); + + if (!Revisions.ContainsKey(revisionId)) + { + throw new InvalidOperationException( + $"Cannot set '{revisionId}' as the default revision: no revision with that id is registered." + ); + } + + DefaultRevisionId = revisionId; + } } diff --git a/Vortex.Players/PlayerModule.cs b/Vortex.Players/PlayerModule.cs index 500d01a3..03e30459 100644 --- a/Vortex.Players/PlayerModule.cs +++ b/Vortex.Players/PlayerModule.cs @@ -5,6 +5,7 @@ using Vortex.Players.Providers; using Vortex.Players.Quests; using Vortex.Primitives.Groups.Providers; +using Vortex.Primitives.Hosting; using Vortex.Primitives.MysteryBox; using Vortex.Primitives.Pets.Providers; using Vortex.Primitives.Players; @@ -29,11 +30,29 @@ public void ConfigureServices(IServiceCollection services, HostApplicationBuilde ); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Vortex.Players/Providers/CurrencyTypeProvider.cs b/Vortex.Players/Providers/CurrencyTypeProvider.cs index d8f3765a..010f77b6 100644 --- a/Vortex.Players/Providers/CurrencyTypeProvider.cs +++ b/Vortex.Players/Providers/CurrencyTypeProvider.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Vortex.Database.Context; using Vortex.Database.Entities.Catalog; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Players.Providers; using Vortex.Primitives.Players.Snapshots; using Vortex.Primitives.Players.Wallet; @@ -15,8 +16,10 @@ namespace Vortex.Players.Providers; public sealed class CurrencyTypeProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : ICurrencyTypeProvider +) : ICurrencyTypeProvider, IReferenceDataProvider { + public int LoadStage => 0; + private sealed record State( ImmutableDictionary ById, ImmutableDictionary IdByKind diff --git a/Vortex.Players/Providers/GroupBadgePartProvider.cs b/Vortex.Players/Providers/GroupBadgePartProvider.cs index 5cb156a9..ced08b06 100644 --- a/Vortex.Players/Providers/GroupBadgePartProvider.cs +++ b/Vortex.Players/Providers/GroupBadgePartProvider.cs @@ -9,14 +9,17 @@ using Vortex.Database.Entities.Groups; using Vortex.Primitives.Groups.Providers; using Vortex.Primitives.Groups.Snapshots; +using Vortex.Primitives.Hosting; namespace Vortex.Players.Providers; public sealed class GroupBadgePartProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : IGroupBadgePartProvider +) : IGroupBadgePartProvider, IReferenceDataProvider { + public int LoadStage => 0; + private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly ILogger _logger = logger; diff --git a/Vortex.Players/Providers/PetCommandProvider.cs b/Vortex.Players/Providers/PetCommandProvider.cs index 940cf25f..e7608c11 100644 --- a/Vortex.Players/Providers/PetCommandProvider.cs +++ b/Vortex.Players/Providers/PetCommandProvider.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Vortex.Database.Context; using Vortex.Database.Entities.Pets; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Pets.Providers; using Vortex.Primitives.Pets.Snapshots; @@ -15,8 +16,10 @@ namespace Vortex.Players.Providers; public sealed class PetCommandProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : IPetCommandProvider +) : IPetCommandProvider, IReferenceDataProvider { + public int LoadStage => 0; + private ImmutableDictionary> _byType = ImmutableDictionary< int, ImmutableArray diff --git a/Vortex.Players/Providers/PetLevelProvider.cs b/Vortex.Players/Providers/PetLevelProvider.cs index 61d5a904..80e1c8c7 100644 --- a/Vortex.Players/Providers/PetLevelProvider.cs +++ b/Vortex.Players/Providers/PetLevelProvider.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Vortex.Database.Context; using Vortex.Database.Entities.Pets; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Pets.Providers; using Vortex.Primitives.Pets.Snapshots; @@ -15,8 +16,10 @@ namespace Vortex.Players.Providers; public sealed class PetLevelProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : IPetLevelProvider +) : IPetLevelProvider, IReferenceDataProvider { + public int LoadStage => 0; + private ImmutableDictionary> _byType = ImmutableDictionary< int, ImmutableArray diff --git a/Vortex.Players/Providers/PetPaletteProvider.cs b/Vortex.Players/Providers/PetPaletteProvider.cs index 730d373c..cfbe7a77 100644 --- a/Vortex.Players/Providers/PetPaletteProvider.cs +++ b/Vortex.Players/Providers/PetPaletteProvider.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Vortex.Database.Context; using Vortex.Database.Entities.Pets; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Pets.Providers; using Vortex.Primitives.Pets.Snapshots; @@ -15,8 +16,10 @@ namespace Vortex.Players.Providers; public sealed class PetPaletteProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : IPetPaletteProvider +) : IPetPaletteProvider, IReferenceDataProvider { + public int LoadStage => 0; + private ImmutableDictionary> _byType = ImmutableDictionary< int, ImmutableArray diff --git a/Vortex.Players/Providers/PetVocalProvider.cs b/Vortex.Players/Providers/PetVocalProvider.cs index cf581750..a1bc3bb3 100644 --- a/Vortex.Players/Providers/PetVocalProvider.cs +++ b/Vortex.Players/Providers/PetVocalProvider.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Vortex.Database.Context; using Vortex.Database.Entities.Pets; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Pets.Providers; namespace Vortex.Players.Providers; @@ -19,8 +20,10 @@ namespace Vortex.Players.Providers; public sealed class PetVocalProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : IPetVocalProvider +) : IPetVocalProvider, IReferenceDataProvider { + public int LoadStage => 0; + private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly ILogger _logger = logger; diff --git a/Vortex.Primitives/Hosting/IReferenceDataProvider.cs b/Vortex.Primitives/Hosting/IReferenceDataProvider.cs new file mode 100644 index 00000000..870d41c9 --- /dev/null +++ b/Vortex.Primitives/Hosting/IReferenceDataProvider.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Vortex.Primitives.Hosting; + +/// +/// A cache of reference data (furniture definitions, catalog snapshots, pet palettes, ...) loaded +/// once at startup and reloadable on demand. Implemented by a provider's existing concrete class +/// alongside its specific interface so VortexEmulator can discover and load every provider +/// through instead of a fixed constructor +/// parameter list. +/// +public interface IReferenceDataProvider +{ + /// + /// Providers in the same stage are reloaded concurrently; stages run in ascending order. Two + /// providers only need different stages when one's reload genuinely depends on data the other + /// just loaded — independent reference caches should share a stage. + /// + int LoadStage { get; } + + Task ReloadAsync(CancellationToken ct); +} diff --git a/Vortex.Primitives/Networking/Revisions/IRevisionManager.cs b/Vortex.Primitives/Networking/Revisions/IRevisionManager.cs index 579198b9..a50bb94c 100644 --- a/Vortex.Primitives/Networking/Revisions/IRevisionManager.cs +++ b/Vortex.Primitives/Networking/Revisions/IRevisionManager.cs @@ -10,4 +10,8 @@ public interface IRevisionManager public IRevision? GetRevision(string revisionName); public void RegisterRevision(IRevision revision); + + /// Explicitly pins to an already-registered revision, + /// instead of leaving it to whichever revision happened to register first. + public void SetDefault(string revisionId); } diff --git a/Vortex.Revisions/Configuration/RevisionConfig.cs b/Vortex.Revisions/Configuration/RevisionConfig.cs new file mode 100644 index 00000000..b6f87caa --- /dev/null +++ b/Vortex.Revisions/Configuration/RevisionConfig.cs @@ -0,0 +1,13 @@ +namespace Vortex.Revisions.Configuration; + +/// +/// Which registered new sessions are +/// pinned to. Left unset, the manager keeps its "first one registered wins" fallback - fine while +/// only one revision exists, but explicit once a second is added. +/// +public sealed class RevisionConfig +{ + public const string SECTION_NAME = "Vortex:Revisions"; + + public string? DefaultRevisionId { get; init; } +} diff --git a/Vortex.Revisions/Extensions/ServiceCollectionExtensions.cs b/Vortex.Revisions/Extensions/ServiceCollectionExtensions.cs index a4792017..af676eac 100644 --- a/Vortex.Revisions/Extensions/ServiceCollectionExtensions.cs +++ b/Vortex.Revisions/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Vortex.Primitives.Networking.Revisions; using Vortex.Revisions.Configuration; using RevisionType = Vortex.Revisions.Revision20260701.Revision20260701; @@ -16,7 +17,14 @@ HostApplicationBuilder builder builder.Configuration.GetSection(ProtocolLimitsConfig.SECTION_NAME) ); - services.AddSingleton(); + services.Configure( + builder.Configuration.GetSection(RevisionConfig.SECTION_NAME) + ); + + // Registered by contract (not the concrete type) so new revisions are picked up by adding + // an IRevision implementation, without VortexEmulator ever knowing a concrete type exists. + services.AddSingleton(); + services.AddHostedService(); return services; } diff --git a/Vortex.Revisions/RevisionRegistrationService.cs b/Vortex.Revisions/RevisionRegistrationService.cs new file mode 100644 index 00000000..08e2c157 --- /dev/null +++ b/Vortex.Revisions/RevisionRegistrationService.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Configuration; + +namespace Vortex.Revisions; + +/// +/// Registers every discovered with the at +/// startup, by contract rather than by a fixed constructor parameter naming a concrete revision +/// type. Registered before VortexEmulator so sessions never see the network listeners open +/// before a default revision is set. +/// +public sealed class RevisionRegistrationService( + IRevisionManager revisionManager, + IEnumerable revisions, + IOptions config +) : IHostedService +{ + private readonly IRevisionManager _revisionManager = revisionManager; + private readonly IEnumerable _revisions = revisions; + private readonly RevisionConfig _config = config.Value; + + public Task StartAsync(CancellationToken ct) + { + foreach (IRevision revision in _revisions) + { + _revisionManager.RegisterRevision(revision); + } + + if (!string.IsNullOrEmpty(_config.DefaultRevisionId)) + { + _revisionManager.SetDefault(_config.DefaultRevisionId); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken ct) => Task.CompletedTask; +} diff --git a/Vortex.Rooms/Providers/RoomModelProvider.cs b/Vortex.Rooms/Providers/RoomModelProvider.cs index a1471906..68f35753 100644 --- a/Vortex.Rooms/Providers/RoomModelProvider.cs +++ b/Vortex.Rooms/Providers/RoomModelProvider.cs @@ -9,6 +9,7 @@ using Vortex.Database.Context; using Vortex.Database.Entities.Room; using Vortex.Primitives.Rooms.Enums; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Rooms.Object; using Vortex.Primitives.Rooms.Providers; using Vortex.Primitives.Rooms.Snapshots.Mapping; @@ -18,8 +19,10 @@ namespace Vortex.Rooms.Providers; public sealed class RoomModelProvider( IDbContextFactory dbCtxFactory, ILogger logger -) : IRoomModelProvider +) : IRoomModelProvider, IReferenceDataProvider { + public int LoadStage => 0; + private readonly IDbContextFactory _dbCtxFactory = dbCtxFactory; private readonly ILogger _logger = logger; diff --git a/Vortex.Rooms/RoomModule.cs b/Vortex.Rooms/RoomModule.cs index 97de60bb..27141bc3 100644 --- a/Vortex.Rooms/RoomModule.cs +++ b/Vortex.Rooms/RoomModule.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Vortex.Primitives.Hosting; using Vortex.Primitives.Moderation; using Vortex.Primitives.Plugins; using Vortex.Primitives.Rooms; @@ -24,6 +25,9 @@ public void ConfigureServices(IServiceCollection services, HostApplicationBuilde services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(sp => + (IReferenceDataProvider)sp.GetRequiredService() + ); services.AddSingleton(); services.AddSingleton(); From 8b6e80de3b2ff1b3a062f15ad79439ff4d8b6956 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 01:26:07 +0000 Subject: [PATCH 04/11] fix: purchase-grant ordering and opt-in single-use SSO tickets - SEC-06: in CatalogPurchaseGrain (direct purchase and gift purchase), reorder the compensated purchase callback so the reversible step (tracking credit spend) runs before the irreversible one (granting the inventory item), and move the CatalogPurchasedEvent/ CatalogGiftPurchasedEvent publishes outside the compensated scope entirely so a failing event subscriber can no longer trigger a refund of an already-completed sale. Checked every other ExecutePurchaseAsync call site (Marketplace, LtdRaffle, RentableSpace, PlayerTargetedOffer, room-ad purchase, club months); none of them have the same shape, so no further changes needed. - SEC-02: add Vortex:Authentication:TicketSingleUse (default false, preserving current behavior) and an optional TicketAbsoluteLifetimeSeconds cap. With TicketSingleUse on, a non-locked SSO ticket is deleted on first successful use instead of having its expiry pushed forward, closing the "observed once, reusable forever" replay window. Locked (persistent/CMS) tickets are intentionally left untouched either way. All 564 existing tests pass; build stays clean at 0 warnings. --- .../AuthenticationService.cs | 37 +++++++++++- .../Configuration/AuthenticationConfig.cs | 16 +++++ .../AuthenticationConfigValidator.cs | 9 +++ .../Grains/CatalogPurchaseGrain.Gift.cs | 58 ++++++++----------- Vortex.Catalog/Grains/CatalogPurchaseGrain.cs | 38 +++++++----- 5 files changed, 104 insertions(+), 54 deletions(-) diff --git a/Vortex.Authentication/AuthenticationService.cs b/Vortex.Authentication/AuthenticationService.cs index 1e2677ad..fbafcf2b 100644 --- a/Vortex.Authentication/AuthenticationService.cs +++ b/Vortex.Authentication/AuthenticationService.cs @@ -23,6 +23,8 @@ IOptions options private readonly IEventPublisher _events = events; private readonly string _ipHashSecret = options.Value.IpHashSecret; private readonly int _ticketTtlSeconds = options.Value.TicketTtlSeconds; + private readonly bool _ticketSingleUse = options.Value.TicketSingleUse; + private readonly int? _ticketAbsoluteLifetimeSeconds = options.Value.TicketAbsoluteLifetimeSeconds; public async Task GetPlayerIdFromTicketAsync( string ticket, @@ -78,11 +80,40 @@ await _events return 0; } + // `IsLocked` tickets are persistent/CMS-managed and intentionally never touched here + // (no slide, no single-use deletion, no expiry) — they are a separate mechanism from the + // one-ticket-per-login-attempt flow this method otherwise implements. if (!entity.IsLocked) { - // Refresh the expiry so the client can reconnect after a disconnect without a - // new ticket, while keeping the replay window bounded. - entity.ExpiresAt = _ticketTtlSeconds > 0 ? now.AddSeconds(_ticketTtlSeconds) : null; + if (_ticketSingleUse) + { + // Consumed on first successful use: an observed ticket (proxy logs, browser + // history, unencrypted transport, Referer) can no longer be replayed at all, + // rather than being able to extend its own validity indefinitely. + dbCtx.SecurityTickets.Remove(entity); + } + else + { + // Refresh the expiry so the client can reconnect after a disconnect without a + // new ticket, while keeping the replay window bounded per use. Left default + // (TicketSingleUse = false) for compatibility with CMS integrations that reuse + // one ticket across reconnects. + DateTime slidExpiry = _ticketTtlSeconds > 0 + ? now.AddSeconds(_ticketTtlSeconds) + : DateTime.MaxValue; + + if (_ticketAbsoluteLifetimeSeconds is int absoluteSeconds) + { + DateTime absoluteCap = entity.CreatedAt.AddSeconds(absoluteSeconds); + + if (slidExpiry > absoluteCap) + { + slidExpiry = absoluteCap; + } + } + + entity.ExpiresAt = slidExpiry == DateTime.MaxValue ? null : slidExpiry; + } await dbCtx.SaveChangesAsync(ct).ConfigureAwait(false); } diff --git a/Vortex.Authentication/Configuration/AuthenticationConfig.cs b/Vortex.Authentication/Configuration/AuthenticationConfig.cs index eecd4fe8..b130547d 100644 --- a/Vortex.Authentication/Configuration/AuthenticationConfig.cs +++ b/Vortex.Authentication/Configuration/AuthenticationConfig.cs @@ -24,4 +24,20 @@ public sealed class AuthenticationConfig /// Default: 30 seconds — tight enough to prevent replay, long enough for slow connections. /// public int TicketTtlSeconds { get; init; } = 30; + + /// + /// When true, a non-locked ticket is deleted on its first successful use instead of having its + /// expiry pushed forward. Default false so behavior is unchanged out of the box: some CMS + /// integrations rely on reusing the same ticket across reconnects, and turning this on would + /// break them until they're updated to mint a fresh ticket per connection attempt. + /// + public bool TicketSingleUse { get; init; } + + /// + /// Optional absolute cap (seconds since a ticket's CreatedAt) on how far the sliding + /// expiry refresh may push a ticket's validity. Unset (default) preserves today's behavior, + /// where each successful use extends the ticket indefinitely into the future. Ignored when + /// is true. + /// + public int? TicketAbsoluteLifetimeSeconds { get; init; } } diff --git a/Vortex.Authentication/Configuration/AuthenticationConfigValidator.cs b/Vortex.Authentication/Configuration/AuthenticationConfigValidator.cs index 202c2c19..3bb6d677 100644 --- a/Vortex.Authentication/Configuration/AuthenticationConfigValidator.cs +++ b/Vortex.Authentication/Configuration/AuthenticationConfigValidator.cs @@ -56,6 +56,15 @@ public ValidateOptionsResult Validate(string? name, AuthenticationConfig options ); } + if (options.TicketAbsoluteLifetimeSeconds is < 1) + { + failures.Add( + $"'{AuthenticationConfig.SECTION_NAME}:{nameof(AuthenticationConfig.TicketAbsoluteLifetimeSeconds)}' " + + $"must be a positive number of seconds when set (got {options.TicketAbsoluteLifetimeSeconds}). " + + "Leave it unset to disable the cap." + ); + } + return failures.Count > 0 ? ValidateOptionsResult.Fail(failures) : ValidateOptionsResult.Success; diff --git a/Vortex.Catalog/Grains/CatalogPurchaseGrain.Gift.cs b/Vortex.Catalog/Grains/CatalogPurchaseGrain.Gift.cs index 7832f592..e399762c 100644 --- a/Vortex.Catalog/Grains/CatalogPurchaseGrain.Gift.cs +++ b/Vortex.Catalog/Grains/CatalogPurchaseGrain.Gift.cs @@ -63,15 +63,10 @@ CancellationToken ct debitRequests, async innerCt => { - // The single line that differs from a normal purchase. Everything around it -- - // the debit, the automatic refund when this delegate throws, the spend tracking, - // the audit trail -- stays identical on purpose: each one of those was a hole - // while this path hand-rolled its own debit. - await _grainFactory - .GetInventoryGrain(receiverId.Value) - .GrantCatalogOfferAsync(offer, extraParam, 1, innerCt) - .ConfigureAwait(true); - + // Ordered least-to-most reversible, and the notification events moved outside + // this compensated scope entirely (see SEC-06): a failure tracking a stat is + // harmless to compensate, but a failure *after* the inventory grant would leave + // the recipient's gift delivered for free once the wallet refunds the buyer. if (creditCost > 0) { await _grainFactory @@ -80,32 +75,9 @@ await _grainFactory .ConfigureAwait(true); } - await _events - .PublishAsync( - new CatalogPurchasedEvent( - buyerId, - catalogType.ToString(), - offerId, - 1, - creditCost - ), - innerCt - ) - .ConfigureAwait(true); - - // Emitted on top of the purchase event rather than instead of it: the buyer's - // spend belongs in the same series as every other purchase, and a gift also has - // to answer "to whom". - await _events - .PublishAsync( - new CatalogGiftPurchasedEvent( - buyerId, - receiverId.Value, - offerId, - creditCost - ), - innerCt - ) + await _grainFactory + .GetInventoryGrain(receiverId.Value) + .GrantCatalogOfferAsync(offer, extraParam, 1, innerCt) .ConfigureAwait(true); return offer; @@ -120,6 +92,22 @@ await _events throw CreateInsufficientBalanceException(result.Failure); } + await _events + .PublishAsync( + new CatalogPurchasedEvent(buyerId, catalogType.ToString(), offerId, 1, creditCost), + ct + ) + .ConfigureAwait(true); + + // Emitted on top of the purchase event rather than instead of it: the buyer's spend belongs + // in the same series as every other purchase, and a gift also has to answer "to whom". + await _events + .PublishAsync( + new CatalogGiftPurchasedEvent(buyerId, receiverId.Value, offerId, creditCost), + ct + ) + .ConfigureAwait(true); + return result.Reward!; } } diff --git a/Vortex.Catalog/Grains/CatalogPurchaseGrain.cs b/Vortex.Catalog/Grains/CatalogPurchaseGrain.cs index 5602b48e..0b4add6f 100644 --- a/Vortex.Catalog/Grains/CatalogPurchaseGrain.cs +++ b/Vortex.Catalog/Grains/CatalogPurchaseGrain.cs @@ -74,11 +74,9 @@ out List debitRequests debitRequests, async innerCt => { - await _grainFactory - .GetInventoryGrain((int)this.GetPrimaryKeyLong()) - .GrantCatalogOfferAsync(offer, extraParam, quantity, innerCt) - .ConfigureAwait(true); - + // Ordered least-to-most reversible: a failure tracking a stat is harmless to + // compensate, but a failure *after* the inventory grant would leave the item + // granted for free once the wallet refunds. See SEC-06. if (creditCost > 0) { await _grainFactory @@ -87,17 +85,9 @@ await _grainFactory .ConfigureAwait(true); } - await _events - .PublishAsync( - new CatalogPurchasedEvent( - (int)this.GetPrimaryKeyLong(), - catalogType.ToString(), - offerId, - quantity, - creditCost - ), - innerCt - ) + await _grainFactory + .GetInventoryGrain((int)this.GetPrimaryKeyLong()) + .GrantCatalogOfferAsync(offer, extraParam, quantity, innerCt) .ConfigureAwait(true); return offer; @@ -112,6 +102,22 @@ await _events throw CreateInsufficientBalanceException(result.Failure); } + // Published after the purchase has fully succeeded and is out of the compensated scope: it + // is a notification, not a purchase step, so a failing subscriber must never be able to + // trigger a refund of an already-completed sale (SEC-06). + await _events + .PublishAsync( + new CatalogPurchasedEvent( + (int)this.GetPrimaryKeyLong(), + catalogType.ToString(), + offerId, + quantity, + creditCost + ), + ct + ) + .ConfigureAwait(true); + return result.Reward!; } From 9ea9c9127bfefd5c420cf8849951a1045c8fd58a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 01:36:43 +0000 Subject: [PATCH 05/11] test: add safety-net coverage for Crypto, Authentication, and Pipeline TEST-01: these three subsystems had zero tests despite being the highest-consequence code to regress silently (a weakened cipher or a broken dispatch engine breaks everything downstream without a single existing test noticing). - Vortex.Crypto.Tests (34 tests): RC4 round-trip plus a classical known-answer vector, key-length guards, the CODE-02 argument validation on ProcessBytes, Peek non-mutation; RSA encrypt/decrypt round trip, sign/verify, corrupted-ciphertext rejection; DiffieService's real Diffie-Hellman exchange cross-checked against a manual ModPow, plus rejection of small-subgroup/out-of-range client public keys (0, 1, prime-1, prime). - Vortex.Authentication.Tests (13 tests, EF Core InMemory VortexDbContext): valid-ticket resolution, PlayerLoggedInEvent/ PlayerLoginFailedEvent publishing, expired-ticket deletion vs. the IsLocked exemption, the ExpiresAt ?? CreatedAt+TTL fallback, and the sliding-expiry refresh (skipped when locked). - Vortex.Pipeline.Tests (13 tests): drives EnvelopeHost directly with local test envelope/context types - handler dispatch, parallel vs. sequential execution, [Order]-attributed behavior sequencing, disposal-based unregistration, EnableInheritanceDispatch, the OnHandlerActivationError/OnHandlerInvokeError/OnNoHandlerRegistered callbacks, and MaxHandlerDegreeOfParallelism actually bounding concurrency. 624 total tests pass (564 pre-existing + 60 new); build stays clean at 0 warnings. --- .../AuthenticationServiceTests.cs | 446 ++++++++++++++++++ .../Vortex.Authentication.Tests.csproj | 21 + Vortex.Cloud.sln | 42 ++ Vortex.Crypto.Tests/DiffieServiceTests.cs | 195 ++++++++ Vortex.Crypto.Tests/Rc4EngineTests.cs | 214 +++++++++ Vortex.Crypto.Tests/RsaServiceTests.cs | 107 +++++ Vortex.Crypto.Tests/TestRsaKeyFactory.cs | 69 +++ .../Vortex.Crypto.Tests.csproj | 19 + Vortex.Pipeline.Tests/EnvelopeHostTests.cs | 382 +++++++++++++++ Vortex.Pipeline.Tests/PipelineTestFixtures.cs | 167 +++++++ .../Vortex.Pipeline.Tests.csproj | 20 + 11 files changed, 1682 insertions(+) create mode 100644 Vortex.Authentication.Tests/AuthenticationServiceTests.cs create mode 100644 Vortex.Authentication.Tests/Vortex.Authentication.Tests.csproj create mode 100644 Vortex.Crypto.Tests/DiffieServiceTests.cs create mode 100644 Vortex.Crypto.Tests/Rc4EngineTests.cs create mode 100644 Vortex.Crypto.Tests/RsaServiceTests.cs create mode 100644 Vortex.Crypto.Tests/TestRsaKeyFactory.cs create mode 100644 Vortex.Crypto.Tests/Vortex.Crypto.Tests.csproj create mode 100644 Vortex.Pipeline.Tests/EnvelopeHostTests.cs create mode 100644 Vortex.Pipeline.Tests/PipelineTestFixtures.cs create mode 100644 Vortex.Pipeline.Tests/Vortex.Pipeline.Tests.csproj diff --git a/Vortex.Authentication.Tests/AuthenticationServiceTests.cs b/Vortex.Authentication.Tests/AuthenticationServiceTests.cs new file mode 100644 index 00000000..b01c0a3b --- /dev/null +++ b/Vortex.Authentication.Tests/AuthenticationServiceTests.cs @@ -0,0 +1,446 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Vortex.Authentication.Configuration; +using Vortex.Database.Context; +using Vortex.Database.Entities.Players; +using Vortex.Database.Entities.Security; +using Vortex.Primitives.Events; +using Vortex.Primitives.Players.Enums; +using Vortex.Primitives.Rooms.Enums; +using Xunit; + +namespace Vortex.Authentication.Tests; + +/// +/// TEST-01 safety net for : ticket +/// lookup, the ExpiresAt ?? CreatedAt + TicketTtlSeconds expiry computation, the +/// locked-ticket exemption from deletion-on-expiry, the sliding-expiry refresh on a successful +/// login, and which of / +/// gets published for each outcome. +/// +public sealed class AuthenticationServiceTests +{ + private const int TICKET_TTL_SECONDS = 30; + + private sealed class RecordingEventPublisher : IEventPublisher + { + public List Published { get; } = []; + + public Task PublishAsync(IEvent @event, CancellationToken ct = default) + { + Published.Add(@event); + + return Task.CompletedTask; + } + } + + private sealed class TestDbContextFactory(DbContextOptions options) + : IDbContextFactory + { + public VortexDbContext CreateDbContext() => new(options); + } + + private static DbContextOptions NewOptions() => + new DbContextOptionsBuilder() + .UseInMemoryDatabase($"auth-{Guid.NewGuid():N}") + .Options; + + private static AuthenticationService NewService( + DbContextOptions options, + RecordingEventPublisher events, + int ticketTtlSeconds = TICKET_TTL_SECONDS + ) + { + AuthenticationConfig config = new() + { + IpHashSecret = "test-secret", + TicketTtlSeconds = ticketTtlSeconds, + }; + + return new AuthenticationService( + new TestDbContextFactory(options), + events, + Options.Create(config) + ); + } + + private static PlayerEntity NewPlayer(string name) => + new() + { + Name = name, + Figure = "hr-115-42.hd-195-19.ch-3030-82.lg-275-1408.fa-1201.ca-1804-64", + Gender = AvatarGenderType.Male, + PlayerStatus = PlayerStatusType.Offline, + PlayerPerks = PlayerPerkFlags.None, + }; + + private static async Task SeedPlayerAsync( + DbContextOptions options, + string name + ) + { + await using VortexDbContext ctx = new(options); + PlayerEntity player = NewPlayer(name); + ctx.Players.Add(player); + await ctx.SaveChangesAsync().ConfigureAwait(true); + + return player; + } + + private static async Task SeedTicketAsync( + DbContextOptions options, + PlayerEntity player, + string ticket, + DateTime createdAt, + DateTime? expiresAt, + bool isLocked + ) + { + await using VortexDbContext ctx = new(options); + // `player` was loaded/created against a different DbContext instance -- attach it as + // Unchanged here first so SaveChanges does not try to re-INSERT an already-persisted row + // via the required PlayerEntity navigation. + ctx.Players.Attach(player); + ctx.SecurityTickets.Add( + new SecurityTicketEntity + { + PlayerEntityId = player.Id, + PlayerEntity = player, + Ticket = ticket, + IpAddress = "127.0.0.1", + IsLocked = isLocked, + ExpiresAt = expiresAt, + CreatedAt = createdAt, + } + ); + await ctx.SaveChangesAsync().ConfigureAwait(true); + } + + [Fact] + public async Task ValidNonExpiredTicket_ResolvesToTheCorrectPlayerId() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "alice").ConfigureAwait(true); + await SeedTicketAsync( + options, + player, + "valid-ticket", + createdAt: DateTime.UtcNow, + expiresAt: DateTime.UtcNow.AddSeconds(60), + isLocked: false + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + int playerId = await service + .GetPlayerIdFromTicketAsync("valid-ticket") + .ConfigureAwait(true); + + playerId.Should().Be(player.Id); + } + + [Fact] + public async Task ValidTicket_PublishesPlayerLoggedInEvent() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "bob").ConfigureAwait(true); + await SeedTicketAsync( + options, + player, + "valid-ticket", + createdAt: DateTime.UtcNow, + expiresAt: DateTime.UtcNow.AddSeconds(60), + isLocked: false + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + await service.GetPlayerIdFromTicketAsync("valid-ticket").ConfigureAwait(true); + + events.Published.Should().ContainSingle().Which.Should().BeOfType(); + ((PlayerLoggedInEvent)events.Published[0]).PlayerId.Should().Be(player.Id); + } + + [Fact] + public async Task MissingTicket_ReturnsZero_AndPublishesLoginFailedEvent() + { + DbContextOptions options = NewOptions(); + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + int playerId = await service + .GetPlayerIdFromTicketAsync("does-not-exist") + .ConfigureAwait(true); + + playerId.Should().Be(0); + events.Published.Should().ContainSingle().Which.Should().BeOfType(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task NullOrEmptyTicket_ReturnsZero_WithoutTouchingTheDatabaseOrPublishing( + string? ticket + ) + { + DbContextOptions options = NewOptions(); + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + int playerId = await service + .GetPlayerIdFromTicketAsync(ticket!) + .ConfigureAwait(true); + + playerId.Should().Be(0); + events.Published.Should().BeEmpty(); + } + + [Fact] + public async Task ExpiredNonLockedTicket_IsRejected_AndDeletedFromTheDatabase() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "carol").ConfigureAwait(true); + await SeedTicketAsync( + options, + player, + "expired-ticket", + createdAt: DateTime.UtcNow.AddMinutes(-10), + expiresAt: DateTime.UtcNow.AddMinutes(-1), + isLocked: false + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + int playerId = await service + .GetPlayerIdFromTicketAsync("expired-ticket") + .ConfigureAwait(true); + + playerId.Should().Be(0); + events.Published.Should().ContainSingle().Which.Should().BeOfType(); + + await using VortexDbContext verifyCtx = new(options); + bool stillExists = await verifyCtx + .SecurityTickets.AnyAsync(t => t.Ticket == "expired-ticket") + .ConfigureAwait(true); + stillExists.Should().BeFalse("an expired, non-locked ticket must be deleted"); + } + + [Fact] + public async Task ExpiredButLockedTicket_IsRejected_ButNotDeleted() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "dave").ConfigureAwait(true); + await SeedTicketAsync( + options, + player, + "locked-expired-ticket", + createdAt: DateTime.UtcNow.AddMinutes(-10), + expiresAt: DateTime.UtcNow.AddMinutes(-1), + isLocked: true + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + int playerId = await service + .GetPlayerIdFromTicketAsync("locked-expired-ticket") + .ConfigureAwait(true); + + // Current behaviour under test (not an endorsement): a locked ticket is rejected as + // expired just like any other, but the IsLocked branch skips the delete. + playerId.Should().Be(0); + events.Published.Should().ContainSingle().Which.Should().BeOfType(); + + await using VortexDbContext verifyCtx = new(options); + bool stillExists = await verifyCtx + .SecurityTickets.AnyAsync(t => t.Ticket == "locked-expired-ticket") + .ConfigureAwait(true); + stillExists.Should().BeTrue("locked tickets are exempt from delete-on-expiry"); + } + + [Fact] + public async Task ValidTicket_WithNullExpiresAt_FallsBackToCreatedAtPlusTtl_AndIsRejectedOnceStale() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "erin").ConfigureAwait(true); + // CreatedAt is far enough in the past that CreatedAt + TicketTtlSeconds has already + // elapsed, with ExpiresAt left null so the TTL fallback is what's actually exercised. + await SeedTicketAsync( + options, + player, + "null-expiry-stale", + createdAt: DateTime.UtcNow.AddSeconds(-(TICKET_TTL_SECONDS + 5)), + expiresAt: null, + isLocked: false + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + int playerId = await service + .GetPlayerIdFromTicketAsync("null-expiry-stale") + .ConfigureAwait(true); + + playerId.Should().Be(0); + events.Published.Should().ContainSingle().Which.Should().BeOfType(); + } + + [Fact] + public async Task ValidTicket_WithNullExpiresAt_WithinTtl_Succeeds() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "frank").ConfigureAwait(true); + await SeedTicketAsync( + options, + player, + "null-expiry-fresh", + createdAt: DateTime.UtcNow, + expiresAt: null, + isLocked: false + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + int playerId = await service + .GetPlayerIdFromTicketAsync("null-expiry-fresh") + .ConfigureAwait(true); + + playerId.Should().Be(player.Id); + } + + [Fact] + public async Task ZeroTicketTtlSeconds_DisablesTheNullExpiryFallback_SoNullExpiryNeverExpires() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "gina").ConfigureAwait(true); + await SeedTicketAsync( + options, + player, + "no-ttl-ticket", + createdAt: DateTime.UtcNow.AddYears(-1), + expiresAt: null, + isLocked: false + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events, ticketTtlSeconds: 0); + + int playerId = await service + .GetPlayerIdFromTicketAsync("no-ttl-ticket") + .ConfigureAwait(true); + + playerId.Should().Be(player.Id); + } + + [Fact] + public async Task SuccessfulLogin_RefreshesExpiresAt_ForSlidingExpiry() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "holly").ConfigureAwait(true); + DateTime originalExpiry = DateTime.UtcNow.AddSeconds(5); + await SeedTicketAsync( + options, + player, + "sliding-ticket", + createdAt: DateTime.UtcNow, + expiresAt: originalExpiry, + isLocked: false + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + await service.GetPlayerIdFromTicketAsync("sliding-ticket").ConfigureAwait(true); + + await using VortexDbContext verifyCtx = new(options); + SecurityTicketEntity refreshed = await verifyCtx + .SecurityTickets.SingleAsync(t => t.Ticket == "sliding-ticket") + .ConfigureAwait(true); + + refreshed + .ExpiresAt.Should() + .NotBe(originalExpiry, "a successful, unlocked login refreshes ExpiresAt"); + refreshed.ExpiresAt.Should().BeAfter(originalExpiry); + } + + [Fact] + public async Task SuccessfulLogin_OnALockedTicket_DoesNotRefreshExpiresAt() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "ian").ConfigureAwait(true); + DateTime originalExpiry = DateTime.UtcNow.AddSeconds(5); + await SeedTicketAsync( + options, + player, + "locked-active-ticket", + createdAt: DateTime.UtcNow, + expiresAt: originalExpiry, + isLocked: true + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + int playerId = await service + .GetPlayerIdFromTicketAsync("locked-active-ticket") + .ConfigureAwait(true); + + // A locked but not-yet-expired ticket still resolves to the player... + playerId.Should().Be(player.Id); + + // ...but the IsLocked branch means the sliding-expiry refresh is skipped. + await using VortexDbContext verifyCtx = new(options); + SecurityTicketEntity unchanged = await verifyCtx + .SecurityTickets.SingleAsync(t => t.Ticket == "locked-active-ticket") + .ConfigureAwait(true); + unchanged.ExpiresAt.Should().Be(originalExpiry); + } + + [Fact] + public async Task RemoteIp_IsHashed_NotStoredOrPublishedInTheClear() + { + DbContextOptions options = NewOptions(); + PlayerEntity player = await SeedPlayerAsync(options, "jack").ConfigureAwait(true); + await SeedTicketAsync( + options, + player, + "ip-ticket", + createdAt: DateTime.UtcNow, + expiresAt: DateTime.UtcNow.AddSeconds(60), + isLocked: false + ) + .ConfigureAwait(true); + + RecordingEventPublisher events = new(); + AuthenticationService service = NewService(options, events); + + await service + .GetPlayerIdFromTicketAsync("ip-ticket", remoteIp: "203.0.113.42") + .ConfigureAwait(true); + + PlayerLoggedInEvent published = events.Published.Should().ContainSingle().Which + as PlayerLoggedInEvent + ?? throw new InvalidOperationException("expected a PlayerLoggedInEvent"); + + published.IpHash.Should().NotBeNullOrEmpty(); + published.IpHash.Should().NotBe("203.0.113.42"); + } +} diff --git a/Vortex.Authentication.Tests/Vortex.Authentication.Tests.csproj b/Vortex.Authentication.Tests/Vortex.Authentication.Tests.csproj new file mode 100644 index 00000000..9ffe12a3 --- /dev/null +++ b/Vortex.Authentication.Tests/Vortex.Authentication.Tests.csproj @@ -0,0 +1,21 @@ + + + net10.0 + disable + enable + false + true + + + + + + + + + + + + + + diff --git a/Vortex.Cloud.sln b/Vortex.Cloud.sln index ae687bfd..dcce6baa 100644 --- a/Vortex.Cloud.sln +++ b/Vortex.Cloud.sln @@ -69,6 +69,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vortex.Plugins.TestPlugin", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vortex.Hosting.Tests", "Vortex.Hosting.Tests\Vortex.Hosting.Tests.csproj", "{79069E02-7C80-4930-97DF-82D320F42FC9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vortex.Crypto.Tests", "Vortex.Crypto.Tests\Vortex.Crypto.Tests.csproj", "{25FB0731-6CCB-4796-A93F-1DD7D476F30F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vortex.Authentication.Tests", "Vortex.Authentication.Tests\Vortex.Authentication.Tests.csproj", "{AE069C3E-DE2D-4737-8CA0-E47B2710C135}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vortex.Pipeline.Tests", "Vortex.Pipeline.Tests\Vortex.Pipeline.Tests.csproj", "{8789349C-266A-4FE3-B5E2-EC8FBCFA1362}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -475,6 +481,42 @@ Global {79069E02-7C80-4930-97DF-82D320F42FC9}.Release|x64.Build.0 = Release|Any CPU {79069E02-7C80-4930-97DF-82D320F42FC9}.Release|x86.ActiveCfg = Release|Any CPU {79069E02-7C80-4930-97DF-82D320F42FC9}.Release|x86.Build.0 = Release|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Debug|x64.ActiveCfg = Debug|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Debug|x64.Build.0 = Debug|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Debug|x86.ActiveCfg = Debug|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Debug|x86.Build.0 = Debug|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Release|Any CPU.Build.0 = Release|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Release|x64.ActiveCfg = Release|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Release|x64.Build.0 = Release|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Release|x86.ActiveCfg = Release|Any CPU + {25FB0731-6CCB-4796-A93F-1DD7D476F30F}.Release|x86.Build.0 = Release|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Debug|x64.ActiveCfg = Debug|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Debug|x64.Build.0 = Debug|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Debug|x86.ActiveCfg = Debug|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Debug|x86.Build.0 = Debug|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Release|Any CPU.Build.0 = Release|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Release|x64.ActiveCfg = Release|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Release|x64.Build.0 = Release|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Release|x86.ActiveCfg = Release|Any CPU + {AE069C3E-DE2D-4737-8CA0-E47B2710C135}.Release|x86.Build.0 = Release|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Debug|x64.ActiveCfg = Debug|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Debug|x64.Build.0 = Debug|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Debug|x86.ActiveCfg = Debug|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Debug|x86.Build.0 = Debug|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Release|Any CPU.Build.0 = Release|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Release|x64.ActiveCfg = Release|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Release|x64.Build.0 = Release|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Release|x86.ActiveCfg = Release|Any CPU + {8789349C-266A-4FE3-B5E2-EC8FBCFA1362}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Vortex.Crypto.Tests/DiffieServiceTests.cs b/Vortex.Crypto.Tests/DiffieServiceTests.cs new file mode 100644 index 00000000..7ce17328 --- /dev/null +++ b/Vortex.Crypto.Tests/DiffieServiceTests.cs @@ -0,0 +1,195 @@ +using System; +using System.Text; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities.Encoders; +using Vortex.Crypto.Configuration; +using Xunit; + +namespace Vortex.Crypto.Tests; + +/// +/// TEST-01 safety net for . Per the audit, the DH private/public key +/// pair is generated once in the constructor (one per service instance), not per call, so: +/// +/// the SAME instance must answer identically every +/// time; +/// two DIFFERENT instances must land on different public values (fresh randomness happens at +/// construction, not at call time). +/// +/// The safe-prime hex constant below is copied verbatim from DiffieService.DH_SAFE_PRIME_HEX +/// (it is private, so it cannot be read off the class under test) purely so tests can independently +/// recompute the expected generator/prime and cross-check a real Diffie-Hellman exchange by manual +/// modpow. +/// +public sealed class DiffieServiceTests +{ + private const string DH_SAFE_PRIME_HEX = + "BA772B7ED412CCEAFB345933614CEC83FD8D5F4DC5EC1DC08382BAD6B0CA97224EF920BD50CCFBE709993C09A766AE87"; + + private static readonly BigInteger s_prime = new(DH_SAFE_PRIME_HEX, 16); + private static readonly BigInteger s_generator = BigInteger.Two; + + private readonly struct Fixture(DiffieService diffie, RsaService rsa, RsaKeyParameters publicKey) + { + public DiffieService Diffie { get; } = diffie; + public RsaService Rsa { get; } = rsa; + public RsaKeyParameters PublicKey { get; } = publicKey; + } + + private static Fixture NewService() + { + (RsaKeyParameters pub, RsaPrivateCrtKeyParameters priv) = TestRsaKeyFactory.Generate(); + IOptions options = TestRsaKeyFactory.ToOptions(pub, priv); + RsaService rsa = new(options); + + return new Fixture(new DiffieService(rsa), rsa, pub); + } + + /// Mirrors DiffieService.EncryptBigInteger's (private) decode side, using only + /// the public key -- exactly what a real client does to read the server's signed values. + private static BigInteger DecodeSigned(string signedHex, RsaKeyParameters publicKey) + { + byte[] signedBytes = Hex.Decode(signedHex); + byte[] plain = TestRsaKeyFactory.VerifyWithPublicKey(publicKey, signedBytes); + + return new BigInteger(Encoding.UTF8.GetString(plain)); + } + + /// Mirrors what a real client does before calling GenerateSharedKey: RSA-encrypt + /// (public key) the decimal string of its own DH public value. + private static string EncryptClientPublicKey(BigInteger clientPublic, RsaService rsa) + { + byte[] payload = Encoding.UTF8.GetBytes(clientPublic.ToString(10)); + byte[] encrypted = rsa.Encrypt(payload); + + return Hex.ToHexString(encrypted); + } + + [Fact] + public void GetPublicKey_SameInstance_IsDeterministicAcrossCalls() + { + Fixture fixture = NewService(); + + BigInteger first = DecodeSigned(fixture.Diffie.GetPublicKey(), fixture.PublicKey); + BigInteger second = DecodeSigned(fixture.Diffie.GetPublicKey(), fixture.PublicKey); + + second.Should().Be(first, "the DH keypair is generated once per instance, not per call"); + } + + [Fact] + public void GetPublicKey_TwoDifferentInstances_ProduceDifferentValues() + { + Fixture fixture = NewService(); + DiffieService second = new(fixture.Rsa); + + BigInteger firstValue = DecodeSigned(fixture.Diffie.GetPublicKey(), fixture.PublicKey); + BigInteger secondValue = DecodeSigned(second.GetPublicKey(), fixture.PublicKey); + + secondValue + .Should() + .NotBe(firstValue, "each instance generates its own random DH private key"); + } + + [Fact] + public void GetSignedPrime_MatchesTheConfiguredSafePrime() + { + Fixture fixture = NewService(); + + BigInteger decoded = DecodeSigned(fixture.Diffie.GetSignedPrime(), fixture.PublicKey); + + decoded.Should().Be(s_prime); + } + + [Fact] + public void GetSignedGenerator_IsTwo() + { + Fixture fixture = NewService(); + + BigInteger decoded = DecodeSigned(fixture.Diffie.GetSignedGenerator(), fixture.PublicKey); + + decoded.Should().Be(s_generator); + } + + [Fact] + public void GenerateSharedKey_MatchesManualModPowComputation() + { + Fixture fixture = NewService(); + + BigInteger serverPublic = DecodeSigned(fixture.Diffie.GetPublicKey(), fixture.PublicKey); + + // Act as the "client": generate our own DH keypair against the same public group. + SecureRandom random = new(); + BigInteger clientPrivate = new BigInteger(256, random).SetBit(255); + BigInteger clientPublic = s_generator.ModPow(clientPrivate, s_prime); + + string encryptedClientPublic = EncryptClientPublicKey(clientPublic, fixture.Rsa); + + byte[] serverComputedShared = fixture.Diffie.GenerateSharedKey(encryptedClientPublic); + + // The client-side computation of the very same shared secret: serverPublic ^ clientPrivate. + byte[] expectedShared = serverPublic.ModPow(clientPrivate, s_prime).ToByteArrayUnsigned(); + + serverComputedShared.Should().Equal(expectedShared); + } + + [Fact] + public void GenerateSharedKey_RejectsZero() + { + Fixture fixture = NewService(); + string encrypted = EncryptClientPublicKey(BigInteger.Zero, fixture.Rsa); + + Action act = () => fixture.Diffie.GenerateSharedKey(encrypted); + + act.Should().Throw(); + } + + [Fact] + public void GenerateSharedKey_RejectsOne() + { + Fixture fixture = NewService(); + string encrypted = EncryptClientPublicKey(BigInteger.One, fixture.Rsa); + + Action act = () => fixture.Diffie.GenerateSharedKey(encrypted); + + act.Should().Throw(); + } + + [Fact] + public void GenerateSharedKey_RejectsPrimeMinusOne() + { + Fixture fixture = NewService(); + string encrypted = EncryptClientPublicKey(s_prime.Subtract(BigInteger.One), fixture.Rsa); + + Action act = () => fixture.Diffie.GenerateSharedKey(encrypted); + + act.Should().Throw(); + } + + [Fact] + public void GenerateSharedKey_RejectsThePrimeItself() + { + Fixture fixture = NewService(); + string encrypted = EncryptClientPublicKey(s_prime, fixture.Rsa); + + Action act = () => fixture.Diffie.GenerateSharedKey(encrypted); + + act.Should().Throw(); + } + + [Fact] + public void GenerateSharedKey_AcceptsValueJustInsideTheValidRange() + { + Fixture fixture = NewService(); + // Valid range is documented as [2, prime - 1) -- prime - 2 is the top boundary that must + // still be accepted. + string encrypted = EncryptClientPublicKey(s_prime.Subtract(BigInteger.Two), fixture.Rsa); + + Action act = () => fixture.Diffie.GenerateSharedKey(encrypted); + + act.Should().NotThrow(); + } +} diff --git a/Vortex.Crypto.Tests/Rc4EngineTests.cs b/Vortex.Crypto.Tests/Rc4EngineTests.cs new file mode 100644 index 00000000..f2bf54ae --- /dev/null +++ b/Vortex.Crypto.Tests/Rc4EngineTests.cs @@ -0,0 +1,214 @@ +using System; +using System.Text; +using FluentAssertions; +using Xunit; + +namespace Vortex.Crypto.Tests; + +/// +/// TEST-01 safety net for : round-trip correctness, one well-known RC4 +/// test vector, the constructor's key-length guards, and the CODE-02 argument validation added to +/// . +/// +public sealed class Rc4EngineTests +{ + private static byte[] Key(string s) => Encoding.UTF8.GetBytes(s); + + [Fact] + public void RoundTrip_EncryptThenDecrypt_ReturnsOriginalPlaintext() + { + byte[] key = Key("a reasonably long RC4 session key, not just a couple of bytes"); + byte[] plaintext = new byte[4096]; + new Random(1234).NextBytes(plaintext); + + Rc4Engine encryptor = new(key); + byte[] ciphertext = encryptor.Process(plaintext); + + ciphertext.Should().NotEqual(plaintext, "RC4 must actually transform the data"); + + // A fresh engine re-keyed identically must reproduce the same keystream so it can decrypt. + Rc4Engine decryptor = new(key); + byte[] decrypted = decryptor.Process(ciphertext); + + decrypted.Should().Equal(plaintext); + } + + [Fact] + public void KnownVector_KeyPlaintext_MatchesStandardRc4Ciphertext() + { + // Classical RC4 test vector (Key="Key", Plaintext="Plaintext"), widely reproduced across + // reference implementations. dropN=0 / no offset matches the plain, undropped RC4 stream. + Rc4Engine engine = new(Key("Key")); + byte[] ciphertext = engine.Process(Key("Plaintext")); + + Convert.ToHexString(ciphertext).Should().Be("BBF316E8D940AF0AD3"); + } + + [Fact] + public void Constructor_NullKey_ThrowsArgumentNullException() + { + Action act = () => _ = new Rc4Engine(null!); + + act.Should().Throw(); + } + + [Fact] + public void Constructor_EmptyKey_ThrowsArgumentException() + { + Action act = () => _ = new Rc4Engine(Array.Empty()); + + act.Should().Throw(); + } + + [Fact] + public void Constructor_KeyLongerThan256Bytes_ThrowsArgumentException() + { + Action act = () => _ = new Rc4Engine(new byte[257]); + + act.Should().Throw(); + } + + [Fact] + public void Constructor_KeyExactly256Bytes_Succeeds() + { + Action act = () => _ = new Rc4Engine(new byte[256]); + + act.Should().NotThrow(); + } + + [Fact] + public void Constructor_NegativeDropN_ThrowsArgumentOutOfRangeException() + { + Action act = () => _ = new Rc4Engine(Key("some-key"), dropN: -1); + + act.Should().Throw(); + } + + [Theory] + [InlineData(-1, 0)] + [InlineData(0, -1)] + public void ProcessBytes_NegativeOffset_ThrowsArgumentOutOfRangeException( + int inputOffset, + int outputOffset + ) + { + Rc4Engine engine = new(Key("k")); + byte[] buffer = new byte[8]; + + Action act = () => + engine.ProcessBytes(buffer, inputOffset, 4, buffer, outputOffset); + + act.Should().Throw(); + } + + [Fact] + public void ProcessBytes_NegativeLength_ThrowsArgumentOutOfRangeException() + { + Rc4Engine engine = new(Key("k")); + byte[] buffer = new byte[8]; + + Action act = () => engine.ProcessBytes(buffer, 0, -1, buffer, 0); + + act.Should().Throw(); + } + + [Fact] + public void ProcessBytes_NullInputData_ThrowsArgumentNullException() + { + Rc4Engine engine = new(Key("k")); + byte[] buffer = new byte[8]; + + Action act = () => engine.ProcessBytes(null!, 0, 4, buffer, 0); + + act.Should().Throw(); + } + + [Fact] + public void ProcessBytes_NullOutputData_ThrowsArgumentNullException() + { + Rc4Engine engine = new(Key("k")); + byte[] buffer = new byte[8]; + + Action act = () => engine.ProcessBytes(buffer, 0, 4, null!, 0); + + act.Should().Throw(); + } + + [Fact] + public void ProcessBytes_InputBufferShorterThanRequestedLength_ThrowsArgumentException() + { + Rc4Engine engine = new(Key("k")); + byte[] input = new byte[4]; + byte[] output = new byte[8]; + + Action act = () => engine.ProcessBytes(input, 0, 8, output, 0); + + act.Should().Throw(); + } + + [Fact] + public void ProcessBytes_OutputBufferShorterThanRequestedLength_ThrowsArgumentException() + { + Rc4Engine engine = new(Key("k")); + byte[] input = new byte[8]; + byte[] output = new byte[4]; + + Action act = () => engine.ProcessBytes(input, 0, 8, output, 0); + + act.Should().Throw(); + } + + [Fact] + public void Peek_DoesNotAdvanceKeystream_AndMatchesSubsequentProcessBytes() + { + byte[] key = Key("peek-does-not-consume"); + byte[] plaintext = Key("some plaintext that is peeked at before being really processed"); + + Rc4Engine engine = new(key); + + byte[] peeked = engine.Peek(plaintext); + byte[] peekedAgain = engine.Peek(plaintext); + + // Peeking twice must return identical output -- proof it never mutates (_i, _j). + peekedAgain.Should().Equal(peeked); + + byte[] processed = engine.ProcessBytes( + plaintext, + 0, + plaintext.Length, + new byte[plaintext.Length], + 0 + ); + + // The real (state-advancing) call must produce the exact same bytes Peek predicted. + processed.Should().Equal(peeked); + } + + [Fact] + public void DropN_ProducesDifferentKeystreamThanNoDrop() + { + byte[] key = Key("drop-n-changes-the-stream"); + byte[] plaintext = new byte[64]; + new Random(99).NextBytes(plaintext); + + Rc4Engine noDrop = new(key); + Rc4Engine withDrop = new(key, dropN: 256); + + byte[] noDropCipher = noDrop.Process(plaintext); + byte[] withDropCipher = withDrop.Process(plaintext); + + withDropCipher.Should().NotEqual(noDropCipher); + } + + [Fact] + public void SameKey_TwoIndependentEngines_ProduceIdenticalCiphertext() + { + byte[] key = Key("deterministic-keystream"); + byte[] plaintext = Key("identical input must yield identical output for the same key"); + + Rc4Engine first = new(key); + Rc4Engine second = new(key); + + first.Process(plaintext).Should().Equal(second.Process(plaintext)); + } +} diff --git a/Vortex.Crypto.Tests/RsaServiceTests.cs b/Vortex.Crypto.Tests/RsaServiceTests.cs new file mode 100644 index 00000000..75635c28 --- /dev/null +++ b/Vortex.Crypto.Tests/RsaServiceTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Text; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Org.BouncyCastle.Crypto.Parameters; +using Vortex.Crypto.Configuration; +using Xunit; + +namespace Vortex.Crypto.Tests; + +/// +/// TEST-01 safety net for : encrypt/decrypt round-trips at the configured +/// key size, sign/verify round-trips, and that corrupted ciphertext throws instead of silently +/// returning wrong bytes (PKCS#1 padding failure). +/// +public sealed class RsaServiceTests +{ + private static (RsaService Service, RsaKeyParameters PublicKey) NewService() + { + (RsaKeyParameters pub, RsaPrivateCrtKeyParameters priv) = TestRsaKeyFactory.Generate(); + IOptions options = TestRsaKeyFactory.ToOptions(pub, priv); + + return (new RsaService(options), pub); + } + + [Theory] + [InlineData("short")] + [InlineData("a somewhat longer payload that still fits in a single 1024-bit RSA/PKCS#1 block")] + public void EncryptThenDecrypt_RoundTrips_ToOriginalBytes(string plaintext) + { + (RsaService service, _) = NewService(); + byte[] original = Encoding.UTF8.GetBytes(plaintext); + + byte[] encrypted = service.Encrypt(original); + byte[] decrypted = service.Decrypt(encrypted); + + decrypted.Should().Equal(original); + } + + [Fact] + public void Encrypt_ProducesCiphertextDifferentFromPlaintext() + { + (RsaService service, _) = NewService(); + byte[] original = Encoding.UTF8.GetBytes("do not leak me in the clear"); + + byte[] encrypted = service.Encrypt(original); + + encrypted.Should().NotEqual(original); + } + + [Fact] + public void Encrypt_IsNonDeterministic_AcrossCalls() + { + // PKCS#1 v1.5 encryption pads with random bytes, so the same plaintext must not encrypt to + // the same ciphertext twice -- a regression here would mean padding got dropped. + (RsaService service, _) = NewService(); + byte[] original = Encoding.UTF8.GetBytes("same plaintext, twice"); + + byte[] first = service.Encrypt(original); + byte[] second = service.Encrypt(original); + + first.Should().NotEqual(second); + service.Decrypt(first).Should().Equal(original); + service.Decrypt(second).Should().Equal(original); + } + + [Fact] + public void SignThenVerifyWithPublicKey_RecoversOriginalBytes() + { + (RsaService service, RsaKeyParameters publicKey) = NewService(); + byte[] original = Encoding.UTF8.GetBytes("398471209384712093847120938471209"); + + byte[] signed = service.Sign(original); + byte[] recovered = TestRsaKeyFactory.VerifyWithPublicKey(publicKey, signed); + + recovered.Should().Equal(original); + } + + [Fact] + public void Decrypt_CorruptedCiphertext_ThrowsInsteadOfReturningWrongData() + { + (RsaService service, _) = NewService(); + byte[] encrypted = service.Encrypt(Encoding.UTF8.GetBytes("legit payload")); + + // Flip a byte in the middle of the ciphertext block -- PKCS#1 padding must reject this. + encrypted[encrypted.Length / 2] ^= 0xFF; + + Action act = () => service.Decrypt(encrypted); + + act.Should().Throw(); + } + + [Fact] + public void Decrypt_RandomGarbageOfCorrectBlockSize_Throws() + { + (RsaService service, RsaKeyParameters publicKey) = NewService(); + byte[] garbage = new byte[(publicKey.Modulus.BitLength + 7) / 8]; + new Random(42).NextBytes(garbage); + // PKCS#1 v1.5 requires the first byte to be 0x00; force it invalid so this is not a flaky, + // astronomically-unlikely-to-pass-by-chance assertion. + garbage[0] = 0xFF; + + Action act = () => service.Decrypt(garbage); + + act.Should().Throw(); + } +} diff --git a/Vortex.Crypto.Tests/TestRsaKeyFactory.cs b/Vortex.Crypto.Tests/TestRsaKeyFactory.cs new file mode 100644 index 00000000..b448dad8 --- /dev/null +++ b/Vortex.Crypto.Tests/TestRsaKeyFactory.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Options; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Encodings; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Vortex.Crypto.Configuration; + +namespace Vortex.Crypto.Tests; + +/// +/// Generates a fresh, real (never hard-coded/fabricated) RSA key pair for each test run and wraps +/// it as the shape expects, plus a bare +/// "verify with the public key" helper -- only exposes +/// Encrypt/Decrypt/Sign (both Decrypt and Sign always use the private key), so recovering the +/// plaintext behind a value produced requires driving BouncyCastle's +/// PKCS#1 engine directly with the public key, exactly like a real client would. +/// +internal static class TestRsaKeyFactory +{ + public static (RsaKeyParameters PublicKey, RsaPrivateCrtKeyParameters PrivateKey) Generate( + int strengthBits = 1024 + ) + { + RsaKeyPairGenerator generator = new(); + generator.Init( + new RsaKeyGenerationParameters( + BigInteger.ValueOf(0x10001), + new SecureRandom(), + strengthBits, + 80 + ) + ); + + AsymmetricCipherKeyPair pair = generator.GenerateKeyPair(); + + return ((RsaKeyParameters)pair.Public, (RsaPrivateCrtKeyParameters)pair.Private); + } + + public static IOptions ToOptions( + RsaKeyParameters publicKey, + RsaPrivateCrtKeyParameters privateKey + ) + { + CryptoConfig config = new() + { + KeySize = publicKey.Exponent.ToString(16), + PublicKey = publicKey.Modulus.ToString(16), + PrivateKey = privateKey.Exponent.ToString(16), + EnableServerToClientEncryption = true, + }; + + return Options.Create(config); + } + + /// + /// The public-key-side inverse of : recovers the bytes that were + /// signed, using only the public modulus/exponent (never the private key). + /// + public static byte[] VerifyWithPublicKey(RsaKeyParameters publicKey, byte[] signed) + { + Pkcs1Encoding cipher = new(new RsaEngine()); + cipher.Init(false, publicKey); + + return cipher.ProcessBlock(signed, 0, signed.Length); + } +} diff --git a/Vortex.Crypto.Tests/Vortex.Crypto.Tests.csproj b/Vortex.Crypto.Tests/Vortex.Crypto.Tests.csproj new file mode 100644 index 00000000..677655ed --- /dev/null +++ b/Vortex.Crypto.Tests/Vortex.Crypto.Tests.csproj @@ -0,0 +1,19 @@ + + + net10.0 + disable + enable + false + true + + + + + + + + + + + + diff --git a/Vortex.Pipeline.Tests/EnvelopeHostTests.cs b/Vortex.Pipeline.Tests/EnvelopeHostTests.cs new file mode 100644 index 00000000..5957a7db --- /dev/null +++ b/Vortex.Pipeline.Tests/EnvelopeHostTests.cs @@ -0,0 +1,382 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Vortex.Pipeline.Attributes; +using Vortex.Pipeline.Delegates; +using Vortex.Runtime; +using Xunit; + +namespace Vortex.Pipeline.Tests; + +/// +/// TEST-01 safety net for the generic dispatch engine in +/// (shared by Vortex.Messages and Vortex.Events), exercised directly with locally-defined +/// envelope/context types rather than any real Vortex message. +/// +public sealed class EnvelopeHostTests +{ + private static ServiceProvider NewProvider(Recorder recorder, ConcurrencyTracker? tracker = null) + { + ServiceCollection services = new(); + services.AddSingleton(recorder); + + if (tracker is not null) + { + services.AddSingleton(tracker); + } + + return services.BuildServiceProvider(); + } + + private static EnvelopeHost NewHost( + IServiceProvider sp, + bool enableInheritance = true, + HandlerExecutionMode mode = HandlerExecutionMode.Parallel, + int? maxDop = null, + Action? onHandlerActivationError = null, + Action? onHandlerInvokeError = null, + Action? onNoHandlerRegistered = null + ) + { + EnvelopeHostOptions options = new() + { + CreateContextAsync = (env, meta) => Task.FromResult(new TestContext()), + EnableInheritanceDispatch = enableInheritance, + HandlerMode = mode, + MaxHandlerDegreeOfParallelism = maxDop, + OnHandlerActivationError = onHandlerActivationError, + OnHandlerInvokeError = onHandlerInvokeError, + OnNoHandlerRegistered = onNoHandlerRegistered, + }; + + return new EnvelopeHost(sp, options); + } + + private static IDisposable RegisterHandler( + EnvelopeHost host, + IServiceProvider sp, + Type envType + ) + where THandler : class + { + EnvelopeInvokerFactory factory = new(); + HandlerInvoker invoker = factory.CreateHandlerInvoker(typeof(THandler), envType); + Func activator = ActivatorHelpers.BuildActivator(typeof(THandler)); + + return host.RegisterHandler(envType, sp, activator, invoker); + } + + private static IDisposable RegisterBehavior( + EnvelopeHost host, + IServiceProvider sp, + Type envType + ) + where TBehavior : class + { + EnvelopeInvokerFactory factory = new(); + BehaviorInvoker invoker = factory.CreateBehaviorInvoker( + typeof(TBehavior), + envType + ); + Func activator = ActivatorHelpers.BuildActivator(typeof(TBehavior)); + int order = typeof(TBehavior).GetCustomAttribute()?.Value ?? 0; + + return host.RegisterBehavior(envType, sp, activator, invoker, order); + } + + [Fact] + public async Task RegisterHandler_PublishedEnvelope_IsReceivedByHandler() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + EnvelopeHost host = NewHost(sp); + using IDisposable reg = RegisterHandler(host, sp, typeof(TestEnvelope)); + + await host + .PublishAsync(new TestEnvelope { Payload = "hello" }, null, CancellationToken.None) + .ConfigureAwait(true); + + recorder.Entries.Should().Equal("A:hello"); + } + + [Fact] + public void DefaultHandlerMode_IsParallel() + { + EnvelopeHostOptions options = new() + { + CreateContextAsync = (env, meta) => Task.FromResult(new TestContext()), + }; + + options.HandlerMode.Should().Be(HandlerExecutionMode.Parallel); + } + + [Fact] + public async Task MultipleHandlersForSameEnvelopeType_AllRun_UnderTheDefaultParallelMode() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + EnvelopeHost host = NewHost(sp); + using IDisposable regA = RegisterHandler(host, sp, typeof(TestEnvelope)); + using IDisposable regB = RegisterHandler(host, sp, typeof(TestEnvelope)); + + await host + .PublishAsync(new TestEnvelope { Payload = "x" }, null, CancellationToken.None) + .ConfigureAwait(true); + + recorder.Entries.Should().BeEquivalentTo(["A:x", "B:x"]); + } + + [Fact] + public async Task SequentialMode_HandlersRunInRegistrationOrder() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + EnvelopeHost host = NewHost( + sp, + mode: HandlerExecutionMode.Sequential + ); + using IDisposable regB = RegisterHandler(host, sp, typeof(TestEnvelope)); + using IDisposable regA = RegisterHandler(host, sp, typeof(TestEnvelope)); + + await host + .PublishAsync(new TestEnvelope { Payload = "y" }, null, CancellationToken.None) + .ConfigureAwait(true); + + recorder.Entries.Should().Equal("B:y", "A:y"); + } + + [Fact] + public async Task Behaviors_RunInOrderAttributeSequence_BeforeHandlers() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + EnvelopeHost host = NewHost(sp); + + // Registered deliberately out of numeric order to prove the pipeline sorts by + // [Order], not by registration order. + using IDisposable regLow = RegisterBehavior( + host, + sp, + typeof(TestEnvelope) + ); + using IDisposable regUnordered = RegisterBehavior( + host, + sp, + typeof(TestEnvelope) + ); + using IDisposable regHigh = RegisterBehavior( + host, + sp, + typeof(TestEnvelope) + ); + using IDisposable regHandler = RegisterHandler( + host, + sp, + typeof(TestEnvelope) + ); + + await host + .PublishAsync(new TestEnvelope { Payload = "z" }, null, CancellationToken.None) + .ConfigureAwait(true); + + recorder + .Entries.Should() + .Equal( + "Behavior(-5):before", + "Behavior(0):before", + "Behavior(10):before", + "A:z", + "Behavior(10):after", + "Behavior(0):after", + "Behavior(-5):after" + ); + } + + [Fact] + public async Task DisposingTheRegistration_UnregistersTheHandler_StopsFurtherInvocations() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + EnvelopeHost host = NewHost(sp); + IDisposable reg = RegisterHandler(host, sp, typeof(TestEnvelope)); + + await host + .PublishAsync(new TestEnvelope { Payload = "1" }, null, CancellationToken.None) + .ConfigureAwait(true); + + reg.Dispose(); + + await host + .PublishAsync(new TestEnvelope { Payload = "2" }, null, CancellationToken.None) + .ConfigureAwait(true); + + recorder.Entries.Should().Equal("A:1"); + } + + [Fact] + public async Task EnableInheritanceDispatch_True_BaseHandlerReceivesDerivedEnvelope() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + EnvelopeHost host = NewHost(sp, enableInheritance: true); + using IDisposable reg = RegisterHandler(host, sp, typeof(TestEnvelope)); + + await host + .PublishAsync(new DerivedTestEnvelope { Payload = "d" }, null, CancellationToken.None) + .ConfigureAwait(true); + + recorder.Entries.Should().Equal("A:d"); + } + + [Fact] + public async Task EnableInheritanceDispatch_False_BaseHandlerDoesNotReceiveDerivedEnvelope() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + EnvelopeHost host = NewHost( + sp, + enableInheritance: false + ); + using IDisposable reg = RegisterHandler(host, sp, typeof(TestEnvelope)); + + await host + .PublishAsync(new DerivedTestEnvelope { Payload = "d" }, null, CancellationToken.None) + .ConfigureAwait(true); + + recorder.Entries.Should().BeEmpty(); + } + + [Fact] + public async Task OnHandlerActivationError_Fires_AndOtherHandlersStillRun() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + List activationErrors = []; + EnvelopeHost host = NewHost( + sp, + onHandlerActivationError: (ex, env) => activationErrors.Add(ex) + ); + + EnvelopeInvokerFactory factory = new(); + HandlerInvoker invoker = factory.CreateHandlerInvoker( + typeof(RecordingHandlerA), + typeof(TestEnvelope) + ); + Func throwingActivator = _ => + throw new InvalidOperationException("cannot activate"); + using IDisposable badReg = host.RegisterHandler( + typeof(TestEnvelope), + sp, + throwingActivator, + invoker + ); + using IDisposable goodReg = RegisterHandler(host, sp, typeof(TestEnvelope)); + + await host + .PublishAsync(new TestEnvelope { Payload = "e" }, null, CancellationToken.None) + .ConfigureAwait(true); + + activationErrors.Should().ContainSingle(); + activationErrors[0].Should().BeOfType(); + recorder.Entries.Should().Equal("B:e"); + } + + [Fact] + public async Task OnHandlerInvokeError_Fires_AndOtherHandlersStillRun() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + List invokeErrors = []; + EnvelopeHost host = NewHost( + sp, + onHandlerInvokeError: (ex, env) => invokeErrors.Add(ex) + ); + + using IDisposable throwingReg = RegisterHandler( + host, + sp, + typeof(TestEnvelope) + ); + using IDisposable goodReg = RegisterHandler(host, sp, typeof(TestEnvelope)); + + await host + .PublishAsync(new TestEnvelope { Payload = "f" }, null, CancellationToken.None) + .ConfigureAwait(true); + + invokeErrors.Should().ContainSingle(); + invokeErrors[0].Should().BeOfType(); + recorder.Entries.Should().Equal("A:f"); + } + + [Fact] + public async Task OnNoHandlerRegistered_FiresWhenTheBucketExistsButHasNoHandlers() + { + Recorder recorder = new(); + using ServiceProvider sp = NewProvider(recorder); + List noHandlerEnvelopes = []; + EnvelopeHost host = NewHost( + sp, + onNoHandlerRegistered: env => noHandlerEnvelopes.Add(env) + ); + + // Registering only a behavior still creates the envelope-type bucket, with zero handlers. + using IDisposable behaviorReg = RegisterBehavior( + host, + sp, + typeof(TestEnvelope) + ); + + TestEnvelope published = new() { Payload = "none" }; + + await host.PublishAsync(published, null, CancellationToken.None).ConfigureAwait(true); + + noHandlerEnvelopes.Should().ContainSingle(); + noHandlerEnvelopes[0].Should().BeSameAs(published); + recorder.Entries.Should().Equal("Behavior(0):before", "Behavior(0):after"); + } + + [Fact] + public async Task Publish_WithNothingRegisteredForTheEnvelopeType_ReturnsContext_WithoutThrowing() + { + using ServiceProvider sp = NewProvider(new Recorder()); + EnvelopeHost host = NewHost(sp); + + TestContext ctx = await host + .PublishWithContextAsync(new TestEnvelope { Payload = "none" }, null, CancellationToken.None) + .ConfigureAwait(true); + + ctx.Should().NotBeNull(); + } + + [Fact] + public async Task MaxHandlerDegreeOfParallelism_BoundsConcurrentHandlerExecutions() + { + Recorder recorder = new(); + ConcurrencyTracker tracker = new(); + using ServiceProvider sp = NewProvider(recorder, tracker); + EnvelopeHost host = NewHost(sp, maxDop: 2); + + CompositeDisposable regs = new(); + for (int i = 0; i < 6; i++) + { + regs.Add( + RegisterHandler(host, sp, typeof(TestEnvelope)) + ); + } + + await host + .PublishAsync(new TestEnvelope { Payload = "p" }, null, CancellationToken.None) + .ConfigureAwait(true); + + tracker.Max.Should().BeLessThanOrEqualTo(2); + tracker + .Max.Should() + .BeGreaterThan(1, "degree 2 should allow real overlap, not fully serialize the work"); + + regs.Dispose(); + } +} diff --git a/Vortex.Pipeline.Tests/PipelineTestFixtures.cs b/Vortex.Pipeline.Tests/PipelineTestFixtures.cs new file mode 100644 index 00000000..68cbaefa --- /dev/null +++ b/Vortex.Pipeline.Tests/PipelineTestFixtures.cs @@ -0,0 +1,167 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Vortex.Pipeline.Attributes; +using Vortex.Pipeline.Registry; + +namespace Vortex.Pipeline.Tests; + +// A small, self-contained envelope/context/meta shape for exercising EnvelopeHost<,,> directly -- +// deliberately NOT any real Vortex message/event type, per the TEST-01 brief. + +public class TestEnvelope +{ + public string Payload { get; init; } = string.Empty; +} + +public sealed class DerivedTestEnvelope : TestEnvelope { } + +public sealed class TestContext +{ + public bool ShortCircuit { get; set; } +} + +/// Thread-safe call-order/invocation recorder shared (via DI) between the host and the +/// handler/behavior instances it activates for each publish. +public sealed class Recorder +{ + private readonly ConcurrentQueue _entries = new(); + + public IReadOnlyCollection Entries => _entries; + + public void Record(string entry) => _entries.Enqueue(entry); +} + +/// Tracks the maximum number of handlers observed running concurrently, to verify +/// is +/// actually enforced by the dispatch engine and not just accepted as a no-op setting. +public sealed class ConcurrencyTracker +{ + private int _current; + private int _max; + + public int Max => Volatile.Read(ref _max); + + public void Enter() + { + int now = Interlocked.Increment(ref _current); + + int observedMax; + do + { + observedMax = Volatile.Read(ref _max); + if (now <= observedMax) + { + break; + } + } while (Interlocked.CompareExchange(ref _max, now, observedMax) != observedMax); + } + + public void Exit() => Interlocked.Decrement(ref _current); +} + +public sealed class RecordingHandlerA(Recorder recorder) : IHandler +{ + public ValueTask HandleAsync(TestEnvelope env, TestContext ctx, CancellationToken ct) + { + recorder.Record($"A:{env.Payload}"); + + return ValueTask.CompletedTask; + } +} + +public sealed class RecordingHandlerB(Recorder recorder) : IHandler +{ + public ValueTask HandleAsync(TestEnvelope env, TestContext ctx, CancellationToken ct) + { + recorder.Record($"B:{env.Payload}"); + + return ValueTask.CompletedTask; + } +} + +public sealed class ThrowingHandler : IHandler +{ + public sealed class BoomException(string message) : System.Exception(message); + + public ValueTask HandleAsync(TestEnvelope env, TestContext ctx, CancellationToken ct) => + throw new BoomException("handler blew up"); +} + +public sealed class DerivedOnlyHandler(Recorder recorder) + : IHandler +{ + public ValueTask HandleAsync(DerivedTestEnvelope env, TestContext ctx, CancellationToken ct) + { + recorder.Record($"DerivedOnly:{env.Payload}"); + + return ValueTask.CompletedTask; + } +} + +public sealed class ConcurrencyTrackingHandler(ConcurrencyTracker tracker) + : IHandler +{ + public async ValueTask HandleAsync(TestEnvelope env, TestContext ctx, CancellationToken ct) + { + tracker.Enter(); + try + { + await Task.Delay(30, ct).ConfigureAwait(true); + } + finally + { + tracker.Exit(); + } + } +} + +[Order(10)] +public sealed class LowPriorityBehavior(Recorder recorder) : IBehavior +{ + public async ValueTask InvokeAsync( + TestEnvelope env, + TestContext ctx, + System.Func next, + CancellationToken ct + ) + { + recorder.Record("Behavior(10):before"); + await next().ConfigureAwait(true); + recorder.Record("Behavior(10):after"); + } +} + +[Order(-5)] +public sealed class HighPriorityBehavior(Recorder recorder) : IBehavior +{ + public async ValueTask InvokeAsync( + TestEnvelope env, + TestContext ctx, + System.Func next, + CancellationToken ct + ) + { + recorder.Record("Behavior(-5):before"); + await next().ConfigureAwait(true); + recorder.Record("Behavior(-5):after"); + } +} + +/// A behavior with no [Order] attribute -- exercises the "defaults to 0" fallback that +/// EnvelopeFeatureProcessor applies via GetCustomAttribute<OrderAttribute>()?.Value ?? 0. +public sealed class UnorderedBehavior(Recorder recorder) : IBehavior +{ + public async ValueTask InvokeAsync( + TestEnvelope env, + TestContext ctx, + System.Func next, + CancellationToken ct + ) + { + recorder.Record("Behavior(0):before"); + await next().ConfigureAwait(true); + recorder.Record("Behavior(0):after"); + } +} diff --git a/Vortex.Pipeline.Tests/Vortex.Pipeline.Tests.csproj b/Vortex.Pipeline.Tests/Vortex.Pipeline.Tests.csproj new file mode 100644 index 00000000..6e7fd7bc --- /dev/null +++ b/Vortex.Pipeline.Tests/Vortex.Pipeline.Tests.csproj @@ -0,0 +1,20 @@ + + + net10.0 + disable + enable + false + true + + + + + + + + + + + + + From e0a06f501b417b41e62eb9f0c57628f80c830deb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 01:37:01 +0000 Subject: [PATCH 06/11] feat: add transport-level and chat-level rate limiting (SEC-03) Closes the cheapest DoS vector the audit flagged: nothing bounded how many packets a session could send per second, and the protocol's own FloodControlMessageComposer was modeled but never sent by anything. - Transport level: RateLimitBehavior is a global IMessageBehavior registered against IMessageEvent itself (EnableInheritanceDispatch applies it to every concrete message via one registration) with [Order(int.MinValue)] so it runs before any other behavior, handler, or the grain call resolving the message context. Backed by TokenBucketRateLimiter, a per-session token bucket (default 50/s sustained, 100 burst, both configurable under Vortex:Networking:RateLimit) that self-sweeps stale sessions so it doesn't leak memory across reconnects. A rate-limited packet is dropped silently and counted in the existing packets_dropped_total metric (reason "rate_limited") rather than closing the connection. Since Vortex.Messages isn't a plugin module and its assembly is never scanned by PluginBootstrapper, a small MessageBehaviorRegistrationService triggers that scan for it, registered ahead of VortexEmulator so it's active before the network listeners open. - Chat level: RoomChatSystem now enforces a per-player minimum gap between chat lines, keyed by the room's existing (previously unenforced) ChatSettingsSnapshot.FloodSensitivity, and sends FloodControlMessageComposer with the remaining wait instead of silently dropping the line. Thresholds are new tunable defaults under RoomConfig.ChatFloodIntervalSeconds (4/2/1s for Extra/Normal/ Minimal), not a tuned game-balance decision - adjust freely. All 624 tests pass; build stays clean at 0 warnings. --- Vortex.Main/Program.cs | 2 +- .../Behaviors/RateLimitBehavior.cs | 49 +++++++++ .../Configuration/RateLimitConfig.cs | 21 ++++ .../Extensions/ServiceCollectionExtensions.cs | 14 ++- .../MessageBehaviorRegistrationService.cs | 37 +++++++ Vortex.Messages/RateLimit/IRateLimiter.cs | 10 ++ .../RateLimit/TokenBucketRateLimiter.cs | 100 ++++++++++++++++++ Vortex.Rooms/Configuration/RoomConfig.cs | 6 ++ Vortex.Rooms/Grains/Systems/RoomChatSystem.cs | 45 ++++++++ 9 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 Vortex.Messages/Behaviors/RateLimitBehavior.cs create mode 100644 Vortex.Messages/Configuration/RateLimitConfig.cs create mode 100644 Vortex.Messages/MessageBehaviorRegistrationService.cs create mode 100644 Vortex.Messages/RateLimit/IRateLimiter.cs create mode 100644 Vortex.Messages/RateLimit/TokenBucketRateLimiter.cs diff --git a/Vortex.Main/Program.cs b/Vortex.Main/Program.cs index 98659a4e..b3e3865a 100644 --- a/Vortex.Main/Program.cs +++ b/Vortex.Main/Program.cs @@ -102,7 +102,7 @@ IConfigurationProvider p in ((IConfigurationRoot)builder.Configuration).Provider builder.Services.AddVortexPlugins(builder); builder.Services.AddVortexDatabaseContext(builder); builder.Services.AddVortexEventSystem(); - builder.Services.AddVortexMessageSystem(); + builder.Services.AddVortexMessageSystem(builder); builder.Services.AddVortexCrypto(builder); builder.Services.AddVortexRevisions(builder); diff --git a/Vortex.Messages/Behaviors/RateLimitBehavior.cs b/Vortex.Messages/Behaviors/RateLimitBehavior.cs new file mode 100644 index 00000000..478ea6fb --- /dev/null +++ b/Vortex.Messages/Behaviors/RateLimitBehavior.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Vortex.Messages.RateLimit; +using Vortex.Messages.Registry; +using Vortex.Pipeline.Attributes; +using Vortex.Primitives.Networking; +using Vortex.Primitives.Observability; + +namespace Vortex.Messages.Behaviors; + +/// +/// Global per-session token-bucket gate on every inbound message (SEC-03). Registered for +/// itself, so EnableInheritanceDispatch applies it to every +/// concrete message type without listing them; ordered to run before any other behavior or handler +/// so a rate-limited packet never reaches business logic (or the extra grain call that resolving +/// its context would otherwise cost). +/// +[Order(int.MinValue)] +public sealed class RateLimitBehavior( + IRateLimiter limiter, + IVortexMetrics metrics, + ILogger logger +) : IMessageBehavior +{ + public ValueTask InvokeAsync( + IMessageEvent env, + MessageContext ctx, + Func next, + CancellationToken ct + ) + { + if (limiter.TryConsume(ctx.SessionKey)) + { + return next(); + } + + metrics.PacketDropped("rate_limited"); + + logger.LogDebug( + "Rate limit exceeded for session {SessionKey}; dropping {MessageType}.", + ctx.SessionKey, + env.GetType().Name + ); + + return ValueTask.CompletedTask; + } +} diff --git a/Vortex.Messages/Configuration/RateLimitConfig.cs b/Vortex.Messages/Configuration/RateLimitConfig.cs new file mode 100644 index 00000000..604c4f1d --- /dev/null +++ b/Vortex.Messages/Configuration/RateLimitConfig.cs @@ -0,0 +1,21 @@ +namespace Vortex.Messages.Configuration; + +/// +/// Per-session token-bucket limit on inbound packets, enforced by RateLimitBehavior before +/// any handler runs. Generous by default: this exists to close the cheapest DoS vector (a client +/// looping packets at wire speed), not to police normal play. +/// +public sealed class RateLimitConfig +{ + public const string SECTION_NAME = "Vortex:Networking:RateLimit"; + + /// Sustained packets/second a session may send once its burst allowance is spent. + public int MaxPacketsPerSecond { get; init; } = 50; + + /// + /// Bucket capacity - how many packets a session may send in a single instant before the + /// per-second refill rate starts gating it. Defaults to twice the sustained rate so a normal + /// burst (e.g. bootstrap composers, a quick flurry of moves) is never affected. + /// + public int BurstSize { get; init; } = 100; +} diff --git a/Vortex.Messages/Extensions/ServiceCollectionExtensions.cs b/Vortex.Messages/Extensions/ServiceCollectionExtensions.cs index 428fc37f..b6faea18 100644 --- a/Vortex.Messages/Extensions/ServiceCollectionExtensions.cs +++ b/Vortex.Messages/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Vortex.Messages.Configuration; +using Vortex.Messages.RateLimit; using Vortex.Messages.Registry; using Vortex.Pipeline; using Vortex.Runtime.AssemblyProcessing; @@ -7,13 +10,22 @@ namespace Vortex.Messages.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddVortexMessageSystem(this IServiceCollection services) + public static IServiceCollection AddVortexMessageSystem( + this IServiceCollection services, + HostApplicationBuilder builder + ) { services.AddSingleton(); services.AddSingleton>(); services.AddSingleton(); services.AddSingleton(); + services.Configure( + builder.Configuration.GetSection(RateLimitConfig.SECTION_NAME) + ); + services.AddSingleton(); + services.AddHostedService(); + return services; } } diff --git a/Vortex.Messages/MessageBehaviorRegistrationService.cs b/Vortex.Messages/MessageBehaviorRegistrationService.cs new file mode 100644 index 00000000..3329f8d0 --- /dev/null +++ b/Vortex.Messages/MessageBehaviorRegistrationService.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Vortex.Runtime.AssemblyProcessing; + +namespace Vortex.Messages; + +/// +/// Scans this assembly's own IMessageBehavior<>/IMessageHandler<> +/// implementations (currently just ). Every other handler +/// lives in a plugin module assembly, which PluginBootstrapper scans automatically - +/// Vortex.Messages itself isn't a plugin module, so it needs this one small nudge. Registered +/// alongside PluginBootstrapper, before VortexEmulator starts the network listeners. +/// +public sealed class MessageBehaviorRegistrationService( + AssemblyProcessor processor, + IServiceProvider serviceProvider +) : IHostedService +{ + private IDisposable? _registration; + + public async Task StartAsync(CancellationToken ct) + { + _registration = await processor + .ProcessAsync(Assembly.GetExecutingAssembly(), serviceProvider, ct) + .ConfigureAwait(false); + } + + public Task StopAsync(CancellationToken ct) + { + _registration?.Dispose(); + + return Task.CompletedTask; + } +} diff --git a/Vortex.Messages/RateLimit/IRateLimiter.cs b/Vortex.Messages/RateLimit/IRateLimiter.cs new file mode 100644 index 00000000..794cc720 --- /dev/null +++ b/Vortex.Messages/RateLimit/IRateLimiter.cs @@ -0,0 +1,10 @@ +using Vortex.Primitives.Networking; + +namespace Vortex.Messages.RateLimit; + +public interface IRateLimiter +{ + /// Attempts to consume one token for . Returns false when the + /// session's bucket is empty and the packet should be dropped instead of dispatched. + bool TryConsume(SessionKey session); +} diff --git a/Vortex.Messages/RateLimit/TokenBucketRateLimiter.cs b/Vortex.Messages/RateLimit/TokenBucketRateLimiter.cs new file mode 100644 index 00000000..7379e9a8 --- /dev/null +++ b/Vortex.Messages/RateLimit/TokenBucketRateLimiter.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using Microsoft.Extensions.Options; +using Vortex.Messages.Configuration; +using Vortex.Primitives.Networking; + +namespace Vortex.Messages.RateLimit; + +/// +/// One token bucket per session, refilled continuously at +/// up to . Registered as a singleton: the buckets must +/// outlive any single packet's dispatch, since RateLimitBehavior itself is constructed fresh +/// per invocation by the pipeline. +/// +public sealed class TokenBucketRateLimiter : IRateLimiter +{ + // Swept every this-many calls rather than on a timer, so an idle process spends nothing. + private const int SweepEveryNCalls = 4096; + private static readonly TimeSpan StaleAfter = TimeSpan.FromMinutes(5); + + private readonly ConcurrentDictionary _buckets = new(); + private readonly double _ratePerSecond; + private readonly double _capacity; + private long _callCounter; + + public TokenBucketRateLimiter(IOptions options) + { + RateLimitConfig config = options.Value; + _ratePerSecond = Math.Max(1, config.MaxPacketsPerSecond); + _capacity = Math.Max(_ratePerSecond, config.BurstSize); + } + + public bool TryConsume(SessionKey session) + { + Bucket bucket = _buckets.GetOrAdd(session, _ => new Bucket(_capacity)); + + bool allowed; + + lock (bucket) + { + long now = Stopwatch.GetTimestamp(); + double elapsedSeconds = Stopwatch.GetElapsedTime(bucket.LastRefillTimestamp, now) + .TotalSeconds; + + if (elapsedSeconds > 0) + { + bucket.Tokens = Math.Min(_capacity, bucket.Tokens + (elapsedSeconds * _ratePerSecond)); + bucket.LastRefillTimestamp = now; + } + + bucket.LastTouchedUtc = DateTime.UtcNow; + + if (bucket.Tokens >= 1) + { + bucket.Tokens -= 1; + allowed = true; + } + else + { + allowed = false; + } + } + + if (Interlocked.Increment(ref _callCounter) % SweepEveryNCalls == 0) + { + SweepStaleBuckets(); + } + + return allowed; + } + + private void SweepStaleBuckets() + { + DateTime cutoff = DateTime.UtcNow - StaleAfter; + + foreach ((SessionKey key, Bucket bucket) in _buckets) + { + bool stale; + + lock (bucket) + { + stale = bucket.LastTouchedUtc < cutoff; + } + + if (stale) + { + _buckets.TryRemove(key, out _); + } + } + } + + private sealed class Bucket(double initialTokens) + { + public double Tokens = initialTokens; + public long LastRefillTimestamp = Stopwatch.GetTimestamp(); + public DateTime LastTouchedUtc = DateTime.UtcNow; + } +} diff --git a/Vortex.Rooms/Configuration/RoomConfig.cs b/Vortex.Rooms/Configuration/RoomConfig.cs index 3e7cea3e..92fc6890 100644 --- a/Vortex.Rooms/Configuration/RoomConfig.cs +++ b/Vortex.Rooms/Configuration/RoomConfig.cs @@ -61,4 +61,10 @@ public class RoomConfig /// Cap on the inscription a mystery trophy can carry, so an oversized string cannot be /// pushed into the trophy's stuff data (which every viewer of the room then receives). public int MysteryTrophyInscriptionMaxLength { get; init; } = 100; + + /// Minimum seconds between chat lines from the same player under each room-configured + /// ChatFloodSensitivityType, keyed Extra/Normal/Minimal (SEC-03). A line arriving before + /// its slot sends FloodControlMessageComposer with the remaining wait instead of being + /// spoken. These are starting defaults, not a tuned game-balance decision - adjust freely. + public int[] ChatFloodIntervalSeconds { get; init; } = [4, 2, 1]; } diff --git a/Vortex.Rooms/Grains/Systems/RoomChatSystem.cs b/Vortex.Rooms/Grains/Systems/RoomChatSystem.cs index b5e2beed..f854175a 100644 --- a/Vortex.Rooms/Grains/Systems/RoomChatSystem.cs +++ b/Vortex.Rooms/Grains/Systems/RoomChatSystem.cs @@ -7,6 +7,7 @@ using Vortex.Database.Entities.Room; using Vortex.Primitives.Action; using Vortex.Primitives.Messages.Outgoing.Room.Chat; +using Vortex.Primitives.Navigator.Enums; using Vortex.Primitives.Orleans; using Vortex.Primitives.Pets.Snapshots; using Vortex.Primitives.Players; @@ -22,6 +23,11 @@ public sealed class RoomChatSystem(RoomGrain roomGrain) private readonly RoomGrain _roomGrain = roomGrain; private static readonly int MaxChatMessageLength = 100; + // Per-player last-accepted-line timestamp, for the room's configured flood sensitivity + // (SEC-03). Lives only for this room activation; bounded by how many distinct players have + // spoken in the room since it last activated. + private readonly Dictionary _lastChatAtMs = new(); + public async Task SendChatFromPlayerAsync( PlayerId playerId, string text, @@ -60,6 +66,16 @@ await _roomGrain return; } + if (IsFloodGated(playerId, out int floodSecondsRemaining)) + { + await _roomGrain + ._grainFactory.GetPlayerPresenceGrain(playerId) + .SendComposerAsync(new FloodControlMessageComposer { Seconds = floodSecondsRemaining }) + .ConfigureAwait(false); + + return; + } + if (styleId == -1) { styleId = avatar.LastChatStyleId; @@ -276,4 +292,33 @@ private bool IsUserMuted(PlayerId playerId, out int secondsRemaining) secondsRemaining = 0; return false; } + + private bool IsFloodGated(PlayerId playerId, out int secondsRemaining) + { + int[] intervals = _roomGrain._roomConfig.ChatFloodIntervalSeconds; + int sensitivityIndex = (int)_roomGrain._state.RoomSnapshot.ChatSettings.FloodSensitivity; + int intervalSeconds = + intervals.Length == 0 ? 0 + : sensitivityIndex >= 0 && sensitivityIndex < intervals.Length + ? intervals[sensitivityIndex] + : intervals[^1]; + + long nowMs = Environment.TickCount64; + + if (intervalSeconds > 0 && _lastChatAtMs.TryGetValue(playerId, out long lastMs)) + { + long requiredMs = intervalSeconds * 1000L; + long elapsedMs = nowMs - lastMs; + + if (elapsedMs < requiredMs) + { + secondsRemaining = (int)Math.Ceiling((requiredMs - elapsedMs) / 1000.0); + return true; + } + } + + _lastChatAtMs[playerId] = nowMs; + secondsRemaining = 0; + return false; + } } From 02049091ee73907b7aae56b83e993d6bedb3c676 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 01:44:57 +0000 Subject: [PATCH 07/11] feat: make Orleans clustering and grain storage configurable (ORL-02) Previously UseLocalhostClustering()/AddMemoryGrainStorage() were the only code path that existed: the startup guard correctly refused to run unclustered outside Development, but pointed operators at a "configure a persistent provider" instruction they could only satisfy by editing source and recompiling. - OrleansHostConfig gains ClusteringProvider ("localhost" default | "adonet"), GrainStorageProvider ("memory" default | "adonet" - only PubSubStore, per ORL-01), and Invariant (defaults to "MySqlConnector", the driver this project already ships via Pomelo.EntityFrameworkCore.MySql). Defaults are unchanged, so nothing about today's behavior moves until an operator opts in. - "adonet" mode reuses Vortex:Database:ConnectionString rather than a second connection-string surface, and registers MySqlConnector's DbProviderFactory under the configured invariant since Orleans's ADO.NET providers resolve it by name. The startup guard now only fires when clustering is unclustered *and* storage is in-memory, since either alone no longer means "cannot survive a restart". - Added Microsoft.Orleans.Clustering.AdoNet and Microsoft.Orleans.Persistence.AdoNet (10.2.1, matching the existing Orleans package set) plus an explicit MySqlConnector pin. - Documented the one real prerequisite this code can't satisfy itself: Orleans's official clustering/persistence SQL scripts (https://aka.ms/orleans-sql-scripts) still need to be applied to the database before "adonet" mode is used, the same deployment-time step EF migrations already are. - Extended OrleansHostConfigValidator to check the provider names and that a connection string exists when adonet is selected; updated existing tests for its new constructor dependency and added 3 more covering the new validation branches. All 627 tests pass; build stays clean at 0 warnings. --- Directory.Packages.props | 7 +++ Vortex.Hosting.Tests/ConfigValidationTests.cs | 33 ++++++++-- .../Configuration/OrleansHostConfig.cs | 23 +++++++ .../OrleansHostConfigValidator.cs | 53 +++++++++++++++- .../HostApplicationBuilderExtensions.cs | 63 +++++++++++++++++-- Vortex.Main/Vortex.Main.csproj | 3 + 6 files changed, 170 insertions(+), 12 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index beeea0c1..b547ec6b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,6 +33,13 @@ + + + + + diff --git a/Vortex.Hosting.Tests/ConfigValidationTests.cs b/Vortex.Hosting.Tests/ConfigValidationTests.cs index b847902c..ec279758 100644 --- a/Vortex.Hosting.Tests/ConfigValidationTests.cs +++ b/Vortex.Hosting.Tests/ConfigValidationTests.cs @@ -411,27 +411,48 @@ public void ATooShortDashboardSession_IsRefused() // ── Orleans ────────────────────────────────────────────────────────────── + private static OrleansHostConfigValidator OrleansValidator(string connectionString = "server=x") => + new(Options.Create(new DatabaseConfig { ConnectionString = connectionString })); + [Fact] public void TheDefaultOrleansEndpoint_Passes() => - new OrleansHostConfigValidator() - .Validate(null, new OrleansHostConfig()) - .Succeeded.Should() - .BeTrue(); + OrleansValidator().Validate(null, new OrleansHostConfig()).Succeeded.Should().BeTrue(); [Fact] public void ANonLiteralAdvertisedIp_IsRefused() => - new OrleansHostConfigValidator() + OrleansValidator() .Validate(null, new OrleansHostConfig { AdvertisedIp = "silo.internal" }) .Failed.Should() .BeTrue(); [Fact] public void ACollidingSiloAndGatewayPort_IsRefused() => - new OrleansHostConfigValidator() + OrleansValidator() .Validate(null, new OrleansHostConfig { SiloPort = 11111, GatewayPort = 11111 }) .Failed.Should() .BeTrue(); + [Fact] + public void AnUnknownClusteringProvider_IsRefused() => + OrleansValidator() + .Validate(null, new OrleansHostConfig { ClusteringProvider = "zookeeper" }) + .Failed.Should() + .BeTrue(); + + [Fact] + public void AdoNetClusteringWithoutAConnectionString_IsRefused() => + OrleansValidator(connectionString: string.Empty) + .Validate(null, new OrleansHostConfig { ClusteringProvider = "adonet" }) + .Failed.Should() + .BeTrue(); + + [Fact] + public void AdoNetClusteringWithAConnectionString_Passes() => + OrleansValidator() + .Validate(null, new OrleansHostConfig { ClusteringProvider = "adonet" }) + .Succeeded.Should() + .BeTrue(); + // ── Plugins ────────────────────────────────────────────────────────────── [Fact] diff --git a/Vortex.Main/Configuration/OrleansHostConfig.cs b/Vortex.Main/Configuration/OrleansHostConfig.cs index efeb5c76..77397132 100644 --- a/Vortex.Main/Configuration/OrleansHostConfig.cs +++ b/Vortex.Main/Configuration/OrleansHostConfig.cs @@ -20,4 +20,27 @@ public sealed class OrleansHostConfig /// deployment, and configure a persistent clustering/storage provider otherwise. /// public bool AllowUnclusteredOutsideDevelopment { get; init; } + + /// + /// "localhost" (default, unchanged) or "adonet" for multi-silo clustering backed by the same + /// MySQL database as Vortex:Database:ConnectionString. Selecting "adonet" requires + /// Orleans's official clustering SQL scripts (https://aka.ms/orleans-sql-scripts) to already + /// be applied to that database — this is a deployment prerequisite this process cannot apply + /// for you, the same way EF migrations must already be applied before startup. + /// + public string ClusteringProvider { get; init; } = "localhost"; + + /// + /// "memory" (default, unchanged) or "adonet" to persist PubSubStore (the only grain storage + /// actually used — see ORL-01) so in-flight stream messages survive a silo restart. Same SQL + /// script prerequisite as . + /// + public string GrainStorageProvider { get; init; } = "memory"; + + /// + /// ADO.NET provider invariant name for "adonet" mode. Defaults to the driver this project + /// already ships (MySqlConnector, via Pomelo.EntityFrameworkCore.MySql); override only if + /// pointing Orleans at a different ADO.NET provider/database engine. + /// + public string Invariant { get; init; } = "MySqlConnector"; } diff --git a/Vortex.Main/Configuration/OrleansHostConfigValidator.cs b/Vortex.Main/Configuration/OrleansHostConfigValidator.cs index 5bdf8889..998f7577 100644 --- a/Vortex.Main/Configuration/OrleansHostConfigValidator.cs +++ b/Vortex.Main/Configuration/OrleansHostConfigValidator.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Net; using Microsoft.Extensions.Options; +using Vortex.Database.Configuration; namespace Vortex.Main.Configuration; @@ -9,12 +11,61 @@ namespace Vortex.Main.Configuration; /// collision here surfaces as an opaque clustering failure well after the host has started /// announcing itself. /// -public sealed class OrleansHostConfigValidator : IValidateOptions +public sealed class OrleansHostConfigValidator(IOptions databaseConfig) + : IValidateOptions { + private static readonly string[] KnownClusteringProviders = ["localhost", "adonet"]; + private static readonly string[] KnownGrainStorageProviders = ["memory", "adonet"]; + public ValidateOptionsResult Validate(string? name, OrleansHostConfig options) { List failures = []; + if ( + Array.IndexOf(KnownClusteringProviders, options.ClusteringProvider) < 0 + ) + { + failures.Add( + $"'{OrleansHostConfig.SECTION_NAME}:{nameof(OrleansHostConfig.ClusteringProvider)}' " + + $"must be one of: {string.Join(", ", KnownClusteringProviders)} " + + $"(got '{options.ClusteringProvider}')." + ); + } + + if ( + Array.IndexOf(KnownGrainStorageProviders, options.GrainStorageProvider) < 0 + ) + { + failures.Add( + $"'{OrleansHostConfig.SECTION_NAME}:{nameof(OrleansHostConfig.GrainStorageProvider)}' " + + $"must be one of: {string.Join(", ", KnownGrainStorageProviders)} " + + $"(got '{options.GrainStorageProvider}')." + ); + } + + if ( + (options.ClusteringProvider == "adonet" || options.GrainStorageProvider == "adonet") + && string.IsNullOrWhiteSpace(options.Invariant) + ) + { + failures.Add( + $"'{OrleansHostConfig.SECTION_NAME}:{nameof(OrleansHostConfig.Invariant)}' must be " + + "set when clustering or grain storage uses the 'adonet' provider." + ); + } + + if ( + (options.ClusteringProvider == "adonet" || options.GrainStorageProvider == "adonet") + && string.IsNullOrWhiteSpace(databaseConfig.Value.ConnectionString) + ) + { + failures.Add( + $"'{OrleansHostConfig.SECTION_NAME}' selects the 'adonet' provider, but " + + $"'{DatabaseConfig.SECTION_NAME}:{nameof(DatabaseConfig.ConnectionString)}' is " + + "not set. Orleans reuses that connection string rather than a separate one." + ); + } + if (!IPAddress.TryParse(options.AdvertisedIp, out _)) { failures.Add( diff --git a/Vortex.Main/Extensions/HostApplicationBuilderExtensions.cs b/Vortex.Main/Extensions/HostApplicationBuilderExtensions.cs index 4f1e64a5..9e2abc94 100644 --- a/Vortex.Main/Extensions/HostApplicationBuilderExtensions.cs +++ b/Vortex.Main/Extensions/HostApplicationBuilderExtensions.cs @@ -1,10 +1,13 @@ using System; +using System.Data.Common; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using MySqlConnector; using Orleans.Configuration; using Orleans.Hosting; +using Vortex.Database.Configuration; using Vortex.Main.Configuration; using Vortex.Primitives.Orleans; @@ -33,7 +36,14 @@ public static HostApplicationBuilder AddOrleans(this HostApplicationBuilder buil .Get() ?? new OrleansHostConfig(); - if (!builder.Environment.IsDevelopment()) + bool usesAdoNet = + hostConfig.ClusteringProvider == "adonet" || hostConfig.GrainStorageProvider == "adonet"; + + bool unclustered = + hostConfig.ClusteringProvider == "localhost" + && hostConfig.GrainStorageProvider == "memory"; + + if (!builder.Environment.IsDevelopment() && unclustered) { if (!hostConfig.AllowUnclusteredOutsideDevelopment) { @@ -41,7 +51,9 @@ public static HostApplicationBuilder AddOrleans(this HostApplicationBuilder buil "Refusing to start: Orleans would run with single-node localhost clustering and " + "in-memory grain storage/streams outside Development. State does not survive " + "a restart and this cannot scale beyond one silo. Configure a persistent " - + "clustering/storage provider, or set " + + $"clustering/storage provider via '{OrleansHostConfig.SECTION_NAME}:" + + $"{nameof(OrleansHostConfig.ClusteringProvider)}'/" + + $"'{nameof(OrleansHostConfig.GrainStorageProvider)}' (= \"adonet\"), or set " + $"'{OrleansHostConfig.SECTION_NAME}:{nameof(OrleansHostConfig.AllowUnclusteredOutsideDevelopment)}' " + "to true to explicitly accept this for a single-node deployment." ); @@ -55,6 +67,20 @@ public static HostApplicationBuilder AddOrleans(this HostApplicationBuilder buil ); } + if (usesAdoNet) + { + // MySqlConnector isn't registered as a DbProviderFactory by default (unlike the EF Core + // path, which references it directly) - Orleans's ADO.NET providers resolve their driver + // by invariant name through DbProviderFactories. + DbProviderFactories.RegisterFactory(hostConfig.Invariant, MySqlConnectorFactory.Instance); + } + + string databaseConnectionString = + builder + .Configuration.GetSection(DatabaseConfig.SECTION_NAME) + .Get() + ?.ConnectionString ?? string.Empty; + builder.UseOrleans( (System.Action)( silo => @@ -70,12 +96,39 @@ public static HostApplicationBuilder AddOrleans(this HostApplicationBuilder buil listenOnAnyHostAddress: true ); + if (hostConfig.ClusteringProvider == "adonet") + { + silo.UseAdoNetClustering(options => + { + options.Invariant = hostConfig.Invariant; + options.ConnectionString = databaseConnectionString; + }); + } + else + { + silo.UseLocalhostClustering(); + } + // PLAYER_STORE/ROOM_STORE were never wired to a [PersistentState] grain — every // grain in this codebase persists through EF Core instead, so only PubSubStore // (required by the stream providers below) is actually used. - silo.UseLocalhostClustering() - .AddMemoryGrainStorage(OrleansStorageNames.PUB_SUB_STORE) - .AddMemoryStreams(OrleansStreamProviders.DEFAULT_STREAM_PROVIDER) + if (hostConfig.GrainStorageProvider == "adonet") + { + silo.AddAdoNetGrainStorage( + OrleansStorageNames.PUB_SUB_STORE, + options => + { + options.Invariant = hostConfig.Invariant; + options.ConnectionString = databaseConnectionString; + } + ); + } + else + { + silo.AddMemoryGrainStorage(OrleansStorageNames.PUB_SUB_STORE); + } + + silo.AddMemoryStreams(OrleansStreamProviders.DEFAULT_STREAM_PROVIDER) .AddMemoryStreams(OrleansStreamProviders.ROOM_STREAM_PROVIDER); } ) diff --git a/Vortex.Main/Vortex.Main.csproj b/Vortex.Main/Vortex.Main.csproj index e5bab0e2..2c967fdf 100644 --- a/Vortex.Main/Vortex.Main.csproj +++ b/Vortex.Main/Vortex.Main.csproj @@ -15,6 +15,9 @@ + + + From b72c3510da1f001071fefc589efb35596ae07fee Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 01:52:24 +0000 Subject: [PATCH 08/11] feat: add /health endpoint reflecting DB connectivity and degraded state (OPS-02) RequiredServiceGuard already computed IsDegraded/DegradedServices for the console/logs, but nothing exposed it over HTTP, and there was no liveness/readiness probe for a container orchestrator to poll. - New GET /health on Vortex.WebApi: pings the database via IDbContextFactory.Database.CanConnectAsync and reports RequiredServiceGuard's degraded state, returning Healthy/Degraded (200) or Unhealthy (503) with the specific list of degraded services in the body. - Forwarded RequiredServiceGuard and IDbContextFactory into the WebApi's isolated DI container (WebApiWebHost already held a reference to the guard for startup reporting, just hadn't shared it with the endpoints). - Extended WebApiTestFactory with an in-memory DbContextFactory and a real RequiredServiceGuard so the new endpoint is exercised by an actual integration test rather than left uncovered. 628 tests pass (627 + 1 new); build stays clean at 0 warnings. --- .../Vortex.WebApi.Tests.csproj | 3 ++ Vortex.WebApi.Tests/WebApiEndpointsTests.cs | 14 +++++ Vortex.WebApi.Tests/WebApiTestFactory.cs | 17 ++++++ Vortex.WebApi/Hosting/WebApiEndpoints.cs | 53 +++++++++++++++++++ Vortex.WebApi/Hosting/WebApiWebHost.cs | 4 ++ 5 files changed, 91 insertions(+) diff --git a/Vortex.WebApi.Tests/Vortex.WebApi.Tests.csproj b/Vortex.WebApi.Tests/Vortex.WebApi.Tests.csproj index 172124f8..a2c83825 100644 --- a/Vortex.WebApi.Tests/Vortex.WebApi.Tests.csproj +++ b/Vortex.WebApi.Tests/Vortex.WebApi.Tests.csproj @@ -9,12 +9,15 @@ + + + diff --git a/Vortex.WebApi.Tests/WebApiEndpointsTests.cs b/Vortex.WebApi.Tests/WebApiEndpointsTests.cs index 95d36120..e106c192 100644 --- a/Vortex.WebApi.Tests/WebApiEndpointsTests.cs +++ b/Vortex.WebApi.Tests/WebApiEndpointsTests.cs @@ -28,6 +28,20 @@ public async Task Hello_ReturnsOkStatus() (await ReadJsonAsync(response)).GetProperty("status").GetString().Should().Be("ok"); } + [Fact] + public async Task Health_WithDatabaseReachableAndNotDegraded_ReturnsHealthy() + { + await using WebApiTestFactory factory = new WebApiTestFactory(); + + HttpResponseMessage response = await factory.Client.GetAsync("/health"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + JsonElement body = await ReadJsonAsync(response); + body.GetProperty("status").GetString().Should().Be("Healthy"); + body.GetProperty("database").GetString().Should().Be("up"); + } + [Fact] public async Task Login_WithValidCredentials_Returns200AndIssuesCookie() { diff --git a/Vortex.WebApi.Tests/WebApiTestFactory.cs b/Vortex.WebApi.Tests/WebApiTestFactory.cs index c434e78e..4c4d210f 100644 --- a/Vortex.WebApi.Tests/WebApiTestFactory.cs +++ b/Vortex.WebApi.Tests/WebApiTestFactory.cs @@ -4,10 +4,13 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Vortex.Database.Context; +using Vortex.Primitives.Hosting; using Vortex.WebApi.Configuration; using Vortex.WebApi.Hosting; using Vortex.WebApi.Services; @@ -51,6 +54,14 @@ public WebApiTestFactory() builder.Services.AddSingleton(new FakeAuthService(Sessions)); builder.Services.AddSingleton(new FakePlayerService()); builder.Services.AddSingleton>(Options.Create(config)); + builder.Services.AddSingleton(); + builder.Services.AddSingleton>( + new TestDbContextFactory( + new DbContextOptionsBuilder() + .UseInMemoryDatabase($"webapi-health-{Guid.NewGuid():N}") + .Options + ) + ); WebApiAppConfigurator.ConfigureServices(builder.Services, config); @@ -86,4 +97,10 @@ public async ValueTask DisposeAsync() Client.Dispose(); await _app.DisposeAsync().ConfigureAwait(false); } + + private sealed class TestDbContextFactory(DbContextOptions options) + : IDbContextFactory + { + public VortexDbContext CreateDbContext() => new(options); + } } diff --git a/Vortex.WebApi/Hosting/WebApiEndpoints.cs b/Vortex.WebApi/Hosting/WebApiEndpoints.cs index 5756e654..935149c0 100644 --- a/Vortex.WebApi/Hosting/WebApiEndpoints.cs +++ b/Vortex.WebApi/Hosting/WebApiEndpoints.cs @@ -3,6 +3,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Vortex.Database.Context; +using Vortex.Primitives.Hosting; using Vortex.WebApi.Http; using Vortex.WebApi.Services; using Vortex.WebApi.Session; @@ -41,6 +44,56 @@ private static void MapPublic(WebApplication app) .WithName("Hello") .WithSummary("Server liveness probe used by the onboarding client.") .WithTags(TagPublic); + + app.MapGet( + "/health", + async ( + RequiredServiceGuard guard, + IDbContextFactory dbCtxFactory, + CancellationToken ct + ) => + { + bool databaseUp; + + try + { + await using VortexDbContext dbCtx = await dbCtxFactory + .CreateDbContextAsync(ct) + .ConfigureAwait(false); + + databaseUp = await dbCtx.Database.CanConnectAsync(ct).ConfigureAwait(false); + } + catch + { + // A probe failure is a health signal, not an error to propagate - report it + // as part of the response body instead of a 500. + databaseUp = false; + } + + string status = + !databaseUp ? "Unhealthy" + : guard.IsDegraded ? "Degraded" + : "Healthy"; + + return Results.Json( + new + { + status, + database = databaseUp ? "up" : "down", + degradedServices = guard.DegradedServices, + }, + statusCode: status == "Unhealthy" + ? StatusCodes.Status503ServiceUnavailable + : StatusCodes.Status200OK + ); + } + ) + .WithName("Health") + .WithSummary( + "Liveness/readiness probe: database connectivity and RequiredServiceGuard's " + + "degraded-service state (OPS-02)." + ) + .WithTags(TagPublic); } private static void MapAuthentication(WebApplication app) diff --git a/Vortex.WebApi/Hosting/WebApiWebHost.cs b/Vortex.WebApi/Hosting/WebApiWebHost.cs index e8dfd778..b19a7e8f 100644 --- a/Vortex.WebApi/Hosting/WebApiWebHost.cs +++ b/Vortex.WebApi/Hosting/WebApiWebHost.cs @@ -6,10 +6,12 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Vortex.Database.Context; using Vortex.Primitives.Hosting; using Vortex.WebApi.Configuration; using Vortex.WebApi.Services; @@ -252,6 +254,8 @@ private void ForwardSingletons(IServiceCollection services) services.AddSingleton(rootServices.GetRequiredService()); services.AddSingleton(rootServices.GetRequiredService()); services.AddSingleton(rootServices.GetRequiredService()); + services.AddSingleton(rootServices.GetRequiredService()); + services.AddSingleton(rootServices.GetRequiredService>()); services.AddSingleton(options); } } From 07619e2bae33e482768c4e9993f9a69bf18ec94a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 02:00:17 +0000 Subject: [PATCH 09/11] refactor: extract moderation and mystery-box logic out of RoomGrain (ARCH-02, partial) The audit's ARCH-02 R5 recommendation was to pull Trading/Moderation/ MysteryBox out into three separate Orleans grains, characterizing them as low-coupling ("the trade has its own lifecycle", "isolated reservation state"). Investigating the actual code before touching it found the opposite: kick/ban mutate live avatar state (AvatarModule.RemoveAvatarFromPlayerAsync), mute status is read in-process on every chat message by RoomChatSystem, and mystery-box sessions are torn down synchronously from the room tick, from an avatar leaving, and from item removal - all of which currently get single-grain-turn atomicity for free. Moving these to separate grains would turn each of those into a cross-grain call with no transaction to back it, in code that grants prizes and enforces moderation. Given that risk, and no way to validate cross-grain consistency behavior without a live multi-silo cluster, this applies the safer half of the same idea instead: Moderation and MysteryBox move out of RoomGrain's own partial-class files into RoomModerationSystem and RoomMysteryBoxSystem, owned classes constructed the same way this codebase already does for RoomChatSystem/RoomWiredSystem. RoomGrain keeps every existing public/internal method as a one-line delegate, so every external call site (PacketHandlers, RoomObjectModule, the wired kick action, the tick loop, avatar-leave handling) is unchanged. This is a pure move - no method's logic changed - and it's the same pattern already proven safe by Chat/Wired, not a new design. This reduces RoomGrain's own file size/complexity and gives these two areas an isolated home to grow in, without gambling on unverified cross-grain consistency for prize-granting and moderation-enforcement code. Trading is the next candidate (largest of the three); full separate-grain extraction remains a legitimate follow-up if someone invests in a state-sync design and integration tests against a real cluster first. All 251 Vortex.Rooms.Tests pass; full solution build stays clean at 0 warnings. --- Vortex.Rooms/Grains/RoomGrain.Moderation.cs | 322 +-------- Vortex.Rooms/Grains/RoomGrain.MysteryBox.cs | 581 +---------------- Vortex.Rooms/Grains/RoomGrain.cs | 4 + .../Grains/Systems/RoomModerationSystem.cs | 390 +++++++++++ .../Grains/Systems/RoomMysteryBoxSystem.cs | 609 ++++++++++++++++++ 5 files changed, 1025 insertions(+), 881 deletions(-) create mode 100644 Vortex.Rooms/Grains/Systems/RoomModerationSystem.cs create mode 100644 Vortex.Rooms/Grains/Systems/RoomMysteryBoxSystem.cs diff --git a/Vortex.Rooms/Grains/RoomGrain.Moderation.cs b/Vortex.Rooms/Grains/RoomGrain.Moderation.cs index afd65458..75f2bc77 100644 --- a/Vortex.Rooms/Grains/RoomGrain.Moderation.cs +++ b/Vortex.Rooms/Grains/RoomGrain.Moderation.cs @@ -1,13 +1,7 @@ -using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Vortex.Primitives.Action; -using Vortex.Primitives.Events; -using Vortex.Primitives.Navigator.Enums; -using Vortex.Primitives.Permissions; using Vortex.Primitives.Players; -using Vortex.Primitives.Rooms.Enums; namespace Vortex.Rooms.Grains; @@ -17,333 +11,37 @@ public Task KickUserAsync( ActionContext actorCtx, PlayerId targetPlayerId, CancellationToken ct - ) - { - if ( - actorCtx.PlayerId <= 0 - || actorCtx.RoomId != _state.RoomId - || actorCtx.PlayerId == targetPlayerId - ) - { - return Task.FromResult(false); - } - - return KickUserGuardedAsync(actorCtx, targetPlayerId, ct); - } - - private async Task KickUserGuardedAsync( - ActionContext actorCtx, - PlayerId targetPlayerId, - CancellationToken ct - ) - { - if ( - !await CanModerateAsync( - actorCtx, - _state.RoomSnapshot.ModSettings.WhoCanKick, - ModerationAction.Kick - ) - ) - { - return false; - } - - return await KickUserInternalAsync(actorCtx, targetPlayerId, ct).ConfigureAwait(true); - } - - /// - /// Whether may perform a moderation action gated by - /// in this room. System/wired origins resolve to moderator and pass. - /// - private async Task CanModerateAsync( - ActionContext actorCtx, - ModSettingType setting, - ModerationAction action - ) - { - RoomControllerType level = await SecurityModule - .GetControllerLevelAsync(actorCtx) - .ConfigureAwait(true); - - bool hasStaffCapability = await SecurityModule - .HasStaffModerationCapabilityAsync(actorCtx, action) - .ConfigureAwait(true); - - if (RoomModerationPolicy.CanModerate(level, setting, hasStaffCapability)) - { - return true; - } - - _logger.LogDebug( - "Player {ActorId} lacks {Setting} moderation rights in room {RoomId} (level {Level}).", - actorCtx.PlayerId, - setting, - _state.RoomId, - level - ); - - return false; - } + ) => ModerationSystem.KickUserAsync(actorCtx, targetPlayerId, ct); /// Kicks a user without a human actor — for wired / system-driven kicks (the /// wf_act_kick_user action). Called directly on the grain from inside its own turn, so it is /// not a re-entrant grain-reference call. public Task KickUserFromWiredAsync(PlayerId targetPlayerId, CancellationToken ct) => - KickUserInternalAsync(ActionContext.CreateForWired(_state.RoomId), targetPlayerId, ct); - - private async Task KickUserInternalAsync( - ActionContext ctx, - PlayerId targetPlayerId, - CancellationToken ct - ) - { - try - { - if (targetPlayerId <= 0 || !_state.AvatarsByPlayerId.ContainsKey(targetPlayerId)) - { - return false; - } - - await AvatarModule - .RemoveAvatarFromPlayerAsync(ctx, targetPlayerId, ct) - .ConfigureAwait(true); + ModerationSystem.KickUserFromWiredAsync(targetPlayerId, ct); - await _events - .PublishAsync( - new PlayerKickedFromRoomEvent( - ctx.PlayerId, - targetPlayerId, - _state.RoomId.Value - ), - ct - ) - .ConfigureAwait(true); - - return true; - } - catch (Exception ex) - { - _logger.LogWarning( - ex, - "Failed to kick player {TargetPlayerId} from room {RoomId}.", - targetPlayerId, - _state.RoomId - ); - - return false; - } - } - - public async Task MuteUserAsync( + public Task MuteUserAsync( ActionContext actorCtx, PlayerId targetPlayerId, int durationSeconds, CancellationToken ct - ) - { - if (actorCtx.PlayerId <= 0 || targetPlayerId <= 0 || actorCtx.RoomId != _state.RoomId) - { - return false; - } - - if (durationSeconds <= 0 || actorCtx.PlayerId == targetPlayerId) - { - return false; - } - - if ( - !await CanModerateAsync( - actorCtx, - _state.RoomSnapshot.ModSettings.WhoCanMute, - ModerationAction.Mute - ) - ) - { - return false; - } + ) => ModerationSystem.MuteUserAsync(actorCtx, targetPlayerId, durationSeconds, ct); - DateTime expiresUtc = DateTime.UtcNow.AddSeconds(durationSeconds); - - try - { - await _moderationStore.MuteAsync(_state.RoomId.Value, targetPlayerId, expiresUtc, ct); - _state.MuteExpiresUtc[targetPlayerId] = expiresUtc; - - await _events - .PublishAsync( - new PlayerMutedInRoomEvent( - actorCtx.PlayerId, - targetPlayerId, - _state.RoomId.Value, - durationSeconds - ), - ct - ) - .ConfigureAwait(true); - - return true; - } - catch (Exception ex) - { - _logger.LogWarning( - ex, - "Failed to mute player {TargetPlayerId} in room {RoomId}.", - targetPlayerId, - _state.RoomId - ); - } - - return false; - } - - public async Task BanUserAsync( + public Task BanUserAsync( ActionContext actorCtx, PlayerId targetPlayerId, int durationSeconds, CancellationToken ct - ) - { - try - { - if (actorCtx.PlayerId <= 0 || targetPlayerId <= 0 || actorCtx.RoomId != _state.RoomId) - { - return false; - } - - if (durationSeconds <= 0 || actorCtx.PlayerId == targetPlayerId) - { - return false; - } - - if ( - !await CanModerateAsync( - actorCtx, - _state.RoomSnapshot.ModSettings.WhoCanBan, - ModerationAction.Ban - ) - ) - { - return false; - } - - DateTime expiresUtc = DateTime.UtcNow.AddSeconds(durationSeconds); - - await _moderationStore.BanAsync(_state.RoomId.Value, targetPlayerId, expiresUtc, ct); - await AvatarModule - .RemoveAvatarFromPlayerAsync(actorCtx, targetPlayerId, ct) - .ConfigureAwait(true); - - await _events - .PublishAsync( - new PlayerBannedInRoomEvent( - actorCtx.PlayerId, - targetPlayerId, - _state.RoomId.Value, - durationSeconds - ), - ct - ) - .ConfigureAwait(true); + ) => ModerationSystem.BanUserAsync(actorCtx, targetPlayerId, durationSeconds, ct); - return true; - } - catch (Exception ex) - { - _logger.LogWarning( - ex, - "Failed to ban player {TargetPlayerId} in room {RoomId}.", - targetPlayerId, - _state.RoomId - ); - - return false; - } - } - - public async Task UnmuteUserAsync( + public Task UnmuteUserAsync( ActionContext actorCtx, PlayerId targetPlayerId, CancellationToken ct - ) - { - try - { - if (actorCtx.PlayerId <= 0 || targetPlayerId <= 0 || actorCtx.RoomId != _state.RoomId) - { - return false; - } - - if ( - !await CanModerateAsync( - actorCtx, - _state.RoomSnapshot.ModSettings.WhoCanMute, - ModerationAction.Mute - ) - ) - { - return false; - } - - await _moderationStore - .UnmuteAsync(_state.RoomId.Value, targetPlayerId, ct) - .ConfigureAwait(true); - _state.MuteExpiresUtc.Remove(targetPlayerId); - - return true; - } - catch (Exception ex) - { - _logger.LogWarning( - ex, - "Failed to unmute player {TargetPlayerId} in room {RoomId}.", - targetPlayerId, - _state.RoomId - ); + ) => ModerationSystem.UnmuteUserAsync(actorCtx, targetPlayerId, ct); - return false; - } - } - - public async Task UnbanUserAsync( + public Task UnbanUserAsync( ActionContext actorCtx, PlayerId targetPlayerId, CancellationToken ct - ) - { - try - { - if (actorCtx.PlayerId <= 0 || targetPlayerId <= 0 || actorCtx.RoomId != _state.RoomId) - { - return false; - } - - if ( - !await CanModerateAsync( - actorCtx, - _state.RoomSnapshot.ModSettings.WhoCanBan, - ModerationAction.Ban - ) - ) - { - return false; - } - - await _moderationStore - .UnbanAsync(_state.RoomId.Value, targetPlayerId, ct) - .ConfigureAwait(true); - - return true; - } - catch (Exception ex) - { - _logger.LogWarning( - ex, - "Failed to unban player {TargetPlayerId} in room {RoomId}.", - targetPlayerId, - _state.RoomId - ); - - return false; - } - } + ) => ModerationSystem.UnbanUserAsync(actorCtx, targetPlayerId, ct); } diff --git a/Vortex.Rooms/Grains/RoomGrain.MysteryBox.cs b/Vortex.Rooms/Grains/RoomGrain.MysteryBox.cs index 75b979db..39dc74b3 100644 --- a/Vortex.Rooms/Grains/RoomGrain.MysteryBox.cs +++ b/Vortex.Rooms/Grains/RoomGrain.MysteryBox.cs @@ -1,600 +1,43 @@ -using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Vortex.Database.Context; -using Vortex.Database.Entities.Furniture; using Vortex.Primitives.Action; -using Vortex.Primitives.Events; -using Vortex.Primitives.Messages.Outgoing.Mysterybox; -using Vortex.Primitives.MysteryBox; -using Vortex.Primitives.MysteryBox.Grains; -using Vortex.Primitives.MysteryBox.Snapshots; -using Vortex.Primitives.Orleans; using Vortex.Primitives.Players; using Vortex.Primitives.Rooms.Object; -using Vortex.Primitives.Rooms.Object.Furniture; namespace Vortex.Rooms.Grains; -/// -/// The two-player mystery box open flow. Both halves are held by different players, so the pairing -/// lives on the room grain -- the single-threaded coordinator both sides already share -- which is -/// what keeps two key holders from claiming the same box or one key from opening two boxes. -/// public sealed partial class RoomGrain { - public async Task UseMysteryBoxAsync( + public Task UseMysteryBoxAsync( ActionContext ctx, RoomObjectId boxObjectId, CancellationToken ct - ) - { - PlayerId actorId = ctx.PlayerId; + ) => MysteryBoxSystem.UseMysteryBoxAsync(ctx, boxObjectId, ct); - if (!_state.ItemsById.TryGetValue(boxObjectId, out IRoomItem? box)) - { - return; - } - - // A box wears its colour as its state, so the pairing colour comes from the item itself. - string boxColor = MysteryBoxSprite.ColorFromState(box.Logic.GetState()); - - if (boxColor.Length == 0) - { - // State 0: the box was created without a colour, so no key can ever match it. Say so - // rather than silently doing nothing on every click. - _logger.LogWarning( - "Mystery box {ObjectId} (definition {DefinitionId}) is in the colourless state 0 and can never be paired with a key.", - boxObjectId.Value, - box.Definition.Id - ); - - return; - } - - PlayerId ownerId = box.OwnerId; - RoomMysteryBoxSession session = ResolveSession(box, ownerId, boxColor); - - if (session.BoxObjectId != boxObjectId) - { - // The owner already has another box waiting. The cancel message only carries an owner - // id, so a second concurrent session would be un-cancellable. - _logger.LogDebug( - "Player {ActorId} clicked mystery box {BoxObjectId} while its owner already has box {ActiveBoxObjectId} waiting in room {RoomId}.", - actorId.Value, - boxObjectId.Value, - session.BoxObjectId.Value, - _state.RoomId.Value - ); - - return; - } - - if (actorId == ownerId) - { - session.OwnerWaiting = true; - } - else - { - if (session.KeyHolderId is not null && session.KeyHolderId != actorId) - { - // Someone else is already holding this box open; they get first refusal. - return; - } - - bool hasKey = await _grainFactory - .GetPlayerMysteryBoxGrain(actorId) - .HasKeyAsync(session.Color, ct) - .ConfigureAwait(true); - - if (!hasKey) - { - // Nobody joined, so don't leave an empty session behind for the sweep to clear. - // Ended rather than removed so every abandonment path goes through the one place - // that also puts the box back to its closed frame. - if (session.IsAbandoned) - { - await EndMysteryBoxSessionAsync(session, notifyParticipants: false) - .ConfigureAwait(true); - } - - return; - } - - session.KeyHolderId = actorId; - } - - session.StartedAtMs = NowMs(); - - if (!session.IsReadyToOpen) - { - // Second frame of the colour's group: the room can see the box is in play rather than - // wondering why nobody else can start it. - await SetMysteryBoxStateAsync(box, MysteryBoxSprite.WaitingState(session.Color)) - .ConfigureAwait(true); - - await SendMysteryBoxComposerAsync(actorId, new ShowMysteryBoxWaitMessageComposer()) - .ConfigureAwait(true); - - return; - } - - await OpenMysteryBoxAsync(ctx, session, ct).ConfigureAwait(true); - } - - public async Task CancelMysteryBoxWaitAsync( + public Task CancelMysteryBoxWaitAsync( ActionContext ctx, PlayerId boxOwnerId, CancellationToken ct - ) - { - PlayerId actorId = ctx.PlayerId; - - if ( - !_state.MysteryBoxSessionsByOwnerId.TryGetValue( - boxOwnerId, - out RoomMysteryBoxSession? session - ) - ) - { - return; - } - - if (!session.IsParticipant(actorId)) - { - return; - } - - if (actorId == session.BoxOwnerId) - { - // The owner walking away ends the whole thing; the key holder's dialog has to close too, - // otherwise they sit in front of a box that will never open. - await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); - - return; - } - - session.KeyHolderId = null; - - await SendMysteryBoxComposerAsync(actorId, new CancelMysteryBoxWaitMessageComposer()) - .ConfigureAwait(true); - - // Ending the session rather than dropping it from the dictionary: the box was repainted to - // its waiting frame when this key holder claimed it, and nothing else repaints it. Removing - // the entry on its own left the box showing "in play" forever with no session behind it. - if (session.IsAbandoned) - { - await EndMysteryBoxSessionAsync(session, notifyParticipants: false) - .ConfigureAwait(true); - } - } + ) => MysteryBoxSystem.CancelMysteryBoxWaitAsync(ctx, boxOwnerId, ct); - public async Task OpenMysteryTrophyAsync( + public Task OpenMysteryTrophyAsync( ActionContext ctx, RoomObjectId objectId, string inscription, CancellationToken ct - ) - { - if (!_state.ItemsById.TryGetValue(objectId, out IRoomItem? trophy)) - { - return; - } - - // Only the owner may inscribe: the trophy is consumed and the prize lands in their - // inventory, so letting a visitor trigger it would spend someone else's furniture. - if (trophy.OwnerId != ctx.PlayerId) - { - return; - } - - MysteryBoxPrizeSnapshot? prize = await _grainFactory - .GetMysteryBoxManagerGrain() - .PickPrizeAsync(MysteryBoxPrizePool.Trophy, string.Empty, ct) - .ConfigureAwait(true); - - if (prize is null) - { - return; - } - - string ownerName = trophy.OwnerName; - - if (string.IsNullOrEmpty(ownerName)) - { - ownerName = await _grainFactory - .GetPlayerDirectoryGrain() - .GetPlayerNameAsync(trophy.OwnerId, ct) - .ConfigureAwait(true); - } - - int maxLength = Math.Max(0, _roomConfig.MysteryTrophyInscriptionMaxLength); - string trimmed = inscription.Length > maxLength ? inscription[..maxLength] : inscription; - - string legacyData = MysteryTrophyStuffData.Build( - ownerName, - DateOnly.FromDateTime(DateTime.UtcNow), - trimmed - ); - - // Consume first: if the grant then fails the player is out a mystery trophy, but the reverse - // order would let a repeated click mint trophies from one furniture. - if (!await ConsumeMysteryFurnitureAsync(ctx, trophy, ct).ConfigureAwait(true)) - { - return; - } - - await _grainFactory - .GetInventoryGrain(trophy.OwnerId.Value) - .GrantFurnitureWithLegacyStuffDataAsync(prize.FurnitureDefinitionId, legacyData, ct) - .ConfigureAwait(true); - - await _events - .PublishAsync( - new MysteryTrophyOpenedEvent( - _state.RoomId.Value, - objectId.Value, - trophy.OwnerId.Value, - prize.Id, - prize.FurnitureDefinitionId - ), - ct - ) - .ConfigureAwait(true); - } + ) => MysteryBoxSystem.OpenMysteryTrophyAsync(ctx, objectId, inscription, ct); /// Tick-driven sweep. The client's wait dialog has no timer, so a player who wandered /// off would otherwise keep the box reserved forever. - internal async Task ProcessMysteryBoxTimeoutsAsync(long nowMs, CancellationToken ct) - { - if (_state.MysteryBoxSessionsByOwnerId.Count == 0) - { - return; - } - - List? expired = null; - - foreach (RoomMysteryBoxSession session in _state.MysteryBoxSessionsByOwnerId.Values) - { - if (nowMs - session.StartedAtMs < _roomConfig.MysteryBoxWaitTimeoutMs) - { - continue; - } - - (expired ??= []).Add(session); - } - - if (expired is null) - { - return; - } - - foreach (RoomMysteryBoxSession session in expired) - { - await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); - } - - await Task.CompletedTask.ConfigureAwait(true); - } + internal Task ProcessMysteryBoxTimeoutsAsync(long nowMs, CancellationToken ct) => + MysteryBoxSystem.ProcessMysteryBoxTimeoutsAsync(nowMs, ct); /// Drops any pending open a leaving player was part of, so the box is not left reserved /// by someone who is no longer in the room. - internal async Task CancelMysteryBoxSessionsForLeavingPlayerAsync(PlayerId playerId) - { - if (_state.MysteryBoxSessionsByOwnerId.Count == 0) - { - return; - } - - List? affected = null; - - foreach (RoomMysteryBoxSession session in _state.MysteryBoxSessionsByOwnerId.Values) - { - if (session.IsParticipant(playerId)) - { - (affected ??= []).Add(session); - } - } - - if (affected is null) - { - return; - } - - foreach (RoomMysteryBoxSession session in affected) - { - if (session.BoxOwnerId == playerId) - { - await EndMysteryBoxSessionAsync(session, notifyParticipants: true) - .ConfigureAwait(true); - - continue; - } - - session.KeyHolderId = null; - - // Same reason as the explicit cancel: the box carries the waiting frame this key holder - // put it in, so it has to be repainted rather than just forgotten. - if (session.IsAbandoned) - { - await EndMysteryBoxSessionAsync(session, notifyParticipants: false) - .ConfigureAwait(true); - } - } - } + internal Task CancelMysteryBoxSessionsForLeavingPlayerAsync(PlayerId playerId) => + MysteryBoxSystem.CancelMysteryBoxSessionsForLeavingPlayerAsync(playerId); /// Drops the pending open attached to a box that is being removed from the room. - internal async Task CancelMysteryBoxSessionForItemAsync(RoomObjectId boxObjectId) - { - RoomMysteryBoxSession? attached = null; - - foreach (RoomMysteryBoxSession session in _state.MysteryBoxSessionsByOwnerId.Values) - { - if (session.BoxObjectId == boxObjectId) - { - attached = session; - - break; - } - } - - if (attached is null) - { - return; - } - - await EndMysteryBoxSessionAsync(attached, notifyParticipants: true).ConfigureAwait(true); - } - - private RoomMysteryBoxSession ResolveSession(IRoomItem box, PlayerId ownerId, string color) - { - if ( - _state.MysteryBoxSessionsByOwnerId.TryGetValue( - ownerId, - out RoomMysteryBoxSession? existing - ) - ) - { - // A session whose box has since left the room is stale; replace it rather than blocking - // the owner's remaining boxes forever. - if (_state.ItemsById.ContainsKey(existing.BoxObjectId)) - { - return existing; - } - - _state.MysteryBoxSessionsByOwnerId.Remove(ownerId); - } - - RoomMysteryBoxSession session = new() - { - BoxObjectId = box.ObjectId, - BoxOwnerId = ownerId, - Color = color, - StartedAtMs = NowMs(), - }; - - _state.MysteryBoxSessionsByOwnerId[ownerId] = session; - - return session; - } - - private async Task OpenMysteryBoxAsync( - ActionContext ctx, - RoomMysteryBoxSession session, - CancellationToken ct - ) - { - PlayerId keyHolderId = session.KeyHolderId!.Value; - PlayerId ownerId = session.BoxOwnerId; - - if (!_state.ItemsById.TryGetValue(session.BoxObjectId, out IRoomItem? box)) - { - await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); - - return; - } - - IPlayerMysteryBoxGrain keyHolderGrain = _grainFactory.GetPlayerMysteryBoxGrain(keyHolderId); - - // Spend the key before anything is handed out. The holder may have opened another box - // between clicking and pairing, and a prize granted against a key that no longer exists is - // free furniture. - if (!await keyHolderGrain.TryConsumeKeyAsync(session.Color, ct).ConfigureAwait(true)) - { - await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); - - return; - } - - // Claim the session before the box leaves the room: removing the furniture runs the - // "a waiting box was picked up" cleanup, which would otherwise cancel this very open. - _state.MysteryBoxSessionsByOwnerId.Remove(ownerId); - - // The box goes next, for the same reason the key did: while it still exists a second pairing - // could claim it. If this fails the key is already gone, so refund it rather than eating it. - if (!await ConsumeMysteryFurnitureAsync(ctx, box, ct).ConfigureAwait(true)) - { - await keyHolderGrain - .GrantKeyAsync(session.Color, "refund:box-open-failed", ct) - .ConfigureAwait(true); - await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); - - return; - } - - await _events - .PublishAsync( - new MysteryBoxOpenedEvent( - _state.RoomId.Value, - session.BoxObjectId.Value, - ownerId.Value, - keyHolderId.Value, - session.Color - ), - ct - ) - .ConfigureAwait(true); - - // Two independent draws: the pair open the box together but do not split one prize. - await Task.WhenAll( - AwardMysteryBoxPrizeAsync(ownerId, session.Color, ct), - AwardMysteryBoxPrizeAsync(keyHolderId, session.Color, ct) - ) - .ConfigureAwait(true); - - // The owner just lost their box, so their tracker is stale. The key holder's was already - // refreshed by the consume. - await _grainFactory - .GetPlayerMysteryBoxGrain(ownerId) - .PushTrackerAsync(ct) - .ConfigureAwait(true); - } - - private async Task AwardMysteryBoxPrizeAsync( - PlayerId playerId, - string color, - CancellationToken ct - ) - { - MysteryBoxPrizeSnapshot? prize = await _grainFactory - .GetMysteryBoxManagerGrain() - .PickPrizeAsync(MysteryBoxPrizePool.Box, color, ct) - .ConfigureAwait(true); - - MysteryBoxPrizeAward? award = prize is null - ? null - : await _grainFactory - .GetPlayerMysteryBoxGrain(playerId) - .GrantPrizeAsync(prize, ct) - .ConfigureAwait(true); - - if (award is null) - { - // Nothing could be awarded (empty pool, broken prize row). Close the dialog instead of - // leaving the player staring at a spinner. - _logger.LogWarning( - "Mystery box opened in room {RoomId} but no prize could be awarded to player {PlayerId}.", - _state.RoomId.Value, - playerId.Value - ); - - await SendMysteryBoxComposerAsync(playerId, new CancelMysteryBoxWaitMessageComposer()) - .ConfigureAwait(true); - - return; - } - - await _events - .PublishAsync( - new MysteryBoxPrizeAwardedEvent( - playerId.Value, - prize!.Id, - color, - award.ContentType, - award.ClassId - ), - ct - ) - .ConfigureAwait(true); - - await SendMysteryBoxComposerAsync( - playerId, - new GotMysteryBoxPrizeMessageComposer - { - ContentType = award.ContentType, - ClassId = award.ClassId, - } - ) - .ConfigureAwait(true); - } - - /// Removes a mystery furniture from the room and the database. Returns false when the - /// delete did not happen, so the caller can avoid handing out a prize for it. - private async Task ConsumeMysteryFurnitureAsync( - ActionContext ctx, - IRoomItem item, - CancellationToken ct - ) - { - try - { - await using VortexDbContext dbCtx = await _dbCtxFactory - .CreateDbContextAsync(ct) - .ConfigureAwait(true); - - FurnitureEntity entity = new() { Id = item.ObjectId.Value }; - dbCtx.Attach(entity); - entity.DeletedAt = DateTime.UtcNow; - dbCtx.Entry(entity).Property(f => f.DeletedAt).IsModified = true; - - await dbCtx.SaveChangesAsync(ct).ConfigureAwait(true); - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Failed to delete mystery furniture {ObjectId} in room {RoomId}", - item.ObjectId.Value, - _state.RoomId.Value - ); - - return false; - } - - await ObjectModule.RemoveObjectAsync(ctx, item, ct, item.OwnerId).ConfigureAwait(true); - - return true; - } - - /// Repaints a box, tolerating the item having already left the room. - private async Task SetMysteryBoxStateAsync(IRoomItem? box, int state) - { - if (box is not null && box.Logic.GetState() != state) - { - await box.Logic.SetStateAsync(state).ConfigureAwait(true); - } - } - - private async Task EndMysteryBoxSessionAsync( - RoomMysteryBoxSession session, - bool notifyParticipants - ) - { - _state.MysteryBoxSessionsByOwnerId.Remove(session.BoxOwnerId); - - // The box is no longer in play, so it goes back to its plain closed frame. - _state.ItemsById.TryGetValue(session.BoxObjectId, out IRoomItem? box); - await SetMysteryBoxStateAsync(box, MysteryBoxSprite.ClosedState(session.Color)) - .ConfigureAwait(true); - - if (!notifyParticipants) - { - return; - } - - if (session.OwnerWaiting) - { - await SendMysteryBoxComposerAsync( - session.BoxOwnerId, - new CancelMysteryBoxWaitMessageComposer() - ) - .ConfigureAwait(true); - } - - if (session.KeyHolderId is PlayerId keyHolderId) - { - await SendMysteryBoxComposerAsync( - keyHolderId, - new CancelMysteryBoxWaitMessageComposer() - ) - .ConfigureAwait(true); - } - } - - private Task SendMysteryBoxComposerAsync( - PlayerId playerId, - Primitives.Networking.IComposer composer - ) => _grainFactory.GetPlayerPresenceGrain(playerId).SendComposerAsync(composer); + internal Task CancelMysteryBoxSessionForItemAsync(RoomObjectId boxObjectId) => + MysteryBoxSystem.CancelMysteryBoxSessionForItemAsync(boxObjectId); } diff --git a/Vortex.Rooms/Grains/RoomGrain.cs b/Vortex.Rooms/Grains/RoomGrain.cs index 445dac32..384a5649 100644 --- a/Vortex.Rooms/Grains/RoomGrain.cs +++ b/Vortex.Rooms/Grains/RoomGrain.cs @@ -81,6 +81,8 @@ public sealed partial class RoomGrain : Grain, IRoomGrain public readonly RoomRollerSystem RollerSystem; public readonly RoomSecurityModule SecurityModule; public readonly RoomWiredSystem WiredSystem; + public readonly RoomModerationSystem ModerationSystem; + public readonly RoomMysteryBoxSystem MysteryBoxSystem; internal IAsyncStream _roomOutbound = default!; @@ -138,6 +140,8 @@ RoomWiredLogChannel wiredLogChannel GameSystem = new RoomGameSystem(this); FreezeSystem = new RoomFreezeSystem(this); GameTimerSystem = new RoomGameTimerSystem(this); + ModerationSystem = new RoomModerationSystem(this); + MysteryBoxSystem = new RoomMysteryBoxSystem(this); EventModule.Register(RollerSystem); EventModule.Register(WiredSystem); diff --git a/Vortex.Rooms/Grains/Systems/RoomModerationSystem.cs b/Vortex.Rooms/Grains/Systems/RoomModerationSystem.cs new file mode 100644 index 00000000..2726c6ca --- /dev/null +++ b/Vortex.Rooms/Grains/Systems/RoomModerationSystem.cs @@ -0,0 +1,390 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Vortex.Primitives.Action; +using Vortex.Primitives.Events; +using Vortex.Primitives.Navigator.Enums; +using Vortex.Primitives.Permissions; +using Vortex.Primitives.Players; +using Vortex.Primitives.Rooms.Enums; + +namespace Vortex.Rooms.Grains.Systems; + +/// +/// Kick/mute/ban enforcement. Kept on the room grain rather than a separate grain: kicking and +/// banning mutate live avatar state (AvatarModule.RemoveAvatarFromPlayerAsync) and muting is +/// read on every chat message via RoomChatSystem's in-memory cache, both of which need the +/// synchronous, single-turn access only the owning grain has. +/// +public sealed class RoomModerationSystem(RoomGrain roomGrain) +{ + private readonly RoomGrain _roomGrain = roomGrain; + + public Task KickUserAsync( + ActionContext actorCtx, + PlayerId targetPlayerId, + CancellationToken ct + ) + { + if ( + actorCtx.PlayerId <= 0 + || actorCtx.RoomId != _roomGrain._state.RoomId + || actorCtx.PlayerId == targetPlayerId + ) + { + return Task.FromResult(false); + } + + return KickUserGuardedAsync(actorCtx, targetPlayerId, ct); + } + + private async Task KickUserGuardedAsync( + ActionContext actorCtx, + PlayerId targetPlayerId, + CancellationToken ct + ) + { + if ( + !await CanModerateAsync( + actorCtx, + _roomGrain._state.RoomSnapshot.ModSettings.WhoCanKick, + ModerationAction.Kick + ) + ) + { + return false; + } + + return await KickUserInternalAsync(actorCtx, targetPlayerId, ct).ConfigureAwait(true); + } + + /// + /// Whether may perform a moderation action gated by + /// in this room. System/wired origins resolve to moderator and pass. + /// + private async Task CanModerateAsync( + ActionContext actorCtx, + ModSettingType setting, + ModerationAction action + ) + { + RoomControllerType level = await _roomGrain + .SecurityModule.GetControllerLevelAsync(actorCtx) + .ConfigureAwait(true); + + bool hasStaffCapability = await _roomGrain + .SecurityModule.HasStaffModerationCapabilityAsync(actorCtx, action) + .ConfigureAwait(true); + + if (RoomModerationPolicy.CanModerate(level, setting, hasStaffCapability)) + { + return true; + } + + _roomGrain._logger.LogDebug( + "Player {ActorId} lacks {Setting} moderation rights in room {RoomId} (level {Level}).", + actorCtx.PlayerId, + setting, + _roomGrain._state.RoomId, + level + ); + + return false; + } + + /// Kicks a user without a human actor — for wired / system-driven kicks (the + /// wf_act_kick_user action). Called directly on the grain from inside its own turn, so it is + /// not a re-entrant grain-reference call. + public Task KickUserFromWiredAsync(PlayerId targetPlayerId, CancellationToken ct) => + KickUserInternalAsync( + ActionContext.CreateForWired(_roomGrain._state.RoomId), + targetPlayerId, + ct + ); + + private async Task KickUserInternalAsync( + ActionContext ctx, + PlayerId targetPlayerId, + CancellationToken ct + ) + { + try + { + if ( + targetPlayerId <= 0 + || !_roomGrain._state.AvatarsByPlayerId.ContainsKey(targetPlayerId) + ) + { + return false; + } + + await _roomGrain + .AvatarModule.RemoveAvatarFromPlayerAsync(ctx, targetPlayerId, ct) + .ConfigureAwait(true); + + await _roomGrain + ._events.PublishAsync( + new PlayerKickedFromRoomEvent( + ctx.PlayerId, + targetPlayerId, + _roomGrain._state.RoomId.Value + ), + ct + ) + .ConfigureAwait(true); + + return true; + } + catch (Exception ex) + { + _roomGrain._logger.LogWarning( + ex, + "Failed to kick player {TargetPlayerId} from room {RoomId}.", + targetPlayerId, + _roomGrain._state.RoomId + ); + + return false; + } + } + + public async Task MuteUserAsync( + ActionContext actorCtx, + PlayerId targetPlayerId, + int durationSeconds, + CancellationToken ct + ) + { + if ( + actorCtx.PlayerId <= 0 + || targetPlayerId <= 0 + || actorCtx.RoomId != _roomGrain._state.RoomId + ) + { + return false; + } + + if (durationSeconds <= 0 || actorCtx.PlayerId == targetPlayerId) + { + return false; + } + + if ( + !await CanModerateAsync( + actorCtx, + _roomGrain._state.RoomSnapshot.ModSettings.WhoCanMute, + ModerationAction.Mute + ) + ) + { + return false; + } + + DateTime expiresUtc = DateTime.UtcNow.AddSeconds(durationSeconds); + + try + { + await _roomGrain._moderationStore.MuteAsync( + _roomGrain._state.RoomId.Value, + targetPlayerId, + expiresUtc, + ct + ); + _roomGrain._state.MuteExpiresUtc[targetPlayerId] = expiresUtc; + + await _roomGrain + ._events.PublishAsync( + new PlayerMutedInRoomEvent( + actorCtx.PlayerId, + targetPlayerId, + _roomGrain._state.RoomId.Value, + durationSeconds + ), + ct + ) + .ConfigureAwait(true); + + return true; + } + catch (Exception ex) + { + _roomGrain._logger.LogWarning( + ex, + "Failed to mute player {TargetPlayerId} in room {RoomId}.", + targetPlayerId, + _roomGrain._state.RoomId + ); + } + + return false; + } + + public async Task BanUserAsync( + ActionContext actorCtx, + PlayerId targetPlayerId, + int durationSeconds, + CancellationToken ct + ) + { + try + { + if ( + actorCtx.PlayerId <= 0 + || targetPlayerId <= 0 + || actorCtx.RoomId != _roomGrain._state.RoomId + ) + { + return false; + } + + if (durationSeconds <= 0 || actorCtx.PlayerId == targetPlayerId) + { + return false; + } + + if ( + !await CanModerateAsync( + actorCtx, + _roomGrain._state.RoomSnapshot.ModSettings.WhoCanBan, + ModerationAction.Ban + ) + ) + { + return false; + } + + DateTime expiresUtc = DateTime.UtcNow.AddSeconds(durationSeconds); + + await _roomGrain._moderationStore.BanAsync( + _roomGrain._state.RoomId.Value, + targetPlayerId, + expiresUtc, + ct + ); + await _roomGrain + .AvatarModule.RemoveAvatarFromPlayerAsync(actorCtx, targetPlayerId, ct) + .ConfigureAwait(true); + + await _roomGrain + ._events.PublishAsync( + new PlayerBannedInRoomEvent( + actorCtx.PlayerId, + targetPlayerId, + _roomGrain._state.RoomId.Value, + durationSeconds + ), + ct + ) + .ConfigureAwait(true); + + return true; + } + catch (Exception ex) + { + _roomGrain._logger.LogWarning( + ex, + "Failed to ban player {TargetPlayerId} in room {RoomId}.", + targetPlayerId, + _roomGrain._state.RoomId + ); + + return false; + } + } + + public async Task UnmuteUserAsync( + ActionContext actorCtx, + PlayerId targetPlayerId, + CancellationToken ct + ) + { + try + { + if ( + actorCtx.PlayerId <= 0 + || targetPlayerId <= 0 + || actorCtx.RoomId != _roomGrain._state.RoomId + ) + { + return false; + } + + if ( + !await CanModerateAsync( + actorCtx, + _roomGrain._state.RoomSnapshot.ModSettings.WhoCanMute, + ModerationAction.Mute + ) + ) + { + return false; + } + + await _roomGrain + ._moderationStore.UnmuteAsync(_roomGrain._state.RoomId.Value, targetPlayerId, ct) + .ConfigureAwait(true); + _roomGrain._state.MuteExpiresUtc.Remove(targetPlayerId); + + return true; + } + catch (Exception ex) + { + _roomGrain._logger.LogWarning( + ex, + "Failed to unmute player {TargetPlayerId} in room {RoomId}.", + targetPlayerId, + _roomGrain._state.RoomId + ); + + return false; + } + } + + public async Task UnbanUserAsync( + ActionContext actorCtx, + PlayerId targetPlayerId, + CancellationToken ct + ) + { + try + { + if ( + actorCtx.PlayerId <= 0 + || targetPlayerId <= 0 + || actorCtx.RoomId != _roomGrain._state.RoomId + ) + { + return false; + } + + if ( + !await CanModerateAsync( + actorCtx, + _roomGrain._state.RoomSnapshot.ModSettings.WhoCanBan, + ModerationAction.Ban + ) + ) + { + return false; + } + + await _roomGrain + ._moderationStore.UnbanAsync(_roomGrain._state.RoomId.Value, targetPlayerId, ct) + .ConfigureAwait(true); + + return true; + } + catch (Exception ex) + { + _roomGrain._logger.LogWarning( + ex, + "Failed to unban player {TargetPlayerId} in room {RoomId}.", + targetPlayerId, + _roomGrain._state.RoomId + ); + + return false; + } + } +} diff --git a/Vortex.Rooms/Grains/Systems/RoomMysteryBoxSystem.cs b/Vortex.Rooms/Grains/Systems/RoomMysteryBoxSystem.cs new file mode 100644 index 00000000..1e4c52ca --- /dev/null +++ b/Vortex.Rooms/Grains/Systems/RoomMysteryBoxSystem.cs @@ -0,0 +1,609 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Vortex.Database.Context; +using Vortex.Database.Entities.Furniture; +using Vortex.Primitives.Action; +using Vortex.Primitives.Events; +using Vortex.Primitives.Messages.Outgoing.Mysterybox; +using Vortex.Primitives.MysteryBox; +using Vortex.Primitives.MysteryBox.Grains; +using Vortex.Primitives.MysteryBox.Snapshots; +using Vortex.Primitives.Orleans; +using Vortex.Primitives.Players; +using Vortex.Primitives.Rooms.Object; +using Vortex.Primitives.Rooms.Object.Furniture; + +namespace Vortex.Rooms.Grains.Systems; + +/// +/// The two-player mystery box open flow. Both halves are held by different players, so the pairing +/// lives on the room grain -- the single-threaded coordinator both sides already share -- which is +/// what keeps two key holders from claiming the same box or one key from opening two boxes. Kept on +/// the room grain rather than a separate grain: sessions are keyed off live floor-item state +/// (_state.ItemsById) and are torn down synchronously from the tick, from an avatar leaving, +/// and from item removal, all of which need single-turn atomicity with that same state. +/// +public sealed class RoomMysteryBoxSystem(RoomGrain roomGrain) +{ + private readonly RoomGrain _roomGrain = roomGrain; + + public async Task UseMysteryBoxAsync( + ActionContext ctx, + RoomObjectId boxObjectId, + CancellationToken ct + ) + { + PlayerId actorId = ctx.PlayerId; + + if (!_roomGrain._state.ItemsById.TryGetValue(boxObjectId, out IRoomItem? box)) + { + return; + } + + // A box wears its colour as its state, so the pairing colour comes from the item itself. + string boxColor = MysteryBoxSprite.ColorFromState(box.Logic.GetState()); + + if (boxColor.Length == 0) + { + // State 0: the box was created without a colour, so no key can ever match it. Say so + // rather than silently doing nothing on every click. + _roomGrain._logger.LogWarning( + "Mystery box {ObjectId} (definition {DefinitionId}) is in the colourless state 0 and can never be paired with a key.", + boxObjectId.Value, + box.Definition.Id + ); + + return; + } + + PlayerId ownerId = box.OwnerId; + RoomMysteryBoxSession session = ResolveSession(box, ownerId, boxColor); + + if (session.BoxObjectId != boxObjectId) + { + // The owner already has another box waiting. The cancel message only carries an owner + // id, so a second concurrent session would be un-cancellable. + _roomGrain._logger.LogDebug( + "Player {ActorId} clicked mystery box {BoxObjectId} while its owner already has box {ActiveBoxObjectId} waiting in room {RoomId}.", + actorId.Value, + boxObjectId.Value, + session.BoxObjectId.Value, + _roomGrain._state.RoomId.Value + ); + + return; + } + + if (actorId == ownerId) + { + session.OwnerWaiting = true; + } + else + { + if (session.KeyHolderId is not null && session.KeyHolderId != actorId) + { + // Someone else is already holding this box open; they get first refusal. + return; + } + + bool hasKey = await _roomGrain + ._grainFactory.GetPlayerMysteryBoxGrain(actorId) + .HasKeyAsync(session.Color, ct) + .ConfigureAwait(true); + + if (!hasKey) + { + // Nobody joined, so don't leave an empty session behind for the sweep to clear. + // Ended rather than removed so every abandonment path goes through the one place + // that also puts the box back to its closed frame. + if (session.IsAbandoned) + { + await EndMysteryBoxSessionAsync(session, notifyParticipants: false) + .ConfigureAwait(true); + } + + return; + } + + session.KeyHolderId = actorId; + } + + session.StartedAtMs = _roomGrain.NowMs(); + + if (!session.IsReadyToOpen) + { + // Second frame of the colour's group: the room can see the box is in play rather than + // wondering why nobody else can start it. + await SetMysteryBoxStateAsync(box, MysteryBoxSprite.WaitingState(session.Color)) + .ConfigureAwait(true); + + await SendMysteryBoxComposerAsync(actorId, new ShowMysteryBoxWaitMessageComposer()) + .ConfigureAwait(true); + + return; + } + + await OpenMysteryBoxAsync(ctx, session, ct).ConfigureAwait(true); + } + + public async Task CancelMysteryBoxWaitAsync( + ActionContext ctx, + PlayerId boxOwnerId, + CancellationToken ct + ) + { + PlayerId actorId = ctx.PlayerId; + + if ( + !_roomGrain._state.MysteryBoxSessionsByOwnerId.TryGetValue( + boxOwnerId, + out RoomMysteryBoxSession? session + ) + ) + { + return; + } + + if (!session.IsParticipant(actorId)) + { + return; + } + + if (actorId == session.BoxOwnerId) + { + // The owner walking away ends the whole thing; the key holder's dialog has to close too, + // otherwise they sit in front of a box that will never open. + await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); + + return; + } + + session.KeyHolderId = null; + + await SendMysteryBoxComposerAsync(actorId, new CancelMysteryBoxWaitMessageComposer()) + .ConfigureAwait(true); + + // Ending the session rather than dropping it from the dictionary: the box was repainted to + // its waiting frame when this key holder claimed it, and nothing else repaints it. Removing + // the entry on its own left the box showing "in play" forever with no session behind it. + if (session.IsAbandoned) + { + await EndMysteryBoxSessionAsync(session, notifyParticipants: false) + .ConfigureAwait(true); + } + } + + public async Task OpenMysteryTrophyAsync( + ActionContext ctx, + RoomObjectId objectId, + string inscription, + CancellationToken ct + ) + { + if (!_roomGrain._state.ItemsById.TryGetValue(objectId, out IRoomItem? trophy)) + { + return; + } + + // Only the owner may inscribe: the trophy is consumed and the prize lands in their + // inventory, so letting a visitor trigger it would spend someone else's furniture. + if (trophy.OwnerId != ctx.PlayerId) + { + return; + } + + MysteryBoxPrizeSnapshot? prize = await _roomGrain + ._grainFactory.GetMysteryBoxManagerGrain() + .PickPrizeAsync(MysteryBoxPrizePool.Trophy, string.Empty, ct) + .ConfigureAwait(true); + + if (prize is null) + { + return; + } + + string ownerName = trophy.OwnerName; + + if (string.IsNullOrEmpty(ownerName)) + { + ownerName = await _roomGrain + ._grainFactory.GetPlayerDirectoryGrain() + .GetPlayerNameAsync(trophy.OwnerId, ct) + .ConfigureAwait(true); + } + + int maxLength = Math.Max(0, _roomGrain._roomConfig.MysteryTrophyInscriptionMaxLength); + string trimmed = inscription.Length > maxLength ? inscription[..maxLength] : inscription; + + string legacyData = MysteryTrophyStuffData.Build( + ownerName, + DateOnly.FromDateTime(DateTime.UtcNow), + trimmed + ); + + // Consume first: if the grant then fails the player is out a mystery trophy, but the reverse + // order would let a repeated click mint trophies from one furniture. + if (!await ConsumeMysteryFurnitureAsync(ctx, trophy, ct).ConfigureAwait(true)) + { + return; + } + + await _roomGrain + ._grainFactory.GetInventoryGrain(trophy.OwnerId.Value) + .GrantFurnitureWithLegacyStuffDataAsync(prize.FurnitureDefinitionId, legacyData, ct) + .ConfigureAwait(true); + + await _roomGrain + ._events.PublishAsync( + new MysteryTrophyOpenedEvent( + _roomGrain._state.RoomId.Value, + objectId.Value, + trophy.OwnerId.Value, + prize.Id, + prize.FurnitureDefinitionId + ), + ct + ) + .ConfigureAwait(true); + } + + /// Tick-driven sweep. The client's wait dialog has no timer, so a player who wandered + /// off would otherwise keep the box reserved forever. + public async Task ProcessMysteryBoxTimeoutsAsync(long nowMs, CancellationToken ct) + { + if (_roomGrain._state.MysteryBoxSessionsByOwnerId.Count == 0) + { + return; + } + + List? expired = null; + + foreach (RoomMysteryBoxSession session in _roomGrain._state.MysteryBoxSessionsByOwnerId.Values) + { + if (nowMs - session.StartedAtMs < _roomGrain._roomConfig.MysteryBoxWaitTimeoutMs) + { + continue; + } + + (expired ??= []).Add(session); + } + + if (expired is null) + { + return; + } + + foreach (RoomMysteryBoxSession session in expired) + { + await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); + } + + await Task.CompletedTask.ConfigureAwait(true); + } + + /// Drops any pending open a leaving player was part of, so the box is not left reserved + /// by someone who is no longer in the room. + public async Task CancelMysteryBoxSessionsForLeavingPlayerAsync(PlayerId playerId) + { + if (_roomGrain._state.MysteryBoxSessionsByOwnerId.Count == 0) + { + return; + } + + List? affected = null; + + foreach (RoomMysteryBoxSession session in _roomGrain._state.MysteryBoxSessionsByOwnerId.Values) + { + if (session.IsParticipant(playerId)) + { + (affected ??= []).Add(session); + } + } + + if (affected is null) + { + return; + } + + foreach (RoomMysteryBoxSession session in affected) + { + if (session.BoxOwnerId == playerId) + { + await EndMysteryBoxSessionAsync(session, notifyParticipants: true) + .ConfigureAwait(true); + + continue; + } + + session.KeyHolderId = null; + + // Same reason as the explicit cancel: the box carries the waiting frame this key holder + // put it in, so it has to be repainted rather than just forgotten. + if (session.IsAbandoned) + { + await EndMysteryBoxSessionAsync(session, notifyParticipants: false) + .ConfigureAwait(true); + } + } + } + + /// Drops the pending open attached to a box that is being removed from the room. + public async Task CancelMysteryBoxSessionForItemAsync(RoomObjectId boxObjectId) + { + RoomMysteryBoxSession? attached = null; + + foreach (RoomMysteryBoxSession session in _roomGrain._state.MysteryBoxSessionsByOwnerId.Values) + { + if (session.BoxObjectId == boxObjectId) + { + attached = session; + + break; + } + } + + if (attached is null) + { + return; + } + + await EndMysteryBoxSessionAsync(attached, notifyParticipants: true).ConfigureAwait(true); + } + + private RoomMysteryBoxSession ResolveSession(IRoomItem box, PlayerId ownerId, string color) + { + if ( + _roomGrain._state.MysteryBoxSessionsByOwnerId.TryGetValue( + ownerId, + out RoomMysteryBoxSession? existing + ) + ) + { + // A session whose box has since left the room is stale; replace it rather than blocking + // the owner's remaining boxes forever. + if (_roomGrain._state.ItemsById.ContainsKey(existing.BoxObjectId)) + { + return existing; + } + + _roomGrain._state.MysteryBoxSessionsByOwnerId.Remove(ownerId); + } + + RoomMysteryBoxSession session = new() + { + BoxObjectId = box.ObjectId, + BoxOwnerId = ownerId, + Color = color, + StartedAtMs = _roomGrain.NowMs(), + }; + + _roomGrain._state.MysteryBoxSessionsByOwnerId[ownerId] = session; + + return session; + } + + private async Task OpenMysteryBoxAsync( + ActionContext ctx, + RoomMysteryBoxSession session, + CancellationToken ct + ) + { + PlayerId keyHolderId = session.KeyHolderId!.Value; + PlayerId ownerId = session.BoxOwnerId; + + if (!_roomGrain._state.ItemsById.TryGetValue(session.BoxObjectId, out IRoomItem? box)) + { + await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); + + return; + } + + IPlayerMysteryBoxGrain keyHolderGrain = _roomGrain._grainFactory.GetPlayerMysteryBoxGrain( + keyHolderId + ); + + // Spend the key before anything is handed out. The holder may have opened another box + // between clicking and pairing, and a prize granted against a key that no longer exists is + // free furniture. + if (!await keyHolderGrain.TryConsumeKeyAsync(session.Color, ct).ConfigureAwait(true)) + { + await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); + + return; + } + + // Claim the session before the box leaves the room: removing the furniture runs the + // "a waiting box was picked up" cleanup, which would otherwise cancel this very open. + _roomGrain._state.MysteryBoxSessionsByOwnerId.Remove(ownerId); + + // The box goes next, for the same reason the key did: while it still exists a second pairing + // could claim it. If this fails the key is already gone, so refund it rather than eating it. + if (!await ConsumeMysteryFurnitureAsync(ctx, box, ct).ConfigureAwait(true)) + { + await keyHolderGrain + .GrantKeyAsync(session.Color, "refund:box-open-failed", ct) + .ConfigureAwait(true); + await EndMysteryBoxSessionAsync(session, notifyParticipants: true).ConfigureAwait(true); + + return; + } + + await _roomGrain + ._events.PublishAsync( + new MysteryBoxOpenedEvent( + _roomGrain._state.RoomId.Value, + session.BoxObjectId.Value, + ownerId.Value, + keyHolderId.Value, + session.Color + ), + ct + ) + .ConfigureAwait(true); + + // Two independent draws: the pair open the box together but do not split one prize. + await Task.WhenAll( + AwardMysteryBoxPrizeAsync(ownerId, session.Color, ct), + AwardMysteryBoxPrizeAsync(keyHolderId, session.Color, ct) + ) + .ConfigureAwait(true); + + // The owner just lost their box, so their tracker is stale. The key holder's was already + // refreshed by the consume. + await _roomGrain + ._grainFactory.GetPlayerMysteryBoxGrain(ownerId) + .PushTrackerAsync(ct) + .ConfigureAwait(true); + } + + private async Task AwardMysteryBoxPrizeAsync( + PlayerId playerId, + string color, + CancellationToken ct + ) + { + MysteryBoxPrizeSnapshot? prize = await _roomGrain + ._grainFactory.GetMysteryBoxManagerGrain() + .PickPrizeAsync(MysteryBoxPrizePool.Box, color, ct) + .ConfigureAwait(true); + + MysteryBoxPrizeAward? award = prize is null + ? null + : await _roomGrain + ._grainFactory.GetPlayerMysteryBoxGrain(playerId) + .GrantPrizeAsync(prize, ct) + .ConfigureAwait(true); + + if (award is null) + { + // Nothing could be awarded (empty pool, broken prize row). Close the dialog instead of + // leaving the player staring at a spinner. + _roomGrain._logger.LogWarning( + "Mystery box opened in room {RoomId} but no prize could be awarded to player {PlayerId}.", + _roomGrain._state.RoomId.Value, + playerId.Value + ); + + await SendMysteryBoxComposerAsync(playerId, new CancelMysteryBoxWaitMessageComposer()) + .ConfigureAwait(true); + + return; + } + + await _roomGrain + ._events.PublishAsync( + new MysteryBoxPrizeAwardedEvent( + playerId.Value, + prize!.Id, + color, + award.ContentType, + award.ClassId + ), + ct + ) + .ConfigureAwait(true); + + await SendMysteryBoxComposerAsync( + playerId, + new GotMysteryBoxPrizeMessageComposer + { + ContentType = award.ContentType, + ClassId = award.ClassId, + } + ) + .ConfigureAwait(true); + } + + /// Removes a mystery furniture from the room and the database. Returns false when the + /// delete did not happen, so the caller can avoid handing out a prize for it. + private async Task ConsumeMysteryFurnitureAsync( + ActionContext ctx, + IRoomItem item, + CancellationToken ct + ) + { + try + { + await using VortexDbContext dbCtx = await _roomGrain + ._dbCtxFactory.CreateDbContextAsync(ct) + .ConfigureAwait(true); + + FurnitureEntity entity = new() { Id = item.ObjectId.Value }; + dbCtx.Attach(entity); + entity.DeletedAt = DateTime.UtcNow; + dbCtx.Entry(entity).Property(f => f.DeletedAt).IsModified = true; + + await dbCtx.SaveChangesAsync(ct).ConfigureAwait(true); + } + catch (Exception ex) + { + _roomGrain._logger.LogError( + ex, + "Failed to delete mystery furniture {ObjectId} in room {RoomId}", + item.ObjectId.Value, + _roomGrain._state.RoomId.Value + ); + + return false; + } + + await _roomGrain + .ObjectModule.RemoveObjectAsync(ctx, item, ct, item.OwnerId) + .ConfigureAwait(true); + + return true; + } + + /// Repaints a box, tolerating the item having already left the room. + private async Task SetMysteryBoxStateAsync(IRoomItem? box, int state) + { + if (box is not null && box.Logic.GetState() != state) + { + await box.Logic.SetStateAsync(state).ConfigureAwait(true); + } + } + + private async Task EndMysteryBoxSessionAsync( + RoomMysteryBoxSession session, + bool notifyParticipants + ) + { + _roomGrain._state.MysteryBoxSessionsByOwnerId.Remove(session.BoxOwnerId); + + // The box is no longer in play, so it goes back to its plain closed frame. + _roomGrain._state.ItemsById.TryGetValue(session.BoxObjectId, out IRoomItem? box); + await SetMysteryBoxStateAsync(box, MysteryBoxSprite.ClosedState(session.Color)) + .ConfigureAwait(true); + + if (!notifyParticipants) + { + return; + } + + if (session.OwnerWaiting) + { + await SendMysteryBoxComposerAsync( + session.BoxOwnerId, + new CancelMysteryBoxWaitMessageComposer() + ) + .ConfigureAwait(true); + } + + if (session.KeyHolderId is PlayerId keyHolderId) + { + await SendMysteryBoxComposerAsync( + keyHolderId, + new CancelMysteryBoxWaitMessageComposer() + ) + .ConfigureAwait(true); + } + } + + private Task SendMysteryBoxComposerAsync( + PlayerId playerId, + Primitives.Networking.IComposer composer + ) => _roomGrain._grainFactory.GetPlayerPresenceGrain(playerId).SendComposerAsync(composer); +} From 4cbfcddf79e8313cb10007336253dea22c0329cd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 02:06:42 +0000 Subject: [PATCH 10/11] refactor: extract trading logic out of RoomGrain (ARCH-02, completes the R5 partial) Same pattern and same reasoning as the moderation/mystery-box move: trading needs the room's live avatar/object-id mapping (_state.AvatarsByPlayerId) on every step and is torn down synchronously from RoomGrain.Avatar.cs when a participant leaves, so it stays on the room grain rather than becoming a separate one - but moves out of RoomGrain's own partial-class file into RoomTradingSystem, an owned class built the same way as RoomChatSystem/RoomWiredSystem/ RoomModerationSystem/RoomMysteryBoxSystem. RoomGrain keeps every existing public/internal method as a one-line delegate, so no external call site changes. Pure move, no logic changed. This completes the ARCH-02 R5 work for this session: RoomGrain.cs itself now owns five focused systems (Chat, Wired, Moderation, MysteryBox, Trading) instead of carrying their bodies directly, matching an established pattern rather than inventing a new one. Actual separate-grain extraction - the audit's original ask, and the part that would shrink IRoomGrain's 109-method interface - remains a follow-up: it requires a cross-grain state-sync design and integration tests against a live multi-silo cluster that this session couldn't validate, for code that grants prizes and enforces moderation. All 628 tests pass (251 in Vortex.Rooms.Tests); build stays clean at 0 warnings. --- Vortex.Rooms/Grains/RoomGrain.Trading.cs | 714 +---------------- Vortex.Rooms/Grains/RoomGrain.cs | 2 + .../Grains/Systems/RoomTradingSystem.cs | 737 ++++++++++++++++++ 3 files changed, 753 insertions(+), 700 deletions(-) create mode 100644 Vortex.Rooms/Grains/Systems/RoomTradingSystem.cs diff --git a/Vortex.Rooms/Grains/RoomGrain.Trading.cs b/Vortex.Rooms/Grains/RoomGrain.Trading.cs index a8fec83d..5677502d 100644 --- a/Vortex.Rooms/Grains/RoomGrain.Trading.cs +++ b/Vortex.Rooms/Grains/RoomGrain.Trading.cs @@ -1,721 +1,35 @@ using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.Extensions.Logging; -using Vortex.Database.Context; -using Vortex.Database.Entities.Furniture; -using Vortex.Primitives.Action; -using Vortex.Primitives.Events; -using Vortex.Primitives.Inventory.Grains; -using Vortex.Primitives.Inventory.Snapshots; -using Vortex.Primitives.Messages.Outgoing.Inventory.Trading; -using Vortex.Primitives.Networking; -using Vortex.Primitives.Orleans; using Vortex.Primitives.Players; -using Vortex.Primitives.Players.Grains; -using Vortex.Primitives.Rooms.Enums; -using Vortex.Primitives.Rooms.Object; namespace Vortex.Rooms.Grains; public sealed partial class RoomGrain { - // Client TradeOpenFailed reasons 7/8 pop a generic alert; any other value shows the templated - // "could not trade with {name}" message, which fits every gating failure here. - private const int TradeOpenFailedReason = 6; + public Task OpenTradeAsync(PlayerId requesterId, int otherRoomObjectId, CancellationToken ct) => + TradingSystem.OpenTradeAsync(requesterId, otherRoomObjectId, ct); - // TradingClose reason 1 is the client's commit-error alert; 0 is an ordinary cancellation. - private const int TradeCloseCancelled = 0; - private const int TradeCloseCommitError = 1; - - public async Task OpenTradeAsync( - PlayerId requesterId, - int otherRoomObjectId, - CancellationToken ct - ) - { - if (!_state.AvatarsByPlayerId.ContainsKey(requesterId)) - { - return; - } - - if ( - !TryResolvePlayerByObjectId(otherRoomObjectId, out PlayerId otherId) - || otherId == requesterId - ) - { - await SendTradeOpenFailedAsync(requesterId, otherId, ct).ConfigureAwait(true); - return; - } - - if ( - _state.TradeSessionsByPlayerId.ContainsKey(requesterId) - || _state.TradeSessionsByPlayerId.ContainsKey(otherId) - ) - { - await SendTradeOpenFailedAsync(requesterId, otherId, ct).ConfigureAwait(true); - return; - } - - bool requesterCanTrade = await CanTradeInRoomAsync(requesterId, ct).ConfigureAwait(true); - bool otherCanTrade = await CanTradeInRoomAsync(otherId, ct).ConfigureAwait(true); - - if (!requesterCanTrade || !otherCanTrade) - { - await SendTradeOpenFailedAsync(requesterId, otherId, ct).ConfigureAwait(true); - return; - } - - RoomObjectId requesterObjectId = _state.AvatarsByPlayerId[requesterId]; - RoomObjectId otherObjectId = _state.AvatarsByPlayerId[otherId]; - - RoomTradeSession session = new() - { - UserOneId = requesterId, - UserTwoId = otherId, - UserOneObjectId = requesterObjectId, - UserTwoObjectId = otherObjectId, - }; - - _state.TradeSessionsByPlayerId[requesterId] = session; - _state.TradeSessionsByPlayerId[otherId] = session; - - await SendToTradeAsync( - session, - new TradingOpenEventMessageComposer - { - UserId = requesterObjectId, - UserCanTrade = true, - OtherUserId = otherObjectId, - OtherUserCanTrade = true, - } - ) - .ConfigureAwait(true); - - await _events - .PublishAsync( - new TradeStartedEvent(requesterId.Value, otherId.Value, _state.RoomId.Value), - ct - ) - .ConfigureAwait(true); - } - - public async Task AddTradeItemsAsync( + public Task AddTradeItemsAsync( PlayerId requesterId, IReadOnlyList itemIds, CancellationToken ct - ) - { - if (!TryGetSession(requesterId, out RoomTradeSession? session)) - { - await SendTradingNotOpenAsync(requesterId, ct).ConfigureAwait(true); - return; - } - - if (session.Phase != TradePhase.Building) - { - return; - } - - List offer = session.ItemsOf(requesterId); - IInventoryGrain inventory = _grainFactory.GetInventoryGrain(requesterId); - bool changed = false; - - foreach (int itemId in itemIds) - { - if (offer.Contains(itemId) || offer.Count >= _roomConfig.MaxTradeItemsPerSide) - { - continue; - } - - FurnitureItemSnapshot? snapshot = await inventory - .GetItemSnapshotAsync(itemId, ct) - .ConfigureAwait(true); - - // An item the requester no longer owns or that is flagged non-tradeable is silently - // dropped from the offer -- there is no distinct "rejected item" composer in this client, - // and it will simply be absent from the item list the other party sees. - if (snapshot is null || !snapshot.Definition.CanTrade) - { - continue; - } - - offer.Add(itemId); - changed = true; - } - - if (changed) - { - session.ResetAgreement(); - await BroadcastItemListAsync(session, ct).ConfigureAwait(true); - } - } - - public async Task RemoveTradeItemAsync(PlayerId requesterId, int itemId, CancellationToken ct) - { - if (!TryGetSession(requesterId, out RoomTradeSession? session)) - { - return; - } - - if (session.Phase != TradePhase.Building) - { - return; - } - - if (!session.ItemsOf(requesterId).Remove(itemId)) - { - return; - } - - session.ResetAgreement(); - await BroadcastItemListAsync(session, ct).ConfigureAwait(true); - } + ) => TradingSystem.AddTradeItemsAsync(requesterId, itemIds, ct); - public async Task SetTradeAcceptAsync(PlayerId requesterId, bool accepted, CancellationToken ct) - { - if (!TryGetSession(requesterId, out RoomTradeSession? session)) - { - return; - } + public Task RemoveTradeItemAsync(PlayerId requesterId, int itemId, CancellationToken ct) => + TradingSystem.RemoveTradeItemAsync(requesterId, itemId, ct); - if (session.Phase != TradePhase.Building || session.AcceptedOf(requesterId) == accepted) - { - return; - } + public Task SetTradeAcceptAsync(PlayerId requesterId, bool accepted, CancellationToken ct) => + TradingSystem.SetTradeAcceptAsync(requesterId, accepted, ct); - session.SetAccepted(requesterId, accepted); + public Task ConfirmTradeAsync(PlayerId requesterId, bool confirm, CancellationToken ct) => + TradingSystem.ConfirmTradeAsync(requesterId, confirm, ct); - await SendToTradeAsync( - session, - new TradingAcceptEventMessageComposer - { - UserId = session.ObjectIdOf(requesterId), - UserAccepts = accepted, - } - ) - .ConfigureAwait(true); - - if (session.BothAccepted) - { - session.Phase = TradePhase.Confirming; - - await SendToTradeAsync(session, new TradingConfirmationEventMessageComposer()) - .ConfigureAwait(true); - } - } - - public async Task ConfirmTradeAsync(PlayerId requesterId, bool confirm, CancellationToken ct) - { - if (!TryGetSession(requesterId, out RoomTradeSession? session)) - { - return; - } - - if (session.Phase != TradePhase.Confirming) - { - return; - } - - if (!confirm) - { - await EndTradeAsync( - session, - session.ObjectIdOf(requesterId), - TradeCloseCancelled, - "declined", - [session.UserOneId, session.UserTwoId], - ct - ) - .ConfigureAwait(true); - return; - } - - session.SetConfirmed(requesterId, true); - - if (session.BothConfirmed) - { - await CommitTradeAsync(session, ct).ConfigureAwait(true); - } - } - - public async Task CloseTradeAsync(PlayerId requesterId, CancellationToken ct) - { - if (!TryGetSession(requesterId, out RoomTradeSession? session)) - { - return; - } - - await EndTradeAsync( - session, - session.ObjectIdOf(requesterId), - TradeCloseCancelled, - "cancelled", - [session.UserOneId, session.UserTwoId], - ct - ) - .ConfigureAwait(true); - } + public Task CloseTradeAsync(PlayerId requesterId, CancellationToken ct) => + TradingSystem.CloseTradeAsync(requesterId, ct); /// Tears down any trade a leaving/disconnecting player is in, notifying the participant /// who is still present. Called from RoomGrain.Avatar.cs when an avatar is removed. - internal async Task CloseTradeForLeavingPlayerAsync(PlayerId playerId, CancellationToken ct) - { - if (!TryGetSession(playerId, out RoomTradeSession? session)) - { - return; - } - - PlayerId remaining = session.OtherOf(playerId); - - await EndTradeAsync( - session, - session.ObjectIdOf(playerId), - TradeCloseCancelled, - "left", - [remaining], - ct - ) - .ConfigureAwait(true); - } - - private async Task CommitTradeAsync(RoomTradeSession session, CancellationToken ct) - { - bool swapped = false; - - try - { - // Re-validate ownership and tradeability right before committing: an item may have been - // placed, sold, or traded elsewhere while the confirm dialog was open. - if ( - !await ValidateOfferAsync(session.UserOneId, session.UserOneItemIds, ct) - .ConfigureAwait(true) - || !await ValidateOfferAsync(session.UserTwoId, session.UserTwoItemIds, ct) - .ConfigureAwait(true) - ) - { - await AbortCommitAsync(session, ct).ConfigureAwait(true); - return; - } - - swapped = await TryPersistOwnershipSwapAsync(session, ct).ConfigureAwait(true); - - if (!swapped) - { - await AbortCommitAsync(session, ct).ConfigureAwait(true); - return; - } - } - catch (System.Exception ex) - { - _logger.LogError( - ex, - "Trade commit failed between {UserOne} and {UserTwo} in room {RoomId}.", - session.UserOneId, - session.UserTwoId, - _state.RoomId - ); - - if (!swapped) - { - await AbortCommitAsync(session, ct).ConfigureAwait(true); - return; - } - } - - int[] oneItems = [.. session.UserOneItemIds]; - int[] twoItems = [.. session.UserTwoItemIds]; - - RemoveSession(session); - - // Ownership is persisted; the rest is best-effort resync/notification. A stale inventory - // cache self-heals on the next reload, so a failure here can never duplicate or lose an item. - try - { - await _grainFactory - .GetInventoryGrain(session.UserOneId) - .ApplyTradeResultAsync(oneItems, twoItems, ct) - .ConfigureAwait(true); - await _grainFactory - .GetInventoryGrain(session.UserTwoId) - .ApplyTradeResultAsync(twoItems, oneItems, ct) - .ConfigureAwait(true); - - // Per-item forensics (item_events) plus one trade-level business-audit record that the - // dashboard's Item audit surface reads. - await PublishTradeAuditAsync(session.UserOneId, session.UserTwoId, oneItems, ct) - .ConfigureAwait(true); - await PublishTradeAuditAsync(session.UserTwoId, session.UserOneId, twoItems, ct) - .ConfigureAwait(true); - await _events - .PublishAsync( - new TradeCompletedEvent( - session.UserOneId.Value, - session.UserTwoId.Value, - oneItems, - twoItems, - _state.RoomId.Value - ), - ct - ) - .ConfigureAwait(true); - } - catch (System.Exception ex) - { - _logger.LogWarning( - ex, - "Trade committed but post-commit resync failed in room {RoomId}.", - _state.RoomId - ); - } - - await Task.WhenAll( - _grainFactory - .GetPlayerPresenceGrain(session.UserOneId) - .SendComposerAsync(new TradingCompletedEventMessageComposer()), - _grainFactory - .GetPlayerPresenceGrain(session.UserTwoId) - .SendComposerAsync(new TradingCompletedEventMessageComposer()) - ) - .ConfigureAwait(true); - } - - private async Task ValidateOfferAsync( - PlayerId playerId, - IReadOnlyList itemIds, - CancellationToken ct - ) - { - if (itemIds.Count == 0) - { - return true; - } - - IInventoryGrain inventory = _grainFactory.GetInventoryGrain(playerId); - - foreach (int itemId in itemIds) - { - FurnitureItemSnapshot? snapshot = await inventory - .GetItemSnapshotAsync(itemId, ct) - .ConfigureAwait(true); - - if (snapshot is null || !snapshot.Definition.CanTrade) - { - return false; - } - } - - return true; - } - - /// Swaps furniture ownership for both offers inside a single database transaction, so the - /// exchange is all-or-nothing: either every item changes hands or none does. Guards that each row - /// is still owned by its offerer and not placed in a room before flipping player_id. - private async Task TryPersistOwnershipSwapAsync( - RoomTradeSession session, - CancellationToken ct - ) - { - List oneIds = session.UserOneItemIds; - List twoIds = session.UserTwoItemIds; - int oneOwner = session.UserOneId.Value; - int twoOwner = session.UserTwoId.Value; - - // A fresh DbContext is created per attempt (rather than shared across retries) so that a - // transient failure retried by the MySQL execution strategy never resubmits entities left - // half-tracked by a rolled-back attempt. - await using VortexDbContext strategyProbe = await _dbCtxFactory - .CreateDbContextAsync(ct) - .ConfigureAwait(true); - IExecutionStrategy strategy = strategyProbe.Database.CreateExecutionStrategy(); - - return await strategy - .ExecuteAsync(async () => - { - await using VortexDbContext dbCtx = await _dbCtxFactory - .CreateDbContextAsync(ct) - .ConfigureAwait(true); - await using IDbContextTransaction tx = await dbCtx - .Database.BeginTransactionAsync(ct) - .ConfigureAwait(true); - - List oneRows = await dbCtx - .Furnitures.Where(f => oneIds.Contains(f.Id)) - .ToListAsync(ct) - .ConfigureAwait(true); - List twoRows = await dbCtx - .Furnitures.Where(f => twoIds.Contains(f.Id)) - .ToListAsync(ct) - .ConfigureAwait(true); - - if ( - !IsOfferPersistable(oneRows, oneIds.Count, oneOwner) - || !IsOfferPersistable(twoRows, twoIds.Count, twoOwner) - ) - { - return false; - } - - foreach (FurnitureEntity row in oneRows) - { - row.PlayerEntityId = twoOwner; - } - - foreach (FurnitureEntity row in twoRows) - { - row.PlayerEntityId = oneOwner; - } - - await dbCtx.SaveChangesAsync(ct).ConfigureAwait(true); - await tx.CommitAsync(ct).ConfigureAwait(true); - - return true; - }) - .ConfigureAwait(true); - } - - private static bool IsOfferPersistable( - List rows, - int expectedCount, - int expectedOwner - ) => - rows.Count == expectedCount - && rows.All(r => r.PlayerEntityId == expectedOwner && r.RoomEntityId is null); - - private async Task PublishTradeAuditAsync( - PlayerId fromOwner, - PlayerId toOwner, - int[] itemIds, - CancellationToken ct - ) - { - foreach (int itemId in itemIds) - { - await _events - .PublishAsync( - new ItemTradedEvent( - itemId, - fromOwner.Value, - fromOwner.Value, - toOwner.Value, - _state.RoomId.Value - ), - ct - ) - .ConfigureAwait(true); - } - } - - private async Task AbortCommitAsync(RoomTradeSession session, CancellationToken ct) - { - await EndTradeAsync( - session, - session.UserOneObjectId, - TradeCloseCommitError, - "commit_error", - [session.UserOneId, session.UserTwoId], - ct - ) - .ConfigureAwait(true); - } - - private async Task EndTradeAsync( - RoomTradeSession session, - RoomObjectId cancellerObjectId, - int reason, - string auditReason, - IReadOnlyList notify, - CancellationToken ct - ) - { - PlayerId userOne = session.UserOneId; - PlayerId userTwo = session.UserTwoId; - - RemoveSession(session); - - TradingCloseEventMessageComposer composer = new() - { - UserId = cancellerObjectId, - Reason = reason, - }; - - await Task.WhenAll( - notify.Select(id => - _grainFactory.GetPlayerPresenceGrain(id).SendComposerAsync(composer) - ) - ) - .ConfigureAwait(true); - - await _events - .PublishAsync( - new TradeCancelledEvent( - userOne.Value, - userTwo.Value, - _state.RoomId.Value, - auditReason - ), - ct - ) - .ConfigureAwait(true); - } - - private void RemoveSession(RoomTradeSession session) - { - _state.TradeSessionsByPlayerId.Remove(session.UserOneId); - _state.TradeSessionsByPlayerId.Remove(session.UserTwoId); - } - - private bool TryGetSession(PlayerId playerId, out RoomTradeSession session) - { - if ( - _state.TradeSessionsByPlayerId.TryGetValue(playerId, out RoomTradeSession? found) - && found.IsParticipant(playerId) - ) - { - session = found; - return true; - } - - session = null!; - return false; - } - - private bool TryResolvePlayerByObjectId(int objectId, out PlayerId playerId) - { - foreach ((PlayerId candidate, RoomObjectId candidateObjectId) in _state.AvatarsByPlayerId) - { - if (candidateObjectId.Value == objectId) - { - playerId = candidate; - return true; - } - } - - playerId = default; - return false; - } - - private async Task CanTradeInRoomAsync(PlayerId playerId, CancellationToken ct) - { - RoomTradeModeType mode = _state.RoomSnapshot.TradeType; - - if (mode == RoomTradeModeType.Disabled) - { - return false; - } - - if (mode == RoomTradeModeType.Everyone) - { - return true; - } - - RoomControllerType level = await GetControllerLevelAsync( - ActionContext.CreateForPlayer(playerId, _state.RoomId), - ct - ) - .ConfigureAwait(true); - - return level >= RoomControllerType.Rights; - } - - private async Task SendTradeOpenFailedAsync( - PlayerId requesterId, - PlayerId otherId, - CancellationToken ct - ) - { - string otherName = - otherId > 0 - ? await _grainFactory - .GetPlayerDirectoryGrain() - .GetPlayerNameAsync(otherId, ct) - .ConfigureAwait(true) - : string.Empty; - - await _grainFactory - .GetPlayerPresenceGrain(requesterId) - .SendComposerAsync( - new TradeOpenFailedEventPaserMessageComposer - { - Reason = TradeOpenFailedReason, - OtherUserName = otherName, - } - ) - .ConfigureAwait(true); - } - - private Task SendTradingNotOpenAsync(PlayerId playerId, CancellationToken ct) => - _grainFactory - .GetPlayerPresenceGrain(playerId) - .SendComposerAsync(new TradingNotOpenEventMessageComposer()); - - private async Task BroadcastItemListAsync(RoomTradeSession session, CancellationToken ct) - { - ImmutableArray oneItems = await ResolveOfferSnapshotsAsync( - session.UserOneId, - session.UserOneItemIds, - ct - ) - .ConfigureAwait(true); - ImmutableArray twoItems = await ResolveOfferSnapshotsAsync( - session.UserTwoId, - session.UserTwoItemIds, - ct - ) - .ConfigureAwait(true); - - await SendToTradeAsync( - session, - new TradingItemListEventMessageComposer - { - FirstUserId = session.UserOneObjectId, - FirstUserItems = oneItems, - FirstUserCredits = 0, - SecondUserId = session.UserTwoObjectId, - SecondUserItems = twoItems, - SecondUserCredits = 0, - } - ) - .ConfigureAwait(true); - } - - private async Task> ResolveOfferSnapshotsAsync( - PlayerId playerId, - List itemIds, - CancellationToken ct - ) - { - if (itemIds.Count == 0) - { - return []; - } - - IInventoryGrain inventory = _grainFactory.GetInventoryGrain(playerId); - ImmutableArray.Builder builder = - ImmutableArray.CreateBuilder(itemIds.Count); - - foreach (int itemId in itemIds) - { - FurnitureItemSnapshot? snapshot = await inventory - .GetItemSnapshotAsync(itemId, ct) - .ConfigureAwait(true); - - if (snapshot is not null) - { - builder.Add(snapshot); - } - } - - return builder.ToImmutable(); - } - - private Task SendToTradeAsync(RoomTradeSession session, IComposer composer) => - Task.WhenAll( - _grainFactory.GetPlayerPresenceGrain(session.UserOneId).SendComposerAsync(composer), - _grainFactory.GetPlayerPresenceGrain(session.UserTwoId).SendComposerAsync(composer) - ); + internal Task CloseTradeForLeavingPlayerAsync(PlayerId playerId, CancellationToken ct) => + TradingSystem.CloseTradeForLeavingPlayerAsync(playerId, ct); } diff --git a/Vortex.Rooms/Grains/RoomGrain.cs b/Vortex.Rooms/Grains/RoomGrain.cs index 384a5649..75cb1664 100644 --- a/Vortex.Rooms/Grains/RoomGrain.cs +++ b/Vortex.Rooms/Grains/RoomGrain.cs @@ -83,6 +83,7 @@ public sealed partial class RoomGrain : Grain, IRoomGrain public readonly RoomWiredSystem WiredSystem; public readonly RoomModerationSystem ModerationSystem; public readonly RoomMysteryBoxSystem MysteryBoxSystem; + public readonly RoomTradingSystem TradingSystem; internal IAsyncStream _roomOutbound = default!; @@ -142,6 +143,7 @@ RoomWiredLogChannel wiredLogChannel GameTimerSystem = new RoomGameTimerSystem(this); ModerationSystem = new RoomModerationSystem(this); MysteryBoxSystem = new RoomMysteryBoxSystem(this); + TradingSystem = new RoomTradingSystem(this); EventModule.Register(RollerSystem); EventModule.Register(WiredSystem); diff --git a/Vortex.Rooms/Grains/Systems/RoomTradingSystem.cs b/Vortex.Rooms/Grains/Systems/RoomTradingSystem.cs new file mode 100644 index 00000000..f2ff464f --- /dev/null +++ b/Vortex.Rooms/Grains/Systems/RoomTradingSystem.cs @@ -0,0 +1,737 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; +using Vortex.Database.Context; +using Vortex.Database.Entities.Furniture; +using Vortex.Primitives.Action; +using Vortex.Primitives.Events; +using Vortex.Primitives.Inventory.Grains; +using Vortex.Primitives.Inventory.Snapshots; +using Vortex.Primitives.Messages.Outgoing.Inventory.Trading; +using Vortex.Primitives.Networking; +using Vortex.Primitives.Orleans; +using Vortex.Primitives.Players; +using Vortex.Primitives.Players.Grains; +using Vortex.Primitives.Rooms.Enums; +using Vortex.Primitives.Rooms.Object; +using Vortex.Rooms.Grains; + +namespace Vortex.Rooms.Grains.Systems; + +/// +/// The player-to-player furniture trade flow. Kept on the room grain rather than a separate grain: +/// opening and offer-building need the room's live avatar/object-id mapping +/// (_state.AvatarsByPlayerId) on every step, and a leaving player's trade is torn down +/// synchronously from RoomGrain.Avatar.cs as part of the same turn that removes their avatar. +/// +public sealed class RoomTradingSystem(RoomGrain roomGrain) +{ + // Client TradeOpenFailed reasons 7/8 pop a generic alert; any other value shows the templated + // "could not trade with {name}" message, which fits every gating failure here. + private const int TradeOpenFailedReason = 6; + + // TradingClose reason 1 is the client's commit-error alert; 0 is an ordinary cancellation. + private const int TradeCloseCancelled = 0; + private const int TradeCloseCommitError = 1; + + private readonly RoomGrain _roomGrain = roomGrain; + + public async Task OpenTradeAsync( + PlayerId requesterId, + int otherRoomObjectId, + CancellationToken ct + ) + { + if (!_roomGrain._state.AvatarsByPlayerId.ContainsKey(requesterId)) + { + return; + } + + if ( + !TryResolvePlayerByObjectId(otherRoomObjectId, out PlayerId otherId) + || otherId == requesterId + ) + { + await SendTradeOpenFailedAsync(requesterId, otherId, ct).ConfigureAwait(true); + return; + } + + if ( + _roomGrain._state.TradeSessionsByPlayerId.ContainsKey(requesterId) + || _roomGrain._state.TradeSessionsByPlayerId.ContainsKey(otherId) + ) + { + await SendTradeOpenFailedAsync(requesterId, otherId, ct).ConfigureAwait(true); + return; + } + + bool requesterCanTrade = await CanTradeInRoomAsync(requesterId, ct).ConfigureAwait(true); + bool otherCanTrade = await CanTradeInRoomAsync(otherId, ct).ConfigureAwait(true); + + if (!requesterCanTrade || !otherCanTrade) + { + await SendTradeOpenFailedAsync(requesterId, otherId, ct).ConfigureAwait(true); + return; + } + + RoomObjectId requesterObjectId = _roomGrain._state.AvatarsByPlayerId[requesterId]; + RoomObjectId otherObjectId = _roomGrain._state.AvatarsByPlayerId[otherId]; + + RoomTradeSession session = new() + { + UserOneId = requesterId, + UserTwoId = otherId, + UserOneObjectId = requesterObjectId, + UserTwoObjectId = otherObjectId, + }; + + _roomGrain._state.TradeSessionsByPlayerId[requesterId] = session; + _roomGrain._state.TradeSessionsByPlayerId[otherId] = session; + + await SendToTradeAsync( + session, + new TradingOpenEventMessageComposer + { + UserId = requesterObjectId, + UserCanTrade = true, + OtherUserId = otherObjectId, + OtherUserCanTrade = true, + } + ) + .ConfigureAwait(true); + + await _roomGrain + ._events.PublishAsync( + new TradeStartedEvent(requesterId.Value, otherId.Value, _roomGrain._state.RoomId.Value), + ct + ) + .ConfigureAwait(true); + } + + public async Task AddTradeItemsAsync( + PlayerId requesterId, + IReadOnlyList itemIds, + CancellationToken ct + ) + { + if (!TryGetSession(requesterId, out RoomTradeSession? session)) + { + await SendTradingNotOpenAsync(requesterId, ct).ConfigureAwait(true); + return; + } + + if (session.Phase != TradePhase.Building) + { + return; + } + + List offer = session.ItemsOf(requesterId); + IInventoryGrain inventory = _roomGrain._grainFactory.GetInventoryGrain(requesterId); + bool changed = false; + + foreach (int itemId in itemIds) + { + if (offer.Contains(itemId) || offer.Count >= _roomGrain._roomConfig.MaxTradeItemsPerSide) + { + continue; + } + + FurnitureItemSnapshot? snapshot = await inventory + .GetItemSnapshotAsync(itemId, ct) + .ConfigureAwait(true); + + // An item the requester no longer owns or that is flagged non-tradeable is silently + // dropped from the offer -- there is no distinct "rejected item" composer in this client, + // and it will simply be absent from the item list the other party sees. + if (snapshot is null || !snapshot.Definition.CanTrade) + { + continue; + } + + offer.Add(itemId); + changed = true; + } + + if (changed) + { + session.ResetAgreement(); + await BroadcastItemListAsync(session, ct).ConfigureAwait(true); + } + } + + public async Task RemoveTradeItemAsync(PlayerId requesterId, int itemId, CancellationToken ct) + { + if (!TryGetSession(requesterId, out RoomTradeSession? session)) + { + return; + } + + if (session.Phase != TradePhase.Building) + { + return; + } + + if (!session.ItemsOf(requesterId).Remove(itemId)) + { + return; + } + + session.ResetAgreement(); + await BroadcastItemListAsync(session, ct).ConfigureAwait(true); + } + + public async Task SetTradeAcceptAsync(PlayerId requesterId, bool accepted, CancellationToken ct) + { + if (!TryGetSession(requesterId, out RoomTradeSession? session)) + { + return; + } + + if (session.Phase != TradePhase.Building || session.AcceptedOf(requesterId) == accepted) + { + return; + } + + session.SetAccepted(requesterId, accepted); + + await SendToTradeAsync( + session, + new TradingAcceptEventMessageComposer + { + UserId = session.ObjectIdOf(requesterId), + UserAccepts = accepted, + } + ) + .ConfigureAwait(true); + + if (session.BothAccepted) + { + session.Phase = TradePhase.Confirming; + + await SendToTradeAsync(session, new TradingConfirmationEventMessageComposer()) + .ConfigureAwait(true); + } + } + + public async Task ConfirmTradeAsync(PlayerId requesterId, bool confirm, CancellationToken ct) + { + if (!TryGetSession(requesterId, out RoomTradeSession? session)) + { + return; + } + + if (session.Phase != TradePhase.Confirming) + { + return; + } + + if (!confirm) + { + await EndTradeAsync( + session, + session.ObjectIdOf(requesterId), + TradeCloseCancelled, + "declined", + [session.UserOneId, session.UserTwoId], + ct + ) + .ConfigureAwait(true); + return; + } + + session.SetConfirmed(requesterId, true); + + if (session.BothConfirmed) + { + await CommitTradeAsync(session, ct).ConfigureAwait(true); + } + } + + public async Task CloseTradeAsync(PlayerId requesterId, CancellationToken ct) + { + if (!TryGetSession(requesterId, out RoomTradeSession? session)) + { + return; + } + + await EndTradeAsync( + session, + session.ObjectIdOf(requesterId), + TradeCloseCancelled, + "cancelled", + [session.UserOneId, session.UserTwoId], + ct + ) + .ConfigureAwait(true); + } + + /// Tears down any trade a leaving/disconnecting player is in, notifying the participant + /// who is still present. Called from RoomGrain.Avatar.cs when an avatar is removed. + public async Task CloseTradeForLeavingPlayerAsync(PlayerId playerId, CancellationToken ct) + { + if (!TryGetSession(playerId, out RoomTradeSession? session)) + { + return; + } + + PlayerId remaining = session.OtherOf(playerId); + + await EndTradeAsync( + session, + session.ObjectIdOf(playerId), + TradeCloseCancelled, + "left", + [remaining], + ct + ) + .ConfigureAwait(true); + } + + private async Task CommitTradeAsync(RoomTradeSession session, CancellationToken ct) + { + bool swapped = false; + + try + { + // Re-validate ownership and tradeability right before committing: an item may have been + // placed, sold, or traded elsewhere while the confirm dialog was open. + if ( + !await ValidateOfferAsync(session.UserOneId, session.UserOneItemIds, ct) + .ConfigureAwait(true) + || !await ValidateOfferAsync(session.UserTwoId, session.UserTwoItemIds, ct) + .ConfigureAwait(true) + ) + { + await AbortCommitAsync(session, ct).ConfigureAwait(true); + return; + } + + swapped = await TryPersistOwnershipSwapAsync(session, ct).ConfigureAwait(true); + + if (!swapped) + { + await AbortCommitAsync(session, ct).ConfigureAwait(true); + return; + } + } + catch (System.Exception ex) + { + _roomGrain._logger.LogError( + ex, + "Trade commit failed between {UserOne} and {UserTwo} in room {RoomId}.", + session.UserOneId, + session.UserTwoId, + _roomGrain._state.RoomId + ); + + if (!swapped) + { + await AbortCommitAsync(session, ct).ConfigureAwait(true); + return; + } + } + + int[] oneItems = [.. session.UserOneItemIds]; + int[] twoItems = [.. session.UserTwoItemIds]; + + RemoveSession(session); + + // Ownership is persisted; the rest is best-effort resync/notification. A stale inventory + // cache self-heals on the next reload, so a failure here can never duplicate or lose an item. + try + { + await _roomGrain + ._grainFactory.GetInventoryGrain(session.UserOneId) + .ApplyTradeResultAsync(oneItems, twoItems, ct) + .ConfigureAwait(true); + await _roomGrain + ._grainFactory.GetInventoryGrain(session.UserTwoId) + .ApplyTradeResultAsync(twoItems, oneItems, ct) + .ConfigureAwait(true); + + // Per-item forensics (item_events) plus one trade-level business-audit record that the + // dashboard's Item audit surface reads. + await PublishTradeAuditAsync(session.UserOneId, session.UserTwoId, oneItems, ct) + .ConfigureAwait(true); + await PublishTradeAuditAsync(session.UserTwoId, session.UserOneId, twoItems, ct) + .ConfigureAwait(true); + await _roomGrain + ._events.PublishAsync( + new TradeCompletedEvent( + session.UserOneId.Value, + session.UserTwoId.Value, + oneItems, + twoItems, + _roomGrain._state.RoomId.Value + ), + ct + ) + .ConfigureAwait(true); + } + catch (System.Exception ex) + { + _roomGrain._logger.LogWarning( + ex, + "Trade committed but post-commit resync failed in room {RoomId}.", + _roomGrain._state.RoomId + ); + } + + await Task.WhenAll( + _roomGrain + ._grainFactory.GetPlayerPresenceGrain(session.UserOneId) + .SendComposerAsync(new TradingCompletedEventMessageComposer()), + _roomGrain + ._grainFactory.GetPlayerPresenceGrain(session.UserTwoId) + .SendComposerAsync(new TradingCompletedEventMessageComposer()) + ) + .ConfigureAwait(true); + } + + private async Task ValidateOfferAsync( + PlayerId playerId, + IReadOnlyList itemIds, + CancellationToken ct + ) + { + if (itemIds.Count == 0) + { + return true; + } + + IInventoryGrain inventory = _roomGrain._grainFactory.GetInventoryGrain(playerId); + + foreach (int itemId in itemIds) + { + FurnitureItemSnapshot? snapshot = await inventory + .GetItemSnapshotAsync(itemId, ct) + .ConfigureAwait(true); + + if (snapshot is null || !snapshot.Definition.CanTrade) + { + return false; + } + } + + return true; + } + + /// Swaps furniture ownership for both offers inside a single database transaction, so the + /// exchange is all-or-nothing: either every item changes hands or none does. Guards that each row + /// is still owned by its offerer and not placed in a room before flipping player_id. + private async Task TryPersistOwnershipSwapAsync( + RoomTradeSession session, + CancellationToken ct + ) + { + List oneIds = session.UserOneItemIds; + List twoIds = session.UserTwoItemIds; + int oneOwner = session.UserOneId.Value; + int twoOwner = session.UserTwoId.Value; + + // A fresh DbContext is created per attempt (rather than shared across retries) so that a + // transient failure retried by the MySQL execution strategy never resubmits entities left + // half-tracked by a rolled-back attempt. + await using VortexDbContext strategyProbe = await _roomGrain + ._dbCtxFactory.CreateDbContextAsync(ct) + .ConfigureAwait(true); + IExecutionStrategy strategy = strategyProbe.Database.CreateExecutionStrategy(); + + return await strategy + .ExecuteAsync(async () => + { + await using VortexDbContext dbCtx = await _roomGrain + ._dbCtxFactory.CreateDbContextAsync(ct) + .ConfigureAwait(true); + await using IDbContextTransaction tx = await dbCtx + .Database.BeginTransactionAsync(ct) + .ConfigureAwait(true); + + List oneRows = await dbCtx + .Furnitures.Where(f => oneIds.Contains(f.Id)) + .ToListAsync(ct) + .ConfigureAwait(true); + List twoRows = await dbCtx + .Furnitures.Where(f => twoIds.Contains(f.Id)) + .ToListAsync(ct) + .ConfigureAwait(true); + + if ( + !IsOfferPersistable(oneRows, oneIds.Count, oneOwner) + || !IsOfferPersistable(twoRows, twoIds.Count, twoOwner) + ) + { + return false; + } + + foreach (FurnitureEntity row in oneRows) + { + row.PlayerEntityId = twoOwner; + } + + foreach (FurnitureEntity row in twoRows) + { + row.PlayerEntityId = oneOwner; + } + + await dbCtx.SaveChangesAsync(ct).ConfigureAwait(true); + await tx.CommitAsync(ct).ConfigureAwait(true); + + return true; + }) + .ConfigureAwait(true); + } + + private static bool IsOfferPersistable( + List rows, + int expectedCount, + int expectedOwner + ) => + rows.Count == expectedCount + && rows.All(r => r.PlayerEntityId == expectedOwner && r.RoomEntityId is null); + + private async Task PublishTradeAuditAsync( + PlayerId fromOwner, + PlayerId toOwner, + int[] itemIds, + CancellationToken ct + ) + { + foreach (int itemId in itemIds) + { + await _roomGrain + ._events.PublishAsync( + new ItemTradedEvent( + itemId, + fromOwner.Value, + fromOwner.Value, + toOwner.Value, + _roomGrain._state.RoomId.Value + ), + ct + ) + .ConfigureAwait(true); + } + } + + private async Task AbortCommitAsync(RoomTradeSession session, CancellationToken ct) + { + await EndTradeAsync( + session, + session.UserOneObjectId, + TradeCloseCommitError, + "commit_error", + [session.UserOneId, session.UserTwoId], + ct + ) + .ConfigureAwait(true); + } + + private async Task EndTradeAsync( + RoomTradeSession session, + RoomObjectId cancellerObjectId, + int reason, + string auditReason, + IReadOnlyList notify, + CancellationToken ct + ) + { + PlayerId userOne = session.UserOneId; + PlayerId userTwo = session.UserTwoId; + + RemoveSession(session); + + TradingCloseEventMessageComposer composer = new() + { + UserId = cancellerObjectId, + Reason = reason, + }; + + await Task.WhenAll( + notify.Select(id => + _roomGrain._grainFactory.GetPlayerPresenceGrain(id).SendComposerAsync(composer) + ) + ) + .ConfigureAwait(true); + + await _roomGrain + ._events.PublishAsync( + new TradeCancelledEvent( + userOne.Value, + userTwo.Value, + _roomGrain._state.RoomId.Value, + auditReason + ), + ct + ) + .ConfigureAwait(true); + } + + private void RemoveSession(RoomTradeSession session) + { + _roomGrain._state.TradeSessionsByPlayerId.Remove(session.UserOneId); + _roomGrain._state.TradeSessionsByPlayerId.Remove(session.UserTwoId); + } + + private bool TryGetSession(PlayerId playerId, out RoomTradeSession session) + { + if ( + _roomGrain._state.TradeSessionsByPlayerId.TryGetValue( + playerId, + out RoomTradeSession? found + ) + && found.IsParticipant(playerId) + ) + { + session = found; + return true; + } + + session = null!; + return false; + } + + private bool TryResolvePlayerByObjectId(int objectId, out PlayerId playerId) + { + foreach ( + (PlayerId candidate, RoomObjectId candidateObjectId) in _roomGrain + ._state.AvatarsByPlayerId + ) + { + if (candidateObjectId.Value == objectId) + { + playerId = candidate; + return true; + } + } + + playerId = default; + return false; + } + + private async Task CanTradeInRoomAsync(PlayerId playerId, CancellationToken ct) + { + RoomTradeModeType mode = _roomGrain._state.RoomSnapshot.TradeType; + + if (mode == RoomTradeModeType.Disabled) + { + return false; + } + + if (mode == RoomTradeModeType.Everyone) + { + return true; + } + + RoomControllerType level = await _roomGrain + .GetControllerLevelAsync( + ActionContext.CreateForPlayer(playerId, _roomGrain._state.RoomId), + ct + ) + .ConfigureAwait(true); + + return level >= RoomControllerType.Rights; + } + + private async Task SendTradeOpenFailedAsync( + PlayerId requesterId, + PlayerId otherId, + CancellationToken ct + ) + { + string otherName = + otherId > 0 + ? await _roomGrain + ._grainFactory.GetPlayerDirectoryGrain() + .GetPlayerNameAsync(otherId, ct) + .ConfigureAwait(true) + : string.Empty; + + await _roomGrain + ._grainFactory.GetPlayerPresenceGrain(requesterId) + .SendComposerAsync( + new TradeOpenFailedEventPaserMessageComposer + { + Reason = TradeOpenFailedReason, + OtherUserName = otherName, + } + ) + .ConfigureAwait(true); + } + + private Task SendTradingNotOpenAsync(PlayerId playerId, CancellationToken ct) => + _roomGrain + ._grainFactory.GetPlayerPresenceGrain(playerId) + .SendComposerAsync(new TradingNotOpenEventMessageComposer()); + + private async Task BroadcastItemListAsync(RoomTradeSession session, CancellationToken ct) + { + ImmutableArray oneItems = await ResolveOfferSnapshotsAsync( + session.UserOneId, + session.UserOneItemIds, + ct + ) + .ConfigureAwait(true); + ImmutableArray twoItems = await ResolveOfferSnapshotsAsync( + session.UserTwoId, + session.UserTwoItemIds, + ct + ) + .ConfigureAwait(true); + + await SendToTradeAsync( + session, + new TradingItemListEventMessageComposer + { + FirstUserId = session.UserOneObjectId, + FirstUserItems = oneItems, + FirstUserCredits = 0, + SecondUserId = session.UserTwoObjectId, + SecondUserItems = twoItems, + SecondUserCredits = 0, + } + ) + .ConfigureAwait(true); + } + + private async Task> ResolveOfferSnapshotsAsync( + PlayerId playerId, + List itemIds, + CancellationToken ct + ) + { + if (itemIds.Count == 0) + { + return []; + } + + IInventoryGrain inventory = _roomGrain._grainFactory.GetInventoryGrain(playerId); + ImmutableArray.Builder builder = + ImmutableArray.CreateBuilder(itemIds.Count); + + foreach (int itemId in itemIds) + { + FurnitureItemSnapshot? snapshot = await inventory + .GetItemSnapshotAsync(itemId, ct) + .ConfigureAwait(true); + + if (snapshot is not null) + { + builder.Add(snapshot); + } + } + + return builder.ToImmutable(); + } + + private Task SendToTradeAsync(RoomTradeSession session, IComposer composer) => + Task.WhenAll( + _roomGrain._grainFactory.GetPlayerPresenceGrain(session.UserOneId).SendComposerAsync(composer), + _roomGrain._grainFactory.GetPlayerPresenceGrain(session.UserTwoId).SendComposerAsync(composer) + ); +} From bd155d2f64cf30d2ad3d8e3b984864cf14a4cb5d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 02:25:46 +0000 Subject: [PATCH 11/11] refactor: split Revision20260701 into per-domain maps, add RevisionBase (REV-01/R3) Revision20260701.cs was 3612 lines: two giant Dictionary initializers (header->parser, composer type->serializer) organized only by #region comments. Every new message meant editing that one file, making it a permanent merge-conflict magnet, and there was no way for a future revision to inherit from this one and override just a handful of entries - adding a second revision meant duplicating the whole file. - IRevisionMap/IRevisionMapBuilder (new, in Vortex.Primitives): a map registers its header/parser and type/serializer pairs into a builder; RevisionMapBuilder collects them and now throws on a duplicate header or composer type instead of silently overwriting it, so two maps accidentally claiming the same entry fails loudly the moment it happens instead of losing a registration silently. - RevisionBase: assembles a revision's Parsers/Serializers tables from a list of IRevisionMap instances, with virtual ConfigureParsers/ ConfigureSerializers hooks a descendant revision can override to add or replace specific entries without duplicating the rest. - Revision20260701.cs is now 59 lines: it lists 43 domain map instances and its revision id string. The three parsers needing ProtocolLimitsConfig (RemoveFriendMessageParser, AddItemsToTradeMessageParser, SaveRoomSettingsMessageParser) are each in their own domain map, taking the config as a constructor argument. - IRevision.Parsers/Serializers are now IReadOnlyDictionary instead of IDictionary (REV-03) - nothing outside revision construction should be able to mutate a revision's tables. - No parser/serializer logic changed - this only reorganizes which file registers which existing implementation against which existing header/type. Verified mechanically: the original two dictionaries had exactly 500 parser and 367 serializer entries; the 43 map files register exactly the same counts, confirmed again at runtime via Revision20260701.Parsers.Count/Serializers.Count. Combined with the earlier by-contract revision registration (RevisionRegistrationService, IRevisionManager.SetDefault), adding a message now touches one domain map file instead of a 3612-line one, and a second revision can inherit from this one instead of starting from a duplicated 4750-line copy. All 628 tests pass; build stays clean at 0 warnings. --- .../Networking/Revisions/IRevision.cs | 4 +- .../Networking/Revisions/IRevisionMap.cs | 6 + .../Revisions/IRevisionMapBuilder.cs | 14 + .../Revision20260701/Maps/AdvertisementMap.cs | 30 + .../Revision20260701/Maps/AvailabilityMap.cs | 42 + .../Revision20260701/Maps/AvatarMap.cs | 48 + .../Revision20260701/Maps/CallForHelpMap.cs | 24 + .../Revision20260701/Maps/CameraMap.cs | 59 + .../Revision20260701/Maps/CampaignMap.cs | 34 + .../Revision20260701/Maps/CatalogMap.cs | 294 ++ .../Revision20260701/Maps/CollectiblesMap.cs | 153 + .../Revision20260701/Maps/CompetitionMap.cs | 53 + .../Revision20260701/Maps/CraftingMap.cs | 25 + .../Revision20260701/Maps/FriendFurniMap.cs | 15 + .../Revision20260701/Maps/FriendListMap.cs | 132 + .../Revision20260701/Maps/GameMap.cs | 107 + .../Revision20260701/Maps/GiftsMap.cs | 24 + .../Revision20260701/Maps/GroupForumsMap.cs | 81 + .../Revision20260701/Maps/HandshakeMap.cs | 71 + .../Revision20260701/Maps/HelpMap.cs | 107 + .../Revision20260701/Maps/HotlooksMap.cs | 12 + .../Revision20260701/Maps/InventoryMap.cs | 377 ++ .../Revision20260701/Maps/LandingViewMap.cs | 20 + .../Revision20260701/Maps/MarketplaceMap.cs | 99 + .../Revision20260701/Maps/ModeratorMap.cs | 59 + .../Revision20260701/Maps/MysteryBoxMap.cs | 42 + .../Revision20260701/Maps/NavigatorMap.cs | 234 ++ .../Revision20260701/Maps/NewNavigatorMap.cs | 71 + .../Revision20260701/Maps/NftMap.cs | 25 + .../Revision20260701/Maps/NotificationsMap.cs | 96 + .../Revision20260701/Maps/NuxMap.cs | 23 + .../Revision20260701/Maps/PerkMap.cs | 18 + .../Revision20260701/Maps/PollMap.cs | 14 + .../Revision20260701/Maps/PreferencesMap.cs | 42 + .../Revision20260701/Maps/QuestMap.cs | 102 + .../Revision20260701/Maps/RegisterMap.cs | 15 + .../Revision20260701/Maps/RoomDirectoryMap.cs | 15 + .../Revision20260701/Maps/RoomMap.cs | 831 ++++ .../Revision20260701/Maps/RoomSettingsMap.cs | 48 + .../Revision20260701/Maps/SoundMap.cs | 35 + .../Revision20260701/Maps/TalentMap.cs | 23 + .../Revision20260701/Maps/TrackingMap.cs | 37 + .../Maps/UserClassificationMap.cs | 19 + .../Maps/UserDefinedRoomEventsMap.cs | 217 + .../Revision20260701/Maps/UsersMap.cs | 310 ++ .../Revision20260701/Maps/VaultMap.cs | 42 + .../Revision20260701/Revision20260701.cs | 3655 +---------------- Vortex.Revisions/RevisionBase.cs | 51 + Vortex.Revisions/RevisionMapBuilder.cs | 42 + 49 files changed, 4291 insertions(+), 3606 deletions(-) create mode 100644 Vortex.Primitives/Networking/Revisions/IRevisionMap.cs create mode 100644 Vortex.Primitives/Networking/Revisions/IRevisionMapBuilder.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/AdvertisementMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/AvailabilityMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/AvatarMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/CallForHelpMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/CameraMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/CampaignMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/CatalogMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/CollectiblesMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/CompetitionMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/CraftingMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/FriendFurniMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/FriendListMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/GameMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/GiftsMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/GroupForumsMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/HandshakeMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/HelpMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/HotlooksMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/InventoryMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/LandingViewMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/MarketplaceMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/ModeratorMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/MysteryBoxMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/NavigatorMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/NewNavigatorMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/NftMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/NotificationsMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/NuxMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/PerkMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/PollMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/PreferencesMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/QuestMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/RegisterMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/RoomDirectoryMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/RoomMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/RoomSettingsMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/SoundMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/TalentMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/TrackingMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/UserClassificationMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/UserDefinedRoomEventsMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/UsersMap.cs create mode 100644 Vortex.Revisions/Revision20260701/Maps/VaultMap.cs create mode 100644 Vortex.Revisions/RevisionBase.cs create mode 100644 Vortex.Revisions/RevisionMapBuilder.cs diff --git a/Vortex.Primitives/Networking/Revisions/IRevision.cs b/Vortex.Primitives/Networking/Revisions/IRevision.cs index b41e140a..118ef850 100644 --- a/Vortex.Primitives/Networking/Revisions/IRevision.cs +++ b/Vortex.Primitives/Networking/Revisions/IRevision.cs @@ -8,7 +8,7 @@ public interface IRevision { public string Revision { get; } - public IDictionary Parsers { get; } + public IReadOnlyDictionary Parsers { get; } - public IDictionary Serializers { get; } + public IReadOnlyDictionary Serializers { get; } } diff --git a/Vortex.Primitives/Networking/Revisions/IRevisionMap.cs b/Vortex.Primitives/Networking/Revisions/IRevisionMap.cs new file mode 100644 index 00000000..35e6777a --- /dev/null +++ b/Vortex.Primitives/Networking/Revisions/IRevisionMap.cs @@ -0,0 +1,6 @@ +namespace Vortex.Primitives.Networking.Revisions; + +public interface IRevisionMap +{ + void RegisterInto(IRevisionMapBuilder builder); +} diff --git a/Vortex.Primitives/Networking/Revisions/IRevisionMapBuilder.cs b/Vortex.Primitives/Networking/Revisions/IRevisionMapBuilder.cs new file mode 100644 index 00000000..0b1b94f1 --- /dev/null +++ b/Vortex.Primitives/Networking/Revisions/IRevisionMapBuilder.cs @@ -0,0 +1,14 @@ +using System; +using Vortex.Primitives.Packets; + +namespace Vortex.Primitives.Networking.Revisions; + +public interface IRevisionMapBuilder +{ + void MapParser(int header, IParser parser); + + void MapSerializer(Type composerType, ISerializer serializer); + + void MapSerializer(ISerializer serializer) => + MapSerializer(typeof(TComposer), serializer); +} diff --git a/Vortex.Revisions/Revision20260701/Maps/AdvertisementMap.cs b/Vortex.Revisions/Revision20260701/Maps/AdvertisementMap.cs new file mode 100644 index 00000000..6cac3b04 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/AdvertisementMap.cs @@ -0,0 +1,30 @@ +using Vortex.Primitives.Messages.Outgoing.Advertisement; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Advertisement; +using Vortex.Revisions.Revision20260701.Serializers.Advertisement; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class AdvertisementMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.GetInterstitialMessageEvent, + new GetInterstitialMessageParser() + ); + builder.MapParser( + MessageEvent.InterstitialShownMessageEvent, + new InterstitialShownMessageParser() + ); + + builder.MapSerializer( + typeof(InterstitialMessageComposer), + new InterstitialMessageComposerSerializer(MessageComposer.InterstitialMessageComposer) + ); + builder.MapSerializer( + typeof(RoomAdErrorEventMessageComposer), + new RoomAdErrorEventMessageComposerSerializer(MessageComposer.RoomAdErrorComposer) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/AvailabilityMap.cs b/Vortex.Revisions/Revision20260701/Maps/AvailabilityMap.cs new file mode 100644 index 00000000..1e9df403 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/AvailabilityMap.cs @@ -0,0 +1,42 @@ +using Vortex.Primitives.Messages.Outgoing.Availability; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Serializers.Availability; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class AvailabilityMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapSerializer( + typeof(AvailabilityStatusMessageComposer), + new AvailabilityStatusMessageComposerSerializer( + MessageComposer.AvailabilityStatusMessageComposer + ) + ); + builder.MapSerializer( + typeof(InfoHotelClosedMessageComposer), + new InfoHotelClosedMessageComposerSerializer( + MessageComposer.InfoHotelClosedMessageComposer + ) + ); + builder.MapSerializer( + typeof(InfoHotelClosingMessageComposer), + new InfoHotelClosingMessageComposerSerializer( + MessageComposer.InfoHotelClosingMessageComposer + ) + ); + builder.MapSerializer( + typeof(LoginFailedHotelClosedMessageComposer), + new LoginFailedHotelClosedMessageComposerSerializer( + MessageComposer.LoginFailedHotelClosedMessageComposer + ) + ); + builder.MapSerializer( + typeof(MaintenanceStatusMessageComposer), + new MaintenanceStatusMessageComposerSerializer( + MessageComposer.MaintenanceStatusMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/AvatarMap.cs b/Vortex.Revisions/Revision20260701/Maps/AvatarMap.cs new file mode 100644 index 00000000..3238b5e5 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/AvatarMap.cs @@ -0,0 +1,48 @@ +using Vortex.Primitives.Messages.Outgoing.Avatar; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Avatar; +using Vortex.Revisions.Revision20260701.Serializers.Avatar; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class AvatarMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.ChangeUserNameInRoomMessageEvent, + new ChangeUserNameInRoomMessageParser() + ); + builder.MapParser( + MessageEvent.ChangeUserNameMessageEvent, + new ChangeUserNameMessageParser() + ); + builder.MapParser(MessageEvent.CheckUserNameMessageEvent, new CheckUserNameMessageParser()); + builder.MapParser(MessageEvent.GetWardrobeMessageEvent, new GetWardrobeMessageParser()); + builder.MapParser( + MessageEvent.SaveWardrobeOutfitMessageEvent, + new SaveWardrobeOutfitMessageParser() + ); + + builder.MapSerializer( + typeof(ChangeUserNameResultMessageComposer), + new ChangeUserNameResultMessageComposerSerializer( + MessageComposer.ChangeUserNameResultMessageComposer + ) + ); + builder.MapSerializer( + typeof(CheckUserNameResultMessageComposer), + new CheckUserNameResultMessageComposerSerializer( + MessageComposer.CheckUserNameResultMessageComposer + ) + ); + builder.MapSerializer( + typeof(FigureUpdateEventMessageComposer), + new FigureUpdateEventMessageComposerSerializer(MessageComposer.FigureUpdateComposer) + ); + builder.MapSerializer( + typeof(WardrobeMessageComposer), + new WardrobeMessageComposerSerializer(MessageComposer.WardrobeMessageComposer) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/CallForHelpMap.cs b/Vortex.Revisions/Revision20260701/Maps/CallForHelpMap.cs new file mode 100644 index 00000000..b5ab554b --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/CallForHelpMap.cs @@ -0,0 +1,24 @@ +using Vortex.Primitives.Messages.Outgoing.Callforhelp; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Serializers.CallForHelp; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class CallForHelpMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapSerializer( + typeof(CfhSanctionMessageComposer), + new CfhSanctionMessageComposerSerializer(MessageComposer.CfhSanctionMessageComposer) + ); + builder.MapSerializer( + typeof(CfhTopicsInitMessageComposer), + new CfhTopicsInitMessageComposerSerializer(MessageComposer.CfhTopicsInitMessageComposer) + ); + builder.MapSerializer( + typeof(SanctionStatusEventMessageComposer), + new SanctionStatusEventMessageComposerSerializer(MessageComposer.SanctionStatusComposer) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/CameraMap.cs b/Vortex.Revisions/Revision20260701/Maps/CameraMap.cs new file mode 100644 index 00000000..703be501 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/CameraMap.cs @@ -0,0 +1,59 @@ +using Vortex.Primitives.Messages.Outgoing.Camera; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Camera; +using Vortex.Revisions.Revision20260701.Serializers.Camera; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class CameraMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.PhotoCompetitionMessageEvent, + new PhotoCompetitionMessageParser() + ); + builder.MapParser(MessageEvent.PublishPhotoMessageEvent, new PublishPhotoMessageParser()); + builder.MapParser(MessageEvent.PurchasePhotoMessageEvent, new PurchasePhotoMessageParser()); + builder.MapParser(MessageEvent.RenderRoomMessageEvent, new RenderRoomMessageParser()); + builder.MapParser( + MessageEvent.RequestCameraConfigurationMessageEvent, + new RequestCameraConfigurationMessageParser() + ); + + builder.MapSerializer( + typeof(CameraPublishStatusMessageComposer), + new CameraPublishStatusMessageComposerSerializer( + MessageComposer.CameraPublishStatusMessageComposer + ) + ); + builder.MapSerializer( + typeof(CameraPurchaseOKMessageComposer), + new CameraPurchaseOKMessageComposerSerializer( + MessageComposer.CameraPurchaseOKMessageComposer + ) + ); + builder.MapSerializer( + typeof(CameraStorageUrlMessageComposer), + new CameraStorageUrlMessageComposerSerializer( + MessageComposer.CameraStorageUrlMessageComposer + ) + ); + builder.MapSerializer( + typeof(CompetitionStatusMessageComposer), + new CompetitionStatusMessageComposerSerializer( + MessageComposer.CompetitionStatusMessageComposer + ) + ); + builder.MapSerializer( + typeof(InitCameraMessageComposer), + new InitCameraMessageComposerSerializer(MessageComposer.InitCameraMessageComposer) + ); + builder.MapSerializer( + typeof(ThumbnailStatusMessageComposer), + new ThumbnailStatusMessageComposerSerializer( + MessageComposer.ThumbnailStatusMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/CampaignMap.cs b/Vortex.Revisions/Revision20260701/Maps/CampaignMap.cs new file mode 100644 index 00000000..2fd3a5d9 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/CampaignMap.cs @@ -0,0 +1,34 @@ +using Vortex.Primitives.Messages.Outgoing.Campaign; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Campaign; +using Vortex.Revisions.Revision20260701.Serializers.Campaign; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class CampaignMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.OpenCampaignCalendarDoorAsStaffEvent, + new OpenCampaignCalendarDoorAsStaffMessageParser() + ); + builder.MapParser( + MessageEvent.OpenCampaignCalendarDoorEvent, + new OpenCampaignCalendarDoorMessageParser() + ); + + builder.MapSerializer( + typeof(CampaignCalendarDataMessageComposer), + new CampaignCalendarDataMessageComposerSerializer( + MessageComposer.CampaignCalendarDataMessageComposer + ) + ); + builder.MapSerializer( + typeof(CampaignCalendarDoorOpenedMessageComposer), + new CampaignCalendarDataMessageComposerSerializer( + MessageComposer.CampaignCalendarDoorOpenedMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/CatalogMap.cs b/Vortex.Revisions/Revision20260701/Maps/CatalogMap.cs new file mode 100644 index 00000000..5d61d29b --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/CatalogMap.cs @@ -0,0 +1,294 @@ +using Vortex.Primitives.Messages.Outgoing.Catalog; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Catalog; +using Vortex.Revisions.Revision20260701.Serializers.Catalog; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class CatalogMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.BuildersClubPlaceRoomItemMessageEvent, + new BuildersClubPlaceRoomItemMessageParser() + ); + builder.MapParser( + MessageEvent.BuildersClubPlaceWallItemMessageEvent, + new BuildersClubPlaceWallItemMessageParser() + ); + builder.MapParser( + MessageEvent.BuildersClubQueryFurniCountMessageEvent, + new BuildersClubQueryFurniCountMessageParser() + ); + // charge firework? + builder.MapParser( + MessageEvent.GetBonusRareInfoMessageEvent, + new GetBonusRareInfoMessageParser() + ); + builder.MapParser( + MessageEvent.GetBundleDiscountRulesetEvent, + new GetBundleDiscountRulesetMessageParser() + ); + builder.MapParser( + MessageEvent.GetBundleDiscountRulesetVortexEvent, + new GetBundleDiscountRulesetMessageParser() + ); + builder.MapParser(MessageEvent.GetCatalogIndexEvent, new GetCatalogIndexMessageParser()); + builder.MapParser(MessageEvent.GetCatalogPageEvent, new GetCatalogPageMessageParser()); + builder.MapParser( + MessageEvent.GetCatalogPageWithEarliestExpiryEvent, + new GetCatalogPageWithEarliestExpiryMessageParser() + ); + builder.MapParser(MessageEvent.GetClubGiftMessageEvent, new GetClubGiftInfoMessageParser()); + builder.MapParser( + MessageEvent.GetClubGiftInfoRequestMessageEvent, + new GetClubGiftInfoMessageParser() + ); + builder.MapParser(MessageEvent.GetClubOffersMessageEvent, new GetClubOffersMessageParser()); + builder.MapParser( + MessageEvent.GetGiftWrappingConfigurationEvent, + new GetGiftWrappingConfigurationMessageParser() + ); + builder.MapParser( + MessageEvent.GetGiftWrappingConfigurationVortexEvent, + new GetGiftWrappingConfigurationMessageParser() + ); + builder.MapParser( + MessageEvent.GetHabboClubExtendOfferMessageEvent, + new GetHabboClubExtendOfferMessageParser() + ); + builder.MapParser( + MessageEvent.GetIsOfferGiftableEvent, + new GetIsOfferGiftableMessageParser() + ); + builder.MapParser( + MessageEvent.GetLimitedOfferAppearingNextEvent, + new GetLimitedOfferAppearingNextMessageParser() + ); + builder.MapParser( + MessageEvent.GetNextTargetedOfferEvent, + new GetNextTargetedOfferMessageParser() + ); + builder.MapParser(MessageEvent.GetProductOfferEvent, new GetProductOfferMessageParser()); + builder.MapParser( + MessageEvent.GetRoomAdPurchaseInfoEvent, + new GetRoomAdPurchaseInfoMessageParser() + ); + builder.MapParser( + MessageEvent.GetSeasonalCalendarDailyEvent, + new GetSeasonalCalendarDailyOfferMessageParser() + ); + builder.MapParser( + MessageEvent.GetSellablePetPalettesEvent, + new GetSellablePetPalettesMessageParser() + ); + builder.MapParser( + MessageEvent.MarkCatalogNewAdditionsPageOpenedEvent, + new MarkCatalogNewAdditionsPageOpenedMessageParser() + ); + builder.MapParser( + MessageEvent.PurchaseBasicMembershipExtensionEvent, + new PurchaseBasicMembershipExtensionMessageParser() + ); + builder.MapParser( + MessageEvent.PurchaseFromCatalogAsGiftEvent, + new PurchaseFromCatalogAsGiftMessageParser() + ); + builder.MapParser( + MessageEvent.PurchaseFromCatalogEvent, + new PurchaseFromCatalogMessageParser() + ); + builder.MapParser( + MessageEvent.PurchaseRoomAdMessageEvent, + new PurchaseRoomAdMessageParser() + ); + builder.MapParser( + MessageEvent.PurchaseTargetedOfferEvent, + new PurchaseTargetedOfferMessageParser() + ); + builder.MapParser( + MessageEvent.PurchaseVipMembershipExtensionEvent, + new PurchaseVipMembershipExtensionMessageParser() + ); + builder.MapParser(MessageEvent.RedeemVoucherMessageEvent, new RedeemVoucherMessageParser()); + builder.MapParser( + MessageEvent.RoomAdPurchaseInitiatedEvent, + new RoomAdPurchaseInitiatedMessageParser() + ); + builder.MapParser(MessageEvent.SelectClubGiftEvent, new SelectClubGiftMessageParser()); + builder.MapParser( + MessageEvent.SetTargetedOfferStateEvent, + new SetTargetedOfferStateMessageParser() + ); + builder.MapParser( + MessageEvent.ShopTargetedOfferViewedEvent, + new ShopTargetedOfferViewedMessageParser() + ); + + builder.MapSerializer( + typeof(BonusRareInfoMessageComposer), + new BonusRareInfoMessageComposerSerializer(MessageComposer.BonusRareInfoMessageComposer) + ); + builder.MapSerializer( + typeof(BuildersClubFurniCountMessageComposer), + new BuildersClubFurniCountMessageComposerSerializer( + MessageComposer.BuildersClubFurniCountMessageComposer + ) + ); + builder.MapSerializer( + typeof(BuildersClubSubscriptionStatusMessageComposer), + new BuildersClubSubscriptionStatusMessageComposerSerializer( + MessageComposer.BuildersClubSubscriptionStatusMessageComposer + ) + ); + builder.MapSerializer( + typeof(BundleDiscountRulesetMessageComposer), + new BundleDiscountRulesetMessageComposerSerializer( + MessageComposer.BundleDiscountRulesetMessageComposer + ) + ); + builder.MapSerializer( + typeof(CatalogIndexMessageComposer), + new CatalogIndexMessageComposerSerializer(MessageComposer.CatalogIndexMessageComposer) + ); + builder.MapSerializer( + typeof(CatalogPageMessageComposer), + new CatalogPageMessageComposerSerializer(MessageComposer.CatalogPageMessageComposer) + ); + builder.MapSerializer( + typeof(CatalogPageWithEarliestExpiryMessageComposer), + new CatalogPageWithEarliestExpiryMessageComposerSerializer( + MessageComposer.CatalogPageWithEarliestExpiryMessageComposer + ) + ); + builder.MapSerializer( + typeof(CatalogPublishedMessageComposer), + new CatalogPublishedMessageComposerSerializer( + MessageComposer.CatalogPublishedMessageComposer + ) + ); + builder.MapSerializer( + typeof(ClubGiftInfoEventMessageComposer), + new ClubGiftInfoEventMessageComposerSerializer(MessageComposer.ClubGiftInfoComposer) + ); + builder.MapSerializer( + typeof(ClubGiftSelectedEventMessageComposer), + new ClubGiftSelectedEventMessageComposerSerializer( + MessageComposer.ClubGiftSelectedComposer + ) + ); + builder.MapSerializer( + typeof(GiftReceiverNotFoundEventMessageComposer), + new GiftReceiverNotFoundEventMessageComposerSerializer( + MessageComposer.GiftReceiverNotFoundComposer + ) + ); + builder.MapSerializer( + typeof(GiftWrappingConfigurationEventMessageComposer), + new GiftWrappingConfigurationEventMessageComposerSerializer( + MessageComposer.GiftWrappingConfigurationComposer + ) + ); + builder.MapSerializer( + typeof(HabboClubExtendOfferMessageComposer), + new HabboClubExtendOfferMessageComposerSerializer( + MessageComposer.HabboClubExtendOfferMessageComposer + ) + ); + builder.MapSerializer( + typeof(HabboClubOffersMessageComposer), + new HabboClubOffersMessageComposerSerializer( + MessageComposer.HabboClubOffersMessageComposer + ) + ); + builder.MapSerializer( + typeof(IsOfferGiftableEventMessageComposer), + new IsOfferGiftableEventMessageComposerSerializer( + MessageComposer.IsOfferGiftableEventMessageComposer + ) + ); + builder.MapSerializer( + typeof(LimitedEditionSoldOutEventMessageComposer), + new LimitedEditionSoldOutEventMessageComposerSerializer( + MessageComposer.LimitedEditionSoldOutComposer + ) + ); + builder.MapSerializer( + typeof(LimitedOfferAppearingNextMessageComposer), + new LimitedOfferAppearingNextMessageComposerSerializer( + MessageComposer.LimitedOfferAppearingNextMessageComposer + ) + ); + builder.MapSerializer( + typeof(NotEnoughBalanceMessageComposer), + new NotEnoughBalanceMessageComposerSerializer( + MessageComposer.NotEnoughBalanceMessageComposer + ) + ); + builder.MapSerializer( + typeof(ProductOfferEventMessageComposer), + new ProductOfferEventMessageComposerSerializer(MessageComposer.ProductOfferComposer) + ); + builder.MapSerializer( + typeof(PurchaseErrorMessageComposer), + new PurchaseErrorMessageComposerSerializer(MessageComposer.PurchaseErrorMessageComposer) + ); + builder.MapSerializer( + typeof(PurchaseNotAllowedMessageComposer), + new PurchaseNotAllowedMessageComposerSerializer( + MessageComposer.PurchaseNotAllowedMessageComposer + ) + ); + builder.MapSerializer( + typeof(PurchaseOKMessageComposer), + new PurchaseOKMessageComposerSerializer(MessageComposer.PurchaseOKMessageComposer) + ); + builder.MapSerializer( + typeof(RoomAdPurchaseInfoEventMessageComposer), + new RoomAdPurchaseInfoEventMessageComposerSerializer( + MessageComposer.RoomAdPurchaseInfoComposer + ) + ); + builder.MapSerializer( + typeof(SeasonalCalendarDailyOfferMessageComposer), + new SeasonalCalendarDailyOfferMessageComposerSerializer( + MessageComposer.SeasonalCalendarDailyOfferMessageComposer + ) + ); + builder.MapSerializer( + typeof(SellablePetPalettesMessageComposer), + new SellablePetPalettesMessageComposerSerializer( + MessageComposer.SellablePetPalettesMessageComposer + ) + ); + builder.MapSerializer( + typeof(SnowWarGameTokensMessageMessageComposer), + new SnowWarGameTokensMessageMessageComposerSerializer( + MessageComposer.SnowWarGameTokensMessageComposer + ) + ); + builder.MapSerializer( + typeof(TargetedOfferEventMessageComposer), + new TargetedOfferEventMessageComposerSerializer(MessageComposer.TargetedOfferComposer) + ); + builder.MapSerializer( + typeof(TargetedOfferNotFoundEventMessageComposer), + new TargetedOfferNotFoundEventMessageComposerSerializer( + MessageComposer.TargetedOfferNotFoundComposer + ) + ); + builder.MapSerializer( + typeof(VoucherRedeemErrorMessageComposer), + new VoucherRedeemErrorMessageComposerSerializer( + MessageComposer.VoucherRedeemErrorMessageComposer + ) + ); + builder.MapSerializer( + typeof(VoucherRedeemOkMessageComposer), + new VoucherRedeemOkMessageComposerSerializer( + MessageComposer.VoucherRedeemOkMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/CollectiblesMap.cs b/Vortex.Revisions/Revision20260701/Maps/CollectiblesMap.cs new file mode 100644 index 00000000..d3c7df99 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/CollectiblesMap.cs @@ -0,0 +1,153 @@ +using Vortex.Primitives.Messages.Outgoing.Collectibles; +using Vortex.Primitives.Messages.Outgoing.Nft; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Collectibles; +using Vortex.Revisions.Revision20260701.Serializers.Collectibles; +using Vortex.Revisions.Revision20260701.Serializers.Nft; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class CollectiblesMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.GetCollectibleMintableItemTypesMessageEvent, + new GetCollectibleMintableItemTypesMessageParser() + ); + builder.MapParser( + MessageEvent.GetCollectibleMintingEnabledMessageEvent, + new GetCollectibleMintingEnabledMessageParser() + ); + builder.MapParser( + MessageEvent.GetCollectibleMintTokensMessageEvent, + new GetCollectibleMintTokensMessageParser() + ); + builder.MapParser( + MessageEvent.GetCollectibleWalletAddressesMessageEvent, + new GetCollectibleWalletAddressesMessageParser() + ); + builder.MapParser( + MessageEvent.GetCollectorScoreMessageEvent, + new GetCollectorScoreMessageParser() + ); + builder.MapParser( + MessageEvent.GetMintTokenOffersMessageEvent, + new GetMintTokenOffersMessageParser() + ); + builder.MapParser( + MessageEvent.GetNftCollectionsMessageEvent, + new GetNftCollectionsMessageParser() + ); + builder.MapParser( + MessageEvent.GetNftTransferFeeMessageEvent, + new GetNftTransferFeeMessageParser() + ); + builder.MapParser(MessageEvent.MintItemMessageEvent, new MintItemMessageParser()); + builder.MapParser( + MessageEvent.NftCollectiblesClaimBonusItemMessageEvent, + new NftCollectiblesClaimBonusItemMessageParser() + ); + builder.MapParser( + MessageEvent.NftCollectiblesClaimRewardItemMessageEvent, + new NftCollectiblesClaimRewardItemMessageParser() + ); + builder.MapParser( + MessageEvent.NftTransferAssetsMessageEvent, + new NftTransferAssetsMessageParser() + ); + builder.MapParser( + MessageEvent.PurchaseMintTokenMessageEvent, + new PurchaseMintTokenMessageParser() + ); + + builder.MapSerializer( + typeof(CollectableMintableItemTypesMessageComposer), + new CollectableMintableItemTypesMessageComposerSerializer( + MessageComposer.CollectableMintableItemTypesMessageComposer + ) + ); + builder.MapSerializer( + typeof(CollectibleMintableItemResultMessageComposer), + new CollectibleMintableItemResultMessageComposerSerializer( + MessageComposer.CollectibleMintableItemResultMessageComposer + ) + ); + builder.MapSerializer( + typeof(CollectibleMintingEnabledMessageComposer), + new CollectibleMintingEnabledMessageComposerSerializer( + MessageComposer.CollectibleMintingEnabledMessageComposer + ) + ); + builder.MapSerializer( + typeof(CollectibleMintTokenCountMessageComposer), + new CollectibleMintTokenCountMessageComposerSerializer( + MessageComposer.CollectibleMintTokenCountMessageComposer + ) + ); + builder.MapSerializer( + typeof(CollectibleMintTokenOffersMessageComposer), + new CollectibleMintTokenOffersMessageComposerSerializer( + MessageComposer.CollectibleMintTokenOffersMessageComposer + ) + ); + builder.MapSerializer( + typeof(CollectibleWalletAddressesMessageComposer), + new CollectibleWalletAddressesMessageComposerSerializer( + MessageComposer.CollectibleWalletAddressesMessageComposer + ) + ); + builder.MapSerializer( + typeof(EmeraldBalanceMessageComposer), + new EmeraldBalanceMessageComposerSerializer( + MessageComposer.EmeraldBalanceMessageComposer + ) + ); + builder.MapSerializer( + typeof(NftBonusItemClaimResultMessageComposer), + new NftBonusItemClaimResultMessageComposerSerializer( + MessageComposer.NftBonusItemClaimResultMessageComposer + ) + ); + builder.MapSerializer( + typeof(NftCollectionsMessageComposer), + new NftCollectionsMessageComposerSerializer( + MessageComposer.NftCollectionsMessageComposer + ) + ); + builder.MapSerializer( + typeof(NftCollectionsScoreMessageComposer), + new NftCollectionsScoreMessageComposerSerializer( + MessageComposer.NftCollectionsScoreMessageComposer + ) + ); + builder.MapSerializer( + typeof(NftRewardItemClaimResultMessageComposer), + new NftRewardItemClaimResultMessageComposerSerializer( + MessageComposer.NftRewardItemClaimResultMessageComposer + ) + ); + builder.MapSerializer( + typeof(NftTransferAssetsResultMessageComposer), + new NftTransferAssetsResultMessageComposerSerializer( + MessageComposer.NftTransferAssetsResultMessageComposer + ) + ); + builder.MapSerializer( + typeof(NftTransferFeeMessageComposer), + new NftTransferFeeMessageComposerSerializer( + MessageComposer.NftTransferFeeMessageComposer + ) + ); + builder.MapSerializer( + typeof(SilverBalanceMessageComposer), + new SilverBalanceMessageComposerSerializer(MessageComposer.SilverBalanceMessageComposer) + ); + builder.MapSerializer( + typeof(UserNftChatStylesMessageComposer), + new UserNftChatStylesMessageComposerSerializer( + MessageComposer.UserNftChatStylesMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/CompetitionMap.cs b/Vortex.Revisions/Revision20260701/Maps/CompetitionMap.cs new file mode 100644 index 00000000..4f976f9d --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/CompetitionMap.cs @@ -0,0 +1,53 @@ +using Vortex.Primitives.Messages.Outgoing.Competition; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Competition; +using Vortex.Revisions.Revision20260701.Serializers.Competition; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class CompetitionMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.ForwardToACompetitionRoomMessageEvent, + new ForwardToACompetitionRoomMessageParser() + ); + builder.MapParser( + MessageEvent.ForwardToASubmittableRoomMessageEvent, + new ForwardToASubmittableRoomMessageParser() + ); + builder.MapParser( + MessageEvent.ForwardToRandomCompetitionRoomMessageEvent, + new ForwardToRandomCompetitionRoomMessageParser() + ); + builder.MapParser( + MessageEvent.GetCurrentTimingCodeMessageEvent, + new GetCurrentTimingCodeMessageParser() + ); + builder.MapParser( + MessageEvent.GetIsUserPartOfCompetitionMessageEvent, + new GetIsUserPartOfCompetitionMessageParser() + ); + builder.MapParser( + MessageEvent.GetSecondsUntilMessageEvent, + new GetSecondsUntilMessageParser() + ); + builder.MapParser( + MessageEvent.RoomCompetitionInitMessageEvent, + new RoomCompetitionInitMessageParser() + ); + builder.MapParser( + MessageEvent.SubmitRoomToCompetitionMessageEvent, + new SubmitRoomToCompetitionMessageParser() + ); + builder.MapParser(MessageEvent.VoteForRoomMessageEvent, new VoteForRoomMessageParser()); + + builder.MapSerializer( + typeof(CurrentTimingCodeMessageComposer), + new CurrentTimingCodeMessageComposerSerializer( + MessageComposer.CurrentTimingCodeMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/CraftingMap.cs b/Vortex.Revisions/Revision20260701/Maps/CraftingMap.cs new file mode 100644 index 00000000..b1baf909 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/CraftingMap.cs @@ -0,0 +1,25 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Crafting; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class CraftingMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.CraftEvent, new CraftMessageParser()); + builder.MapParser(MessageEvent.CraftSecretEvent, new CraftSecretMessageParser()); + builder.MapParser( + MessageEvent.GetCraftableProductsEvent, + new GetCraftableProductsMessageParser() + ); + builder.MapParser( + MessageEvent.GetCraftingRecipeEvent, + new GetCraftingRecipeMessageParser() + ); + builder.MapParser( + MessageEvent.GetCraftingRecipesAvailableEvent, + new GetCraftingRecipesAvailableMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/FriendFurniMap.cs b/Vortex.Revisions/Revision20260701/Maps/FriendFurniMap.cs new file mode 100644 index 00000000..8072e341 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/FriendFurniMap.cs @@ -0,0 +1,15 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.FriendFurni; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class FriendFurniMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.FriendFurniConfirmLockMessageEvent, + new FriendFurniConfirmLockMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/FriendListMap.cs b/Vortex.Revisions/Revision20260701/Maps/FriendListMap.cs new file mode 100644 index 00000000..7fad8f25 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/FriendListMap.cs @@ -0,0 +1,132 @@ +using Vortex.Primitives.Messages.Outgoing.FriendList; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Configuration; +using Vortex.Revisions.Revision20260701.Parsers.FriendList; +using Vortex.Revisions.Revision20260701.Serializers.FriendList; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class FriendListMap : IRevisionMap +{ + private readonly ProtocolLimitsConfig _protocolLimits; + + public FriendListMap(ProtocolLimitsConfig protocolLimits) + { + _protocolLimits = protocolLimits; + } + + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.AcceptFriendMessageEvent, new AcceptFriendMessageParser()); + builder.MapParser(MessageEvent.DeclineFriendMessageEvent, new DeclineFriendMessageParser()); + builder.MapParser( + MessageEvent.FindNewFriendsMessageEvent, + new FindNewFriendsMessageParser() + ); + builder.MapParser(MessageEvent.FollowFriendMessageEvent, new FollowFriendMessageParser()); + builder.MapParser( + MessageEvent.FriendListUpdateMessageEvent, + new FriendListUpdateMessageParser() + ); + builder.MapParser( + MessageEvent.GetFriendRequestsMessageEvent, + new GetFriendRequestsMessageParser() + ); + builder.MapParser( + MessageEvent.GetMessengerHistoryEvent, + new GetMessengerHistoryMessageParser() + ); + builder.MapParser(MessageEvent.HabboSearchMessageEvent, new HabboSearchMessageParser()); + builder.MapParser(MessageEvent.MessengerInitMessageEvent, new MessengerInitMessageParser()); + builder.MapParser( + MessageEvent.RemoveFriendMessageEvent, + new RemoveFriendMessageParser(_protocolLimits.MaxFriendRemovalIds) + ); + builder.MapParser(MessageEvent.RequestFriendMessageEvent, new RequestFriendMessageParser()); + builder.MapParser(MessageEvent.SendMsgMessageEvent, new SendMsgMessageParser()); + builder.MapParser( + MessageEvent.SendRoomInviteMessageEvent, + new SendRoomInviteMessageParser() + ); + builder.MapParser(MessageEvent.VisitUserMessageEvent, new VisitUserMessageParser()); + + builder.MapSerializer( + typeof(AcceptFriendResultMessageComposer), + new AcceptFriendResultMessageSerializer(MessageComposer.AcceptFriendResultComposer) + ); + builder.MapSerializer( + typeof(ConsoleMessageHistoryMessageComposer), + new ConsoleMessageHistoryMessageSerializer( + MessageComposer.ConsoleMessageHistoryComposer + ) + ); + builder.MapSerializer( + typeof(FollowFriendFailedMessageComposer), + new FollowFriendFailedMessageSerializer(MessageComposer.FollowFriendFailedComposer) + ); + builder.MapSerializer( + typeof(FriendListFragmentMessageComposer), + new FriendListFragmentMessageSerializer( + MessageComposer.FriendListFragmentMessageComposer + ) + ); + builder.MapSerializer( + typeof(FriendListUpdateMessageComposer), + new FriendListUpdateMessageSerializer(MessageComposer.FriendListUpdateComposer) + ); + builder.MapSerializer( + typeof(FriendNotificationMessageComposer), + new FriendNotificationMessageSerializer(MessageComposer.FriendNotificationComposer) + ); + builder.MapSerializer( + typeof(FriendRequestsMessageComposer), + new FriendRequestsMessageSerializer(MessageComposer.FriendRequestsComposer) + ); + builder.MapSerializer( + typeof(HabboSearchResultMessageComposer), + new HabboSearchResultMessageSerializer(MessageComposer.HabboSearchResultComposer) + ); + builder.MapSerializer( + typeof(InstantMessageErrorMessageComposer), + new InstantMessageErrorMessageSerializer(MessageComposer.InstantMessageErrorComposer) + ); + builder.MapSerializer( + typeof(MessengerErrorMessageComposer), + new MessengerErrorMessageSerializer(MessageComposer.MessengerErrorComposer) + ); + builder.MapSerializer( + typeof(MessengerInitMessageComposer), + new MessengerInitMessageSerializer(MessageComposer.MessengerInitComposer) + ); + builder.MapSerializer( + typeof(MiniMailNewMessageComposer), + new MiniMailNewMessageSerializer(MessageComposer.MiniMailNewMessageComposer) + ); + builder.MapSerializer( + typeof(MiniMailUnreadCountMessageComposer), + new MiniMailUnreadCountMessageSerializer(MessageComposer.MiniMailUnreadCountComposer) + ); + builder.MapSerializer( + typeof(NewConsoleMessageMessageComposer), + new NewConsoleMessageMessageSerializer(MessageComposer.NewConsoleMessageComposer) + ); + builder.MapSerializer( + typeof(NewFriendRequestMessageComposer), + new NewFriendRequestMessageSerializer(MessageComposer.NewFriendRequestComposer) + ); + builder.MapSerializer( + typeof(RoomInviteErrorMessageComposer), + new RoomInviteErrorMessageSerializer(MessageComposer.RoomInviteErrorComposer) + ); + builder.MapSerializer( + typeof(RoomInviteMessageComposer), + new RoomInviteMessageSerializer(MessageComposer.RoomInviteComposer) + ); + builder.MapSerializer( + typeof(FindFriendsProcessResultMessageComposer), + new FindFriendsProcessResultMessageComposerSerializer( + MessageComposer.FindFriendsProcessResultComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/GameMap.cs b/Vortex.Revisions/Revision20260701/Maps/GameMap.cs new file mode 100644 index 00000000..bfac4fd4 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/GameMap.cs @@ -0,0 +1,107 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Game.Arena; +using Vortex.Revisions.Revision20260701.Parsers.Game.Directory; +using Vortex.Revisions.Revision20260701.Parsers.Game.Ingame; +using Vortex.Revisions.Revision20260701.Parsers.Game.Lobby; +using Vortex.Revisions.Revision20260701.Parsers.Game.Score; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class GameMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + // Game Arena + builder.MapParser(MessageEvent.Game2ExitGameMessageEvent, new Game2ExitGameMessageParser()); + builder.MapParser(MessageEvent.Game2GameChatMessageEvent, new Game2GameChatMessageParser()); + builder.MapParser( + MessageEvent.Game2LoadStageReadyMessageEvent, + new Game2LoadStageReadyMessageParser() + ); + builder.MapParser( + MessageEvent.Game2PlayAgainMessageEvent, + new Game2PlayAgainMessageParser() + ); + + // Game Directory + builder.MapParser( + MessageEvent.Game2CheckGameDirectoryStatusMessageEvent, + new Game2CheckGameDirectoryStatusMessageParser() + ); + builder.MapParser( + MessageEvent.Game2GetAccountGameStatusMessageEvent, + new Game2GetAccountGameStatusMessageParser() + ); + builder.MapParser( + MessageEvent.Game2LeaveGameMessageEvent, + new Game2LeaveGameMessageParser() + ); + builder.MapParser( + MessageEvent.Game2QuickJoinGameMessageEvent, + new Game2QuickJoinGameMessageParser() + ); + builder.MapParser( + MessageEvent.Game2StartSnowWarMessageEvent, + new Game2StartSnowWarMessageParser() + ); + + // Game Ingame + builder.MapParser( + MessageEvent.Game2MakeSnowballMessageEvent, + new Game2MakeSnowballMessageParser() + ); + builder.MapParser( + MessageEvent.Game2RequestFullStatusUpdateMessageEvent, + new Game2RequestFullStatusUpdateMessageParser() + ); + builder.MapParser( + MessageEvent.Game2SetUserMoveTargetMessageEvent, + new Game2SetUserMoveTargetMessageParser() + ); + builder.MapParser( + MessageEvent.Game2ThrowSnowballAtHumanMessageEvent, + new Game2ThrowSnowballAtHumanMessageParser() + ); + builder.MapParser( + MessageEvent.Game2ThrowSnowballAtPositionMessageEvent, + new Game2ThrowSnowballAtPositionMessageParser() + ); + + // Game Lobby + builder.MapParser( + MessageEvent.GetResolutionAchievementsMessageEvent, + new GetResolutionAchievementsMessageParser() + ); + // builder.MapParser(MessageEvent.GetUserGameAchievementsMessageEvent, new GetUserGameAchievementsMessageParser()); + + // Game Score + builder.MapParser( + MessageEvent.Game2GetFriendsLeaderboardEvent, + new Game2GetFriendsLeaderboardMessageParser() + ); + builder.MapParser( + MessageEvent.Game2GetTotalGroupLeaderboardEvent, + new Game2GetTotalGroupLeaderboardMessageParser() + ); + builder.MapParser( + MessageEvent.Game2GetTotalLeaderboardEvent, + new Game2GetTotalLeaderboardMessageParser() + ); + builder.MapParser( + MessageEvent.Game2GetWeeklyFriendsLeaderboardEvent, + new Game2GetWeeklyFriendsLeaderboardMessageParser() + ); + builder.MapParser( + MessageEvent.Game2GetWeeklyGroupLeaderboardEvent, + new Game2GetWeeklyGroupLeaderboardMessageParser() + ); + builder.MapParser( + MessageEvent.Game2GetWeeklyLeaderboardEvent, + new Game2GetWeeklyLeaderboardMessageParser() + ); + // builder.MapParser(MessageEvent.GetFriendsWeeklyCompetitiveLeaderboardMessageEvent, new GetFriendsWeeklyCompetitiveLeaderboardMessageParser()); + // builder.MapParser(MessageEvent.GetWeeklyCompetitiveLeaderboardMessageEvent, new GetWeeklyCompetitiveLeaderboardMessageParser()); + // builder.MapParser(MessageEvent.GetWeeklyGameRewardMessageEvent, new GetWeeklyGameRewardMessageParser()); + // builder.MapParser(MessageEvent.GetWeeklyGameRewardWinnersMessageEvent, new GetWeeklyGameRewardWinnersMessageParser()); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/GiftsMap.cs b/Vortex.Revisions/Revision20260701/Maps/GiftsMap.cs new file mode 100644 index 00000000..b41de858 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/GiftsMap.cs @@ -0,0 +1,24 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Gifts; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class GiftsMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.ResetPhoneNumberStateMessageEvent, + new ResetPhoneNumberStateMessageParser() + ); + builder.MapParser( + MessageEvent.SetPhoneNumberVerificationStatusMessageEvent, + new SetPhoneNumberVerificationStatusMessageParser() + ); + builder.MapParser( + MessageEvent.TryPhoneNumberMessageEvent, + new TryPhoneNumberMessageParser() + ); + builder.MapParser(MessageEvent.VerifyCodeMessageEvent, new VerifyCodeMessageParser()); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/GroupForumsMap.cs b/Vortex.Revisions/Revision20260701/Maps/GroupForumsMap.cs new file mode 100644 index 00000000..d42fd5bc --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/GroupForumsMap.cs @@ -0,0 +1,81 @@ +using Vortex.Primitives.Messages.Outgoing.Groupforums; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.GroupForums; +using Vortex.Revisions.Revision20260701.Serializers.GroupForums; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class GroupForumsMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.GetForumsListMessageEvent, new GetForumsListMessageParser()); + builder.MapParser(MessageEvent.GetForumStatsMessageEvent, new GetForumStatsMessageParser()); + builder.MapParser(MessageEvent.GetMessagesMessageEvent, new GetMessagesMessageParser()); + builder.MapParser(MessageEvent.GetThreadMessageEvent, new GetThreadMessageParser()); + builder.MapParser(MessageEvent.GetThreadsMessageEvent, new GetThreadsMessageParser()); + builder.MapParser( + MessageEvent.GetUnreadForumsCountMessageEvent, + new GetUnreadForumsCountMessageParser() + ); + builder.MapParser( + MessageEvent.ModerateMessageMessageEvent, + new ModerateMessageMessageParser() + ); + builder.MapParser( + MessageEvent.ModerateThreadMessageEvent, + new ModerateThreadMessageParser() + ); + builder.MapParser(MessageEvent.PostMessageMessageEvent, new PostMessageMessageParser()); + builder.MapParser( + MessageEvent.UpdateForumReadMarkerMessageEvent, + new UpdateForumReadMarkerMessageParser() + ); + builder.MapParser( + MessageEvent.UpdateForumSettingsMessageEvent, + new UpdateForumSettingsMessageParser() + ); + builder.MapParser(MessageEvent.UpdateThreadMessageEvent, new UpdateThreadMessageParser()); + + builder.MapSerializer( + typeof(UnreadForumsCountMessageComposer), + new UnreadForumsCountMessageComposerSerializer( + MessageComposer.UnreadForumsCountMessageComposer + ) + ); + builder.MapSerializer( + typeof(ForumsListMessageComposer), + new ForumsListMessageComposerSerializer(MessageComposer.ForumsListMessageComposer) + ); + builder.MapSerializer( + typeof(ForumDataMessageComposer), + new ForumDataMessageComposerSerializer(MessageComposer.ForumDataMessageComposer) + ); + builder.MapSerializer( + typeof(ForumThreadsMessageComposer), + new ForumThreadsMessageComposerSerializer(MessageComposer.ForumThreadsMessageComposer) + ); + builder.MapSerializer( + typeof(ThreadMessagesMessageComposer), + new ThreadMessagesMessageComposerSerializer( + MessageComposer.ThreadMessagesMessageComposer + ) + ); + builder.MapSerializer( + typeof(PostThreadMessageComposer), + new PostThreadMessageComposerSerializer(MessageComposer.PostThreadMessageComposer) + ); + builder.MapSerializer( + typeof(PostMessageMessageComposer), + new PostMessageMessageComposerSerializer(MessageComposer.PostMessageMessageComposer) + ); + builder.MapSerializer( + typeof(UpdateThreadMessageComposer), + new UpdateThreadMessageComposerSerializer(MessageComposer.UpdateThreadMessageComposer) + ); + builder.MapSerializer( + typeof(UpdateMessageMessageComposer), + new UpdateMessageMessageComposerSerializer(MessageComposer.UpdateMessageMessageComposer) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/HandshakeMap.cs b/Vortex.Revisions/Revision20260701/Maps/HandshakeMap.cs new file mode 100644 index 00000000..fb484ad3 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/HandshakeMap.cs @@ -0,0 +1,71 @@ +using Vortex.Primitives.Messages.Outgoing.Handshake; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Handshake; +using Vortex.Revisions.Revision20260701.Serializers.Handshake; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class HandshakeMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.ClientHelloMessageEvent, new ClientHelloMessageParser()); + builder.MapParser( + MessageEvent.CompleteDiffieHandshakeMessageEvent, + new CompleteDiffieHandshakeMessageParser() + ); + builder.MapParser(MessageEvent.DisconnectMessageEvent, new DisconnectMessageParser()); + builder.MapParser(MessageEvent.InfoRetrieveMessageEvent, new InfoRetrieveMessageParser()); + builder.MapParser( + MessageEvent.InitDiffieHandshakeMessageEvent, + new InitDiffieHandshakeMessageParser() + ); + builder.MapParser(MessageEvent.PongMessageEvent, new PongMessageParser()); + builder.MapParser(MessageEvent.SSOTicketMessageEvent, new SSOTicketMessageParser()); + builder.MapParser(MessageEvent.UniqueIDMessageEvent, new UniqueIdMessageParser()); + builder.MapParser(MessageEvent.VersionCheckMessageEvent, new VersionCheckMessageParser()); + + builder.MapSerializer( + typeof(AuthenticationOKMessage), + new AuthenticationOKMessageSerializer(MessageComposer.AuthenticationOKMessageComposer) + ); + builder.MapSerializer( + typeof(CompleteDiffieHandshakeMessageComposer), + new CompleteDiffieHandshakeMessageSerializer( + MessageComposer.CompleteDiffieHandshakeComposer + ) + ); + builder.MapSerializer( + typeof(GenericErrorMessage), + new GenericErrorMessageSerializer(MessageComposer.GenericErrorComposer) + ); + builder.MapSerializer( + typeof(InitDiffieHandshakeMessageComposer), + new InitDiffieHandshakeMessageSerializer(MessageComposer.InitDiffieHandshakeComposer) + ); + builder.MapSerializer( + typeof(IsFirstLoginOfDayMessage), + new IsFirstLoginOfDayMessageSerializer(MessageComposer.IsFirstLoginOfDayComposer) + ); + builder.MapSerializer( + typeof(NoobnessLevelMessage), + new NoobnessLevelMessageSerializer(MessageComposer.NoobnessLevelMessageComposer) + ); + builder.MapSerializer( + typeof(PingMessage), + new PingMessageSerializer(MessageComposer.PingMessageComposer) + ); + builder.MapSerializer( + typeof(UniqueMachineIdMessage), + new UniqueMachineIdMessageSerializer(MessageComposer.UniqueMachineIDComposer) + ); + builder.MapSerializer( + typeof(UserObjectMessage), + new UserObjectMessageSerializer(MessageComposer.UserObjectComposer) + ); + builder.MapSerializer( + typeof(UserRightsMessage), + new UserRightsMessageSerializer(MessageComposer.UserRightsMessageComposer) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/HelpMap.cs b/Vortex.Revisions/Revision20260701/Maps/HelpMap.cs new file mode 100644 index 00000000..1c7019eb --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/HelpMap.cs @@ -0,0 +1,107 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Help; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class HelpMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.CallForHelpFromForumMessageMessageEvent, + new CallForHelpFromForumMessageMessageParser() + ); + builder.MapParser( + MessageEvent.CallForHelpFromForumThreadMessageEvent, + new CallForHelpFromForumThreadMessageParser() + ); + builder.MapParser( + MessageEvent.CallForHelpFromIMMessageEvent, + new CallForHelpFromIMMessageParser() + ); + builder.MapParser( + MessageEvent.CallForHelpFromPhotoMessageEvent, + new CallForHelpFromPhotoMessageParser() + ); + builder.MapParser( + MessageEvent.CallForHelpFromSelfieMessageEvent, + new CallForHelpFromSelfieMessageParser() + ); + builder.MapParser(MessageEvent.CallForHelpMessageEvent, new CallForHelpMessageParser()); + builder.MapParser( + MessageEvent.ChatReviewGuideDecidesOnOfferMessageEvent, + new ChatReviewGuideDecidesOnOfferMessageParser() + ); + builder.MapParser( + MessageEvent.ChatReviewGuideDetachedMessageEvent, + new ChatReviewGuideDetachedMessageParser() + ); + builder.MapParser( + MessageEvent.ChatReviewGuideVoteMessageEvent, + new ChatReviewGuideVoteMessageParser() + ); + builder.MapParser( + MessageEvent.ChatReviewSessionCreateMessageEvent, + new ChatReviewSessionCreateMessageParser() + ); + builder.MapParser( + MessageEvent.DeletePendingCallsForHelpMessageEvent, + new DeletePendingCallsForHelpMessageParser() + ); + builder.MapParser(MessageEvent.GetCfhStatusMessageEvent, new GetCfhStatusMessageParser()); + builder.MapParser( + MessageEvent.GetGuideReportingStatusMessageEvent, + new GetGuideReportingStatusMessageParser() + ); + builder.MapParser( + MessageEvent.GetPendingCallsForHelpMessageEvent, + new GetPendingCallsForHelpMessageParser() + ); + builder.MapParser(MessageEvent.GetQuizQuestionsEvent, new GetQuizQuestionsMessageParser()); + builder.MapParser( + MessageEvent.GuideSessionCreateMessageEvent, + new GuideSessionCreateMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionFeedbackMessageEvent, + new GuideSessionFeedbackMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionGetRequesterRoomMessageEvent, + new GuideSessionGetRequesterRoomMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionGuideDecidesMessageEvent, + new GuideSessionGuideDecidesMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionInviteRequesterMessageEvent, + new GuideSessionInviteRequesterMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionIsTypingMessageEvent, + new GuideSessionIsTypingMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionMessageMessageEvent, + new GuideSessionMessageMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionOnDutyUpdateMessageEvent, + new GuideSessionOnDutyUpdateMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionReportMessageEvent, + new GuideSessionReportMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionRequesterCancelsMessageEvent, + new GuideSessionRequesterCancelsMessageParser() + ); + builder.MapParser( + MessageEvent.GuideSessionResolvedMessageEvent, + new GuideSessionResolvedMessageParser() + ); + builder.MapParser(MessageEvent.PostQuizAnswersEvent, new PostQuizAnswersMessageParser()); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/HotlooksMap.cs b/Vortex.Revisions/Revision20260701/Maps/HotlooksMap.cs new file mode 100644 index 00000000..99ceb336 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/HotlooksMap.cs @@ -0,0 +1,12 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Hotlooks; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class HotlooksMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.GetHotLooksMessageEvent, new GetHotLooksMessageParser()); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/InventoryMap.cs b/Vortex.Revisions/Revision20260701/Maps/InventoryMap.cs new file mode 100644 index 00000000..7c4d0b05 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/InventoryMap.cs @@ -0,0 +1,377 @@ +using Vortex.Primitives.Messages.Outgoing.Inventory.Achievements; +using Vortex.Primitives.Messages.Outgoing.Inventory.Avatareffect; +using Vortex.Primitives.Messages.Outgoing.Inventory.Badges; +using Vortex.Primitives.Messages.Outgoing.Inventory.Bots; +using Vortex.Primitives.Messages.Outgoing.Inventory.Clothing; +using Vortex.Primitives.Messages.Outgoing.Inventory.Furni; +using Vortex.Primitives.Messages.Outgoing.Inventory.Pets; +using Vortex.Primitives.Messages.Outgoing.Inventory.Purse; +using Vortex.Primitives.Messages.Outgoing.Inventory.Trading; +using Vortex.Primitives.Messages.Outgoing.Users; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Configuration; +using Vortex.Revisions.Revision20260701.Parsers.Inventory.Achievements; +using Vortex.Revisions.Revision20260701.Parsers.Inventory.Avatareffect; +using Vortex.Revisions.Revision20260701.Parsers.Inventory.Badges; +using Vortex.Revisions.Revision20260701.Parsers.Inventory.Bots; +using Vortex.Revisions.Revision20260701.Parsers.Inventory.Furni; +using Vortex.Revisions.Revision20260701.Parsers.Inventory.Pets; +using Vortex.Revisions.Revision20260701.Parsers.Inventory.Purse; +using Vortex.Revisions.Revision20260701.Parsers.Inventory.Trading; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Achievements; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Avatareffect; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Badges; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Bots; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Clothing; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Furni; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Pets; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Purse; +using Vortex.Revisions.Revision20260701.Serializers.Inventory.Trading; +using Vortex.Revisions.Revision20260701.Serializers.Users; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class InventoryMap : IRevisionMap +{ + private readonly ProtocolLimitsConfig _protocolLimits; + + public InventoryMap(ProtocolLimitsConfig protocolLimits) + { + _protocolLimits = protocolLimits; + } + + public void RegisterInto(IRevisionMapBuilder builder) + { + // Inventory Achievements + builder.MapParser(MessageEvent.GetAchievementsEvent, new GetAchievementsMessageParser()); + + // Inventory Avatar Effects + builder.MapParser( + MessageEvent.AvatarEffectActivatedEvent, + new AvatarEffectActivatedMessageParser() + ); + builder.MapParser( + MessageEvent.AvatarEffectSelectedEvent, + new AvatarEffectSelectedMessageParser() + ); + + // Inventory Badges + builder.MapParser( + MessageEvent.GetBadgePointLimitsEvent, + new GetBadgePointLimitsMessageParser() + ); + builder.MapParser(MessageEvent.GetBadgesEvent, new GetBadgesMessageParser()); + builder.MapParser( + MessageEvent.GetIsBadgeRequestFulfilledEvent, + new GetIsBadgeRequestFulfilledMessageParser() + ); + builder.MapParser(MessageEvent.RequestABadgeEvent, new RequestABadgeMessageParser()); + builder.MapParser( + MessageEvent.SetActivatedBadgesEvent, + new SetActivatedBadgesMessageParser() + ); + + // Inventory Bots + builder.MapParser(MessageEvent.GetBotInventoryEvent, new GetBotInventoryMessageParser()); + + // Inventory Furni + builder.MapParser( + MessageEvent.RequestFurniInventoryEvent, + new RequestFurniInventoryMessageParser() + ); + builder.MapParser( + MessageEvent.RequestFurniInventoryWhenNotInRoomEvent, + new RequestFurniInventoryWhenNotInRoomMessageParser() + ); + builder.MapParser( + MessageEvent.RequestRoomPropertySet, + new RequestRoomPropertySetMessageParser() + ); + + // Inventory Pets + builder.MapParser( + MessageEvent.CancelPetBreedingEvent, + new CancelPetBreedingMessageParser() + ); + builder.MapParser( + MessageEvent.ConfirmPetBreedingEvent, + new ConfirmPetBreedingMessageParser() + ); + builder.MapParser(MessageEvent.GetPetInventoryEvent, new GetPetInventoryMessageParser()); + + // Inventory Purse + builder.MapParser(MessageEvent.GetCreditsInfoEvent, new GetCreditsInfoMessageParser()); + + // Inventory Trading + builder.MapParser(MessageEvent.AcceptTradingEvent, new AcceptTradingMessageParser()); + builder.MapParser( + MessageEvent.AddItemsToTradeEvent, + new AddItemsToTradeMessageParser(_protocolLimits.MaxTradeItems) + ); + builder.MapParser(MessageEvent.AddItemToTradeEvent, new AddItemToTradeMessageParser()); + builder.MapParser(MessageEvent.CloseTradingEvent, new CloseTradingMessageParser()); + builder.MapParser( + MessageEvent.ConfirmAcceptTradingEvent, + new ConfirmAcceptTradingMessageParser() + ); + builder.MapParser( + MessageEvent.ConfirmDeclineTradingEvent, + new ConfirmDeclineTradingMessageParser() + ); + builder.MapParser(MessageEvent.OpenTradingEvent, new OpenTradingMessageParser()); + builder.MapParser( + MessageEvent.RemoveItemFromTradeEvent, + new RemoveItemFromTradeMessageParser() + ); + builder.MapParser(MessageEvent.SilverFeeMessageEvent, new SilverFeeMessageParser()); + builder.MapParser(MessageEvent.UnacceptTradingEvent, new UnacceptTradingMessageParser()); + + // Inventory Achievements + builder.MapSerializer( + typeof(AchievementEventMessageComposer), + new AchievementEventMessageComposerSerializer(MessageComposer.AchievementComposer) + ); + builder.MapSerializer( + typeof(AchievementsEventMessageComposer), + new AchievementsEventMessageComposerSerializer(MessageComposer.AchievementsComposer) + ); + builder.MapSerializer( + typeof(AchievementsScoreEventMessageComposer), + new AchievementsScoreEventMessageComposerSerializer( + MessageComposer.AchievementsScoreComposer + ) + ); + + // Inventory Avatareffect + builder.MapSerializer( + typeof(AvatarEffectActivatedMessageComposer), + new AvatarEffectActivatedMessageComposerSerializer( + MessageComposer.AvatarEffectActivatedMessageComposer + ) + ); + builder.MapSerializer( + typeof(AvatarEffectAddedMessageComposer), + new AvatarEffectAddedMessageComposerSerializer( + MessageComposer.AvatarEffectAddedMessageComposer + ) + ); + builder.MapSerializer( + typeof(AvatarEffectExpiredMessageComposer), + new AvatarEffectExpiredMessageComposerSerializer( + MessageComposer.AvatarEffectExpiredMessageComposer + ) + ); + builder.MapSerializer( + typeof(AvatarEffectSelectedMessageComposer), + new AvatarEffectSelectedMessageComposerSerializer( + MessageComposer.AvatarEffectSelectedMessageComposer + ) + ); + builder.MapSerializer( + typeof(AvatarEffectsMessageComposer), + new AvatarEffectsMessageComposerSerializer(MessageComposer.AvatarEffectsMessageComposer) + ); + + // Inventory Badges + builder.MapSerializer( + typeof(BadgePointLimitsEventMessageComposer), + new BadgePointLimitsEventMessageComposerSerializer( + MessageComposer.BadgePointLimitsComposer + ) + ); + builder.MapSerializer( + typeof(BadgeReceivedEventMessageComposer), + new BadgeReceivedEventMessageComposerSerializer(MessageComposer.BadgeReceivedComposer) + ); + builder.MapSerializer( + typeof(BadgesEventMessageComposer), + new BadgesEventMessageComposerSerializer(MessageComposer.BadgesComposer) + ); + builder.MapSerializer( + typeof(HabboUserBadgesMessageComposer), + new HabboUserBadgesMessageComposerSerializer( + MessageComposer.HabboUserBadgesMessageComposer + ) + ); + builder.MapSerializer( + typeof(IsBadgeRequestFulfilledEventMessageComposer), + new IsBadgeRequestFulfilledEventMessageComposerSerializer( + MessageComposer.IsBadgeRequestFulfilledComposer + ) + ); + + // Inventory Bots + builder.MapSerializer( + typeof(BotAddedToInventoryEventMessageComposer), + new BotAddedToInventoryEventMessageComposerSerializer( + MessageComposer.BotAddedToInventoryComposer + ) + ); + builder.MapSerializer( + typeof(BotInventoryEventMessageComposer), + new BotInventoryEventMessageComposerSerializer(MessageComposer.BotInventoryComposer) + ); + builder.MapSerializer( + typeof(BotRemovedFromInventoryEventMessageComposer), + new BotRemovedFromInventoryEventMessageComposerSerializer( + MessageComposer.BotRemovedFromInventoryComposer + ) + ); + + // Inventory Clothing + builder.MapSerializer( + typeof(FigureSetIdsEventMessageComposer), + new FigureSetIdsEventMessageComposerSerializer(MessageComposer.FigureSetIdsComposer) + ); + + // Inventory Furni + builder.MapSerializer( + typeof(FurniListAddOrUpdateEventMessageComposer), + new FurniListAddOrUpdateEventMessageComposerSerializer( + MessageComposer.FurniListAddOrUpdateComposer + ) + ); + builder.MapSerializer( + typeof(FurniListEventMessageComposer), + new FurniListEventMessageComposerSerializer(MessageComposer.FurniListComposer) + ); + builder.MapSerializer( + typeof(FurniListInvalidateEventMessageComposer), + new FurniListInvalidateEventMessageComposerSerializer( + MessageComposer.FurniListInvalidateComposer + ) + ); + builder.MapSerializer( + typeof(FurniListRemoveEventMessageComposer), + new FurniListRemoveEventMessageComposerSerializer( + MessageComposer.FurniListRemoveComposer + ) + ); + builder.MapSerializer( + typeof(PostItPlacedEventMessageComposer), + new PostItPlacedEventMessageComposerSerializer(MessageComposer.PostItPlacedComposer) + ); + + // Inventory Pets + builder.MapSerializer( + typeof(ConfirmBreedingRequestEventMessageComposer), + new ConfirmBreedingRequestEventMessageComposerSerializer( + MessageComposer.ConfirmBreedingRequestComposer + ) + ); + builder.MapSerializer( + typeof(ConfirmBreedingResultEventMessageComposer), + new ConfirmBreedingResultEventMessageComposerSerializer( + MessageComposer.ConfirmBreedingResultComposer + ) + ); + builder.MapSerializer( + typeof(GoToBreedingNestFailureEventMessageComposer), + new GoToBreedingNestFailureEventMessageComposerSerializer( + MessageComposer.GoToBreedingNestFailureComposer + ) + ); + builder.MapSerializer( + typeof(NestBreedingSuccessEventMessageComposer), + new NestBreedingSuccessEventMessageComposerSerializer( + MessageComposer.NestBreedingSuccessComposer + ) + ); + builder.MapSerializer( + typeof(PetAddedToInventoryEventMessageComposer), + new PetAddedToInventoryEventMessageComposerSerializer( + MessageComposer.PetAddedToInventoryComposer + ) + ); + builder.MapSerializer( + typeof(PetBreedingEventMessageComposer), + new PetBreedingEventMessageComposerSerializer(MessageComposer.PetBreedingComposer) + ); + builder.MapSerializer( + typeof(PetInventoryEventMessageComposer), + new PetInventoryEventMessageComposerSerializer(MessageComposer.PetInventoryComposer) + ); + builder.MapSerializer( + typeof(PetReceivedMessageComposer), + new PetReceivedMessageComposerSerializer(MessageComposer.PetReceivedMessageComposer) + ); + builder.MapSerializer( + typeof(PetRemovedFromInventoryEventMessageComposer), + new PetRemovedFromInventoryEventMessageComposerSerializer( + MessageComposer.PetRemovedFromInventoryComposer + ) + ); + + // Inventory Purse + builder.MapSerializer( + typeof(CreditBalanceEventMessageComposer), + new CreditBalanceEventMessageComposerSerializer(MessageComposer.CreditBalanceComposer) + ); + + // Inventory Trading + builder.MapSerializer( + typeof(TradeOpenFailedEventPaserMessageComposer), + new TradeOpenFailedEventPaserMessageComposerSerializer( + MessageComposer.TradeOpenFailedComposer + ) + ); + builder.MapSerializer( + typeof(TradeSilverFeeMessageComposer), + new TradeSilverFeeMessageComposerSerializer( + MessageComposer.TradeSilverFeeMessageComposer + ) + ); + builder.MapSerializer( + typeof(TradeSilverSetMessageComposer), + new TradeSilverSetMessageComposerSerializer( + MessageComposer.TradeSilverSetMessageComposer + ) + ); + builder.MapSerializer( + typeof(TradingAcceptEventMessageComposer), + new TradingAcceptEventMessageComposerSerializer(MessageComposer.TradingAcceptComposer) + ); + builder.MapSerializer( + typeof(TradingCloseEventMessageComposer), + new TradingCloseEventMessageComposerSerializer(MessageComposer.TradingCloseComposer) + ); + builder.MapSerializer( + typeof(TradingCompletedEventMessageComposer), + new TradingCompletedEventMessageComposerSerializer( + MessageComposer.TradingCompletedComposer + ) + ); + builder.MapSerializer( + typeof(TradingConfirmationEventMessageComposer), + new TradingConfirmationEventMessageComposerSerializer( + MessageComposer.TradingConfirmationComposer + ) + ); + builder.MapSerializer( + typeof(TradingItemListEventMessageComposer), + new TradingItemListEventMessageComposerSerializer( + MessageComposer.TradingItemListComposer + ) + ); + builder.MapSerializer( + typeof(TradingNotOpenEventMessageComposer), + new TradingNotOpenEventMessageComposerSerializer(MessageComposer.TradingNotOpenComposer) + ); + builder.MapSerializer( + typeof(TradingOpenEventMessageComposer), + new TradingOpenEventMessageComposerSerializer(MessageComposer.TradingOpenComposer) + ); + builder.MapSerializer( + typeof(TradingOtherNotAllowedEventMessageComposer), + new TradingOtherNotAllowedEventMessageComposerSerializer( + MessageComposer.TradingOtherNotAllowedComposer + ) + ); + builder.MapSerializer( + typeof(TradingYouAreNotAllowedEventMessageComposer), + new TradingYouAreNotAllowedEventMessageComposerSerializer( + MessageComposer.TradingYouAreNotAllowedComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/LandingViewMap.cs b/Vortex.Revisions/Revision20260701/Maps/LandingViewMap.cs new file mode 100644 index 00000000..c0afc784 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/LandingViewMap.cs @@ -0,0 +1,20 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.LandingView; +using Vortex.Revisions.Revision20260701.Parsers.LandingView.Votes; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class LandingViewMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.CommunityGoalVoteMessageEvent, + new CommunityGoalVoteMessageParser() + ); + builder.MapParser( + MessageEvent.GetPromoArticlesMessageEvent, + new GetPromoArticlesMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/MarketplaceMap.cs b/Vortex.Revisions/Revision20260701/Maps/MarketplaceMap.cs new file mode 100644 index 00000000..5877ba9a --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/MarketplaceMap.cs @@ -0,0 +1,99 @@ +using Vortex.Primitives.Messages.Outgoing.Marketplace; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Marketplace; +using Vortex.Revisions.Revision20260701.Serializers.Marketplace; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class MarketplaceMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.BuyMarketplaceOfferMessageEvent, + new BuyMarketplaceOfferMessageParser() + ); + builder.MapParser( + MessageEvent.BuyMarketplaceTokensMessageEvent, + new BuyMarketplaceTokensMessageParser() + ); + builder.MapParser( + MessageEvent.CancelMarketplaceOfferMessageEvent, + new CancelMarketplaceOfferMessageParser() + ); + builder.MapParser( + MessageEvent.GetMarketplaceCanMakeOfferMessageEvent, + new GetMarketplaceCanMakeOfferMessageParser() + ); + builder.MapParser( + MessageEvent.GetMarketplaceConfigurationMessageEvent, + new GetMarketplaceConfigurationMessageParser() + ); + builder.MapParser( + MessageEvent.GetMarketplaceItemStatsEvent, + new GetMarketplaceItemStatsMessageParser() + ); + builder.MapParser( + MessageEvent.GetMarketplaceOffersMessageEvent, + new GetMarketplaceOffersMessageParser() + ); + builder.MapParser( + MessageEvent.GetMarketplaceOwnOffersMessageEvent, + new GetMarketplaceOwnOffersMessageParser() + ); + builder.MapParser(MessageEvent.MakeOfferMessageEvent, new MakeOfferMessageParser()); + builder.MapParser( + MessageEvent.RedeemMarketplaceOfferCreditsMessageEvent, + new RedeemMarketplaceOfferCreditsMessageParser() + ); + + builder.MapSerializer( + typeof(MarketplaceBuyOfferResultEventMessageComposer), + new MarketplaceBuyOfferResultEventMessageComposerSerializer( + MessageComposer.MarketplaceBuyOfferResultComposer + ) + ); + builder.MapSerializer( + typeof(MarketplaceCancelOfferResultEventMessageComposer), + new MarketplaceCancelOfferResultEventMessageComposerSerializer( + MessageComposer.MarketplaceCancelOfferResultComposer + ) + ); + builder.MapSerializer( + typeof(MarketplaceCanMakeOfferResultMessageComposer), + new MarketplaceCanMakeOfferResultMessageComposerSerializer( + MessageComposer.MarketplaceCanMakeOfferResult + ) + ); + builder.MapSerializer( + typeof(MarketplaceConfigurationEventMessageComposer), + new MarketplaceConfigurationEventMessageComposerSerializer( + MessageComposer.MarketplaceConfigurationComposer + ) + ); + builder.MapSerializer( + typeof(MarketplaceItemStatsEventMessageComposer), + new MarketplaceItemStatsEventMessageComposerSerializer( + MessageComposer.MarketplaceItemStatsComposer + ) + ); + builder.MapSerializer( + typeof(MarketplaceMakeOfferResultMessageComposer), + new MarketplaceMakeOfferResultMessageComposerSerializer( + MessageComposer.MarketplaceMakeOfferResult + ) + ); + builder.MapSerializer( + typeof(MarketPlaceOffersEventMessageComposer), + new MarketPlaceOffersEventMessageComposerSerializer( + MessageComposer.MarketPlaceOffersComposer + ) + ); + builder.MapSerializer( + typeof(MarketPlaceOwnOffersEventMessageComposer), + new MarketPlaceOwnOffersEventMessageComposerSerializer( + MessageComposer.MarketPlaceOwnOffersComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/ModeratorMap.cs b/Vortex.Revisions/Revision20260701/Maps/ModeratorMap.cs new file mode 100644 index 00000000..31e77546 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/ModeratorMap.cs @@ -0,0 +1,59 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Moderator; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class ModeratorMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.CloseIssueDefaultActionMessageEvent, + new CloseIssueDefaultActionMessageParser() + ); + builder.MapParser(MessageEvent.CloseIssuesMessageEvent, new CloseIssuesMessageParser()); + builder.MapParser( + MessageEvent.DefaultSanctionMessageEvent, + new DefaultSanctionMessageParser() + ); + builder.MapParser(MessageEvent.GetCfhChatlogMessageEvent, new GetCfhChatlogMessageParser()); + builder.MapParser( + MessageEvent.GetModeratorRoomInfoMessageEvent, + new GetModeratorRoomInfoMessageParser() + ); + builder.MapParser( + MessageEvent.GetModeratorUserInfoMessageEvent, + new GetModeratorUserInfoMessageParser() + ); + builder.MapParser( + MessageEvent.GetRoomChatlogMessageEvent, + new GetRoomChatlogMessageParser() + ); + builder.MapParser(MessageEvent.GetRoomVisitsMessageEvent, new GetRoomVisitsMessageParser()); + builder.MapParser( + MessageEvent.GetUserChatlogMessageEvent, + new GetUserChatlogMessageParser() + ); + builder.MapParser(MessageEvent.ModAlertMessageEvent, new ModAlertMessageParser()); + builder.MapParser(MessageEvent.ModBanMessageEvent, new ModBanMessageParser()); + builder.MapParser(MessageEvent.ModerateRoomMessageEvent, new ModerateRoomMessageParser()); + builder.MapParser( + MessageEvent.ModeratorActionMessageEvent, + new ModeratorActionMessageParser() + ); + builder.MapParser(MessageEvent.ModKickMessageEvent, new ModKickMessageParser()); + builder.MapParser(MessageEvent.ModMessageMessageEvent, new ModMessageMessageParser()); + builder.MapParser(MessageEvent.ModMuteMessageEvent, new ModMuteMessageParser()); + builder.MapParser( + MessageEvent.ModToolPreferencesEvent, + new ModToolPreferencesMessageParser() + ); + builder.MapParser(MessageEvent.ModToolSanctionEvent, new ModToolSanctionMessageParser()); + builder.MapParser( + MessageEvent.ModTradingLockMessageEvent, + new ModTradingLockMessageParser() + ); + builder.MapParser(MessageEvent.PickIssuesMessageEvent, new PickIssuesMessageParser()); + builder.MapParser(MessageEvent.ReleaseIssuesMessageEvent, new ReleaseIssuesMessageParser()); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/MysteryBoxMap.cs b/Vortex.Revisions/Revision20260701/Maps/MysteryBoxMap.cs new file mode 100644 index 00000000..a9296c49 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/MysteryBoxMap.cs @@ -0,0 +1,42 @@ +using Vortex.Primitives.Messages.Outgoing.Mysterybox; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.MysteryBox; +using Vortex.Revisions.Revision20260701.Serializers.MysteryBox; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class MysteryBoxMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.MysteryBoxWaitingCanceledMessageEvent, + new MysteryBoxWaitingCanceledMessageParser() + ); + + builder.MapSerializer( + typeof(CancelMysteryBoxWaitMessageComposer), + new CancelMysteryBoxWaitMessageComposerSerializer( + MessageComposer.CancelMysteryBoxWaitMessageComposer + ) + ); + builder.MapSerializer( + typeof(GotMysteryBoxPrizeMessageComposer), + new GotMysteryBoxPrizeMessageComposerSerializer( + MessageComposer.GotMysteryBoxPrizeMessageComposer + ) + ); + builder.MapSerializer( + typeof(MysteryBoxKeysMessageComposer), + new MysteryBoxKeysMessageComposerSerializer( + MessageComposer.MysteryBoxKeysMessageComposer + ) + ); + builder.MapSerializer( + typeof(ShowMysteryBoxWaitMessageComposer), + new ShowMysteryBoxWaitMessageComposerSerializer( + MessageComposer.ShowMysteryBoxWaitMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/NavigatorMap.cs b/Vortex.Revisions/Revision20260701/Maps/NavigatorMap.cs new file mode 100644 index 00000000..31479d95 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/NavigatorMap.cs @@ -0,0 +1,234 @@ +using Vortex.Primitives.Messages.Outgoing.Navigator; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Navigator; +using Vortex.Revisions.Revision20260701.Serializers.Navigator; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class NavigatorMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.AddFavouriteRoomMessageEvent, + new AddFavouriteRoomMessageParser() + ); + builder.MapParser(MessageEvent.CancelEventMessageEvent, new CancelEventMessageParser()); + builder.MapParser(MessageEvent.CanCreateRoomMessageEvent, new CanCreateRoomMessageParser()); + builder.MapParser( + MessageEvent.CompetitionRoomsSearchMessageEvent, + new CompetitionRoomsSearchMessageParser() + ); + builder.MapParser( + MessageEvent.ConvertGlobalRoomIdMessageEvent, + new ConvertGlobalRoomIdMessageParser() + ); + builder.MapParser(MessageEvent.CreateFlatMessageEvent, new CreateFlatMessageParser()); + builder.MapParser( + MessageEvent.DeleteFavouriteRoomMessageEvent, + new DeleteFavouriteRoomMessageParser() + ); + builder.MapParser(MessageEvent.EditEventMessageEvent, new EditEventMessageParser()); + builder.MapParser( + MessageEvent.ForwardToARandomPromotedRoomMessageEvent, + new ForwardToARandomPromotedRoomMessageParser() + ); + builder.MapParser( + MessageEvent.ForwardToSomeRoomMessageEvent, + new ForwardToSomeRoomMessageParser() + ); + builder.MapParser(MessageEvent.GetGuestRoomMessageEvent, new GetGuestRoomMessageParser()); + builder.MapParser( + MessageEvent.GetOfficialRoomsMessageEvent, + new GetOfficialRoomsMessageParser() + ); + builder.MapParser( + MessageEvent.GetPopularRoomTagsMessageEvent, + new GetPopularRoomTagsMessageParser() + ); + builder.MapParser( + MessageEvent.GetUserEventCatsMessageEvent, + new GetUserEventCatsMessageParser() + ); + builder.MapParser( + MessageEvent.GetUserFlatCatsMessageEvent, + new GetUserFlatCatsMessageParser() + ); + builder.MapParser( + MessageEvent.GuildBaseSearchMessageEvent, + new GuildBaseSearchMessageParser() + ); + builder.MapParser( + MessageEvent.MyFavouriteRoomsSearchMessageEvent, + new MyFavouriteRoomsSearchMessageParser() + ); + builder.MapParser( + MessageEvent.MyFrequentRoomHistorySearchMessageEvent, + new MyFrequentRoomHistorySearchMessageParser() + ); + builder.MapParser( + MessageEvent.MyFriendsRoomsSearchMessageEvent, + new MyFriendsRoomsSearchMessageParser() + ); + builder.MapParser( + MessageEvent.MyGuildBasesSearchMessageEvent, + new MyGuildBasesSearchMessageParser() + ); + builder.MapParser( + MessageEvent.MyRecommendedRoomsMessageEvent, + new MyRecommendedRoomsMessageParser() + ); + builder.MapParser( + MessageEvent.MyRoomHistorySearchMessageEvent, + new MyRoomHistorySearchMessageParser() + ); + builder.MapParser( + MessageEvent.MyRoomRightsSearchMessageEvent, + new MyRoomRightsSearchMessageParser() + ); + builder.MapParser(MessageEvent.MyRoomsSearchMessageEvent, new MyRoomsSearchMessageParser()); + builder.MapParser( + MessageEvent.PopularRoomsSearchMessageEvent, + new PopularRoomsSearchMessageParser() + ); + builder.MapParser(MessageEvent.RateFlatMessageEvent, new RateFlatMessageParser()); + builder.MapParser( + MessageEvent.RemoveOwnRoomRightsRoomMessageEvent, + new RemoveOwnRoomRightsRoomMessageParser() + ); + builder.MapParser( + MessageEvent.RoomAdEventTabAdClickedEvent, + new RoomAdEventTabAdClickedMessageParser() + ); + builder.MapParser( + MessageEvent.RoomAdEventTabViewedEvent, + new RoomAdEventTabViewedMessageParser() + ); + builder.MapParser(MessageEvent.RoomAdSearchMessageEvent, new RoomAdSearchMessageParser()); + builder.MapParser( + MessageEvent.RoomsWhereMyFriendsAreSearchMessageEvent, + new RoomsWhereMyFriendsAreSearchMessageParser() + ); + builder.MapParser( + MessageEvent.RoomsWithHighestScoreSearchMessageEvent, + new RoomsWithHighestScoreSearchMessageParser() + ); + builder.MapParser( + MessageEvent.RoomTextSearchMessageEvent, + new RoomTextSearchMessageParser() + ); + builder.MapParser( + MessageEvent.SetRoomSessionTagsMessageEvent, + new SetRoomSessionTagsMessageParser() + ); + builder.MapParser( + MessageEvent.ToggleStaffPickMessageEvent, + new ToggleStaffPickMessageParser() + ); + builder.MapParser( + MessageEvent.UpdateHomeRoomMessageEvent, + new UpdateHomeRoomMessageParser() + ); + + builder.MapSerializer( + typeof(CanCreateRoomEventMessageComposer), + new CanCreateRoomEventMessageComposerSerializer( + MessageComposer.CanCreateRoomEventComposer + ) + ); + builder.MapSerializer( + typeof(CanCreateRoomMessageComposer), + new CanCreateRoomMessageComposerSerializer(MessageComposer.CanCreateRoomComposer) + ); + builder.MapSerializer( + typeof(CategoriesWithVisitorCountMessageComposer), + new CategoriesWithVisitorCountMessageComposerSerializer( + MessageComposer.CategoriesWithVisitorCountComposer + ) + ); + builder.MapSerializer( + typeof(CompetitionRoomsDataMessageComposer), + new CompetitionRoomsDataMessageComposerSerializer( + MessageComposer.CompetitionRoomsDataMessageComposer + ) + ); + builder.MapSerializer( + typeof(ConvertedRoomIdMessageComposer), + new ConvertedRoomIdMessageComposerSerializer(MessageComposer.ConvertedRoomIdComposer) + ); + builder.MapSerializer( + typeof(DoorbellMessageComposer), + new DoorbellMessageComposerSerializer(MessageComposer.DoorbellMessageComposer) + ); + builder.MapSerializer( + typeof(FavouriteChangedMessageComposer), + new FavouriteChangedMessageComposerSerializer(MessageComposer.FavouriteChangedComposer) + ); + builder.MapSerializer( + typeof(FavouritesMessageComposer), + new FavouritesMessageSerializer(MessageComposer.FavouritesComposer) + ); + builder.MapSerializer( + typeof(FlatAccessDeniedMessageComposer), + new FlatAccessDeniedMessageComposerSerializer( + MessageComposer.FlatAccessDeniedMessageComposer + ) + ); + builder.MapSerializer( + typeof(FlatCreatedMessageComposer), + new FlatCreatedMessageComposerSerializer(MessageComposer.FlatCreatedComposer) + ); + builder.MapSerializer( + typeof(GetGuestRoomResultMessageComposer), + new GetGuestRoomResultMessageComposerSerializer( + MessageComposer.GetGuestRoomResultComposer + ) + ); + builder.MapSerializer( + typeof(GuestRoomSearchResultMessageComposer), + new GuestRoomSearchResultMessageComposerSerializer( + MessageComposer.GuestRoomSearchResultComposer + ) + ); + builder.MapSerializer( + typeof(NavigatorSettingsMessageComposer), + new NavigatorSettingsMessageComposerSerializer( + MessageComposer.NavigatorSettingsComposer + ) + ); + builder.MapSerializer( + typeof(OfficialRoomsMessageComposer), + new OfficialRoomsMessageComposerSerializer(MessageComposer.OfficialRoomsComposer) + ); + builder.MapSerializer( + typeof(PopularRoomTagsResultMessageComposer), + new PopularRoomTagsResultMessageComposerSerializer( + MessageComposer.PopularRoomTagsResultComposer + ) + ); + builder.MapSerializer( + typeof(RoomEventCancelMessageComposer), + new RoomEventCancelMessageComposerSerializer(MessageComposer.RoomEventCancelComposer) + ); + builder.MapSerializer( + typeof(RoomEventMessageComposer), + new RoomEventMessageComposerSerializer(MessageComposer.RoomEventComposer) + ); + builder.MapSerializer( + typeof(RoomInfoUpdatedMessageComposer), + new RoomInfoUpdatedMessageComposerSerializer(MessageComposer.RoomInfoUpdatedComposer) + ); + builder.MapSerializer( + typeof(RoomRatingMessageComposer), + new RoomRatingMessageComposerSerializer(MessageComposer.RoomRatingComposer) + ); + builder.MapSerializer( + typeof(UserEventCatsMessageComposer), + new UserEventCatsMessageComposerSerializer(MessageComposer.UserEventCatsComposer) + ); + builder.MapSerializer( + typeof(UserFlatCatsMessageComposer), + new UserFlatCatsMessageComposerSerializer(MessageComposer.UserFlatCatsComposer) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/NewNavigatorMap.cs b/Vortex.Revisions/Revision20260701/Maps/NewNavigatorMap.cs new file mode 100644 index 00000000..11bf7790 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/NewNavigatorMap.cs @@ -0,0 +1,71 @@ +using Vortex.Primitives.Messages.Outgoing.NewNavigator; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.NewNavigator; +using Vortex.Revisions.Revision20260701.Serializers.NewNavigator; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class NewNavigatorMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.NavigatorAddCollapsedCategoryMessageEvent, + new NavigatorAddCollapsedCategoryMessageParser() + ); + builder.MapParser( + MessageEvent.NavigatorAddSavedSearchEvent, + new NavigatorAddSavedSearchMessageParser() + ); + builder.MapParser( + MessageEvent.NavigatorDeleteSavedSearchEvent, + new NavigatorDeleteSavedSearchMessageParser() + ); + builder.MapParser( + MessageEvent.NavigatorRemoveCollapsedCategoryMessageEvent, + new NavigatorRemoveCollapsedCategoryMessageParser() + ); + builder.MapParser( + MessageEvent.NavigatorSetSearchCodeViewModeMessageEvent, + new NavigatorSetSearchCodeViewModeMessageParser() + ); + builder.MapParser(MessageEvent.NewNavigatorInitEvent, new NewNavigatorInitMessageParser()); + builder.MapParser( + MessageEvent.NewNavigatorSearchEvent, + new NewNavigatorSearchMessageParser() + ); + + builder.MapSerializer( + typeof(NavigatorCollapsedCategoriesMessage), + new NavigatorCollapsedCategoriesMessageSerializer( + MessageComposer.NavigatorCollapsedCategoriesMessageComposer + ) + ); + builder.MapSerializer( + typeof(NavigatorLiftedRoomsMessage), + new NavigatorLiftedRoomsMessageSerializer(MessageComposer.NavigatorLiftedRoomsComposer) + ); + builder.MapSerializer( + typeof(NavigatorMetaDataMessage), + new NavigatorMetaDataMessageSerializer(MessageComposer.NavigatorMetaDataComposer) + ); + builder.MapSerializer( + typeof(NavigatorSavedSearchesMessage), + new NavigatorSavedSearchesMessageSerializer( + MessageComposer.NavigatorSavedSearchesComposer + ) + ); + builder.MapSerializer( + typeof(NavigatorSearchResultBlocksMessageComposer), + new NavigatorSearchResultBlocksMessageSerializer( + MessageComposer.NavigatorSearchResultBlocksComposer + ) + ); + builder.MapSerializer( + typeof(NewNavigatorPreferencesMessageComposer), + new NewNavigatorPreferencesMessageSerializer( + MessageComposer.NewNavigatorPreferencesComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/NftMap.cs b/Vortex.Revisions/Revision20260701/Maps/NftMap.cs new file mode 100644 index 00000000..88f19365 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/NftMap.cs @@ -0,0 +1,25 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Nft; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class NftMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.GetNftCreditsMessageEvent, new GetNftCreditsMessageParser()); + builder.MapParser( + MessageEvent.GetSelectedNftWardrobeOutfitMessageEvent, + new GetSelectedNftWardrobeOutfitMessageParser() + ); + builder.MapParser(MessageEvent.GetSilverMessageEvent, new GetSilverMessageParser()); + builder.MapParser( + MessageEvent.GetUserNftWardrobeMessageEvent, + new GetUserNftWardrobeMessageParser() + ); + builder.MapParser( + MessageEvent.SaveUserNftWardrobeMessageEvent, + new SaveUserNftWardrobeMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/NotificationsMap.cs b/Vortex.Revisions/Revision20260701/Maps/NotificationsMap.cs new file mode 100644 index 00000000..03bde969 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/NotificationsMap.cs @@ -0,0 +1,96 @@ +using Vortex.Primitives.Messages.Outgoing.Notifications; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Notifications; +using Vortex.Revisions.Revision20260701.Serializers.Notifications; +using Vortex.Revisions.Revision20260701.Serializers.Preferences; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class NotificationsMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.ResetUnseenItemIdsEvent, + new ResetUnseenItemIdsMessageParser() + ); + builder.MapParser(MessageEvent.ResetUnseenItemsEvent, new ResetUnseenItemsMessageParser()); + + builder.MapSerializer( + typeof(ActivityPointsMessageComposer), + new ActivityPointsMessageComposerSerializer( + MessageComposer.ActivityPointsMessageComposer + ) + ); + builder.MapSerializer( + typeof(ClubGiftNotificationEventMessageComposer), + new ClubGiftNotificationEventMessageComposerSerializer( + MessageComposer.ClubGiftNotificationComposer + ) + ); + builder.MapSerializer( + typeof(ElementPointerMessageComposer), + new ElementPointerMessageComposerSerializer( + MessageComposer.ElementPointerMessageComposer + ) + ); + builder.MapSerializer( + typeof(HabboAchievementNotificationMessageComposer), + new HabboAchievementNotificationMessageComposerSerializer( + MessageComposer.HabboAchievementNotificationMessageComposer + ) + ); + builder.MapSerializer( + typeof(HabboActivityPointNotificationMessageComposer), + new HabboActivityPointNotificationMessageComposerSerializer( + MessageComposer.HabboActivityPointNotificationMessageComposer + ) + ); + builder.MapSerializer( + typeof(HabboBroadcastMessageComposer), + new HabboBroadcastMessageComposerSerializer( + MessageComposer.HabboBroadcastMessageComposer + ) + ); + builder.MapSerializer( + typeof(InfoFeedEnableMessageComposer), + new InfoFeedEnableMessageComposerSerializer( + MessageComposer.InfoFeedEnableMessageComposer + ) + ); + builder.MapSerializer( + typeof(MOTDNotificationEventMessageComposer), + new MOTDNotificationEventMessageComposerSerializer( + MessageComposer.MOTDNotificationComposer + ) + ); + builder.MapSerializer( + typeof(NotificationDialogMessageComposer), + new NotificationDialogMessageComposerSerializer( + MessageComposer.NotificationDialogMessageComposer + ) + ); + builder.MapSerializer( + typeof(OfferRewardDeliveredMessageComposer), + new OfferRewardDeliveredMessageComposerSerializer( + MessageComposer.OfferRewardDeliveredMessageComposer + ) + ); + builder.MapSerializer( + typeof(PetLevelNotificationEventMessageComposer), + new PetLevelNotificationEventMessageComposerSerializer( + MessageComposer.PetLevelNotificationComposer + ) + ); + builder.MapSerializer( + typeof(RestoreClientMessageComposer), + new RestoreClientMessageComposerSerializer(MessageComposer.RestoreClientMessageComposer) + ); + builder.MapSerializer( + typeof(UnseenItemsEventMessageComposer), + new AccountPreferencesEventMessageComposerSerializer( + MessageComposer.UnseenItemsComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/NuxMap.cs b/Vortex.Revisions/Revision20260701/Maps/NuxMap.cs new file mode 100644 index 00000000..70881cd1 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/NuxMap.cs @@ -0,0 +1,23 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Nux; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class NuxMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.NewUserExperienceGetGiftsMessageEvent, + new NewUserExperienceGetGiftsMessageParser() + ); + builder.MapParser( + MessageEvent.NewUserExperienceScriptProceedEvent, + new NewUserExperienceScriptProceedMessageParser() + ); + builder.MapParser( + MessageEvent.SelectInitialRoomEvent, + new SelectInitialRoomMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/PerkMap.cs b/Vortex.Revisions/Revision20260701/Maps/PerkMap.cs new file mode 100644 index 00000000..b47d8a2e --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/PerkMap.cs @@ -0,0 +1,18 @@ +using Vortex.Primitives.Messages.Outgoing.Perk; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Serializers.Perk; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class PerkMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapSerializer( + typeof(PerkAllowancesMessageComposer), + new PerkAllowancesMessageComposerSerializer( + MessageComposer.PerkAllowancesMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/PollMap.cs b/Vortex.Revisions/Revision20260701/Maps/PollMap.cs new file mode 100644 index 00000000..3f39bec0 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/PollMap.cs @@ -0,0 +1,14 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Poll; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class PollMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.PollAnswerEvent, new PollAnswerMessageParser()); + builder.MapParser(MessageEvent.PollRejectEvent, new PollRejectMessageParser()); + builder.MapParser(MessageEvent.PollStartEvent, new PollStartMessageParser()); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/PreferencesMap.cs b/Vortex.Revisions/Revision20260701/Maps/PreferencesMap.cs new file mode 100644 index 00000000..f0cd8209 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/PreferencesMap.cs @@ -0,0 +1,42 @@ +using Vortex.Primitives.Messages.Outgoing.Preferences; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Preferences; +using Vortex.Revisions.Revision20260701.Serializers.Preferences; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class PreferencesMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.SetChatPreferencesMessageEvent, + new SetChatPreferencesMessageParser() + ); + builder.MapParser( + MessageEvent.SetChatStylePreferenceEvent, + new SetChatStylePreferenceMessageParser() + ); + builder.MapParser( + MessageEvent.SetIgnoreRoomInvitesMessageEvent, + new SetIgnoreRoomInvitesMessageParser() + ); + builder.MapParser( + MessageEvent.SetNewNavigatorWindowPreferencesMessageEvent, + new SetNewNavigatorWindowPreferencesMessageParser() + ); + builder.MapParser( + MessageEvent.SetRoomCameraPreferencesMessageEvent, + new SetRoomCameraPreferencesMessageParser() + ); + builder.MapParser(MessageEvent.SetSoundSettingsEvent, new SetSoundSettingsMessageParser()); + builder.MapParser(MessageEvent.SetUIFlagsMessageEvent, new SetUIFlagsMessageParser()); + + builder.MapSerializer( + typeof(AccountPreferencesEventMessageComposer), + new AccountPreferencesEventMessageComposerSerializer( + MessageComposer.AccountPreferencesComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/QuestMap.cs b/Vortex.Revisions/Revision20260701/Maps/QuestMap.cs new file mode 100644 index 00000000..0aac3106 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/QuestMap.cs @@ -0,0 +1,102 @@ +using Vortex.Primitives.Messages.Outgoing.Quest; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Quest; +using Vortex.Revisions.Revision20260701.Serializers.Quest; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class QuestMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.AcceptQuestMessageEvent, new AcceptQuestMessageParser()); + builder.MapParser(MessageEvent.ActivateQuestMessageEvent, new ActivateQuestMessageParser()); + builder.MapParser(MessageEvent.CancelQuestMessageEvent, new CancelQuestMessageParser()); + builder.MapParser( + MessageEvent.FriendRequestQuestCompleteMessageEvent, + new FriendRequestQuestCompleteMessageParser() + ); + builder.MapParser( + MessageEvent.GetCommunityGoalHallOfFameMessageEvent, + new GetCommunityGoalHallOfFameMessageParser() + ); + builder.MapParser( + MessageEvent.GetCommunityGoalProgressMessageEvent, + new GetCommunityGoalProgressMessageParser() + ); + builder.MapParser( + MessageEvent.GetConcurrentUsersGoalProgressMessageEvent, + new GetConcurrentUsersGoalProgressMessageParser() + ); + builder.MapParser( + MessageEvent.GetConcurrentUsersRewardMessageEvent, + new GetConcurrentUsersRewardMessageParser() + ); + builder.MapParser(MessageEvent.GetDailyTasksEvent, new GetDailyTasksMessageParser()); + builder.MapParser(MessageEvent.GetDailyQuestMessageEvent, new GetDailyQuestMessageParser()); + builder.MapParser(MessageEvent.GetQuestsMessageEvent, new GetQuestsMessageParser()); + builder.MapParser( + MessageEvent.GetSeasonalQuestsOnlyMessageEvent, + new GetSeasonalQuestsOnlyMessageParser() + ); + builder.MapParser( + MessageEvent.OpenQuestTrackerMessageEvent, + new OpenQuestTrackerMessageParser() + ); + builder.MapParser(MessageEvent.RejectQuestMessageEvent, new RejectQuestMessageParser()); + builder.MapParser(MessageEvent.StartCampaignMessageEvent, new StartCampaignMessageParser()); + + builder.MapSerializer( + typeof(QuestsMessageComposer), + new QuestsMessageComposerSerializer(MessageComposer.QuestsMessageComposer) + ); + builder.MapSerializer( + typeof(QuestMessageComposer), + new QuestMessageComposerSerializer(MessageComposer.QuestMessageComposer) + ); + builder.MapSerializer( + typeof(QuestDailyMessageComposer), + new QuestDailyMessageComposerSerializer(MessageComposer.QuestDailyMessageComposer) + ); + builder.MapSerializer( + typeof(SeasonalQuestsMessageComposer), + new SeasonalQuestsMessageComposerSerializer( + MessageComposer.SeasonalQuestsMessageComposer + ) + ); + builder.MapSerializer( + typeof(QuestCompletedMessageComposer), + new QuestCompletedMessageComposerSerializer( + MessageComposer.QuestCompletedMessageComposer + ) + ); + builder.MapSerializer( + typeof(QuestCancelledMessageComposer), + new QuestCancelledMessageComposerSerializer( + MessageComposer.QuestCancelledMessageComposer + ) + ); + builder.MapSerializer( + typeof(EpicPopupMessageComposer), + new EpicPopupMessageComposerSerializer(MessageComposer.EpicPopupMessageComposer) + ); + builder.MapSerializer( + typeof(CommunityGoalHallOfFameMessageComposer), + new CommunityGoalHallOfFameMessageComposerSerializer( + MessageComposer.CommunityGoalHallOfFameMessageComposer + ) + ); + builder.MapSerializer( + typeof(CommunityGoalProgressMessageComposer), + new CommunityGoalProgressMessageComposerSerializer( + MessageComposer.CommunityGoalProgressMessageComposer + ) + ); + builder.MapSerializer( + typeof(ConcurrentUsersGoalProgressMessageComposer), + new ConcurrentUsersGoalProgressMessageComposerSerializer( + MessageComposer.ConcurrentUsersGoalProgressMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/RegisterMap.cs b/Vortex.Revisions/Revision20260701/Maps/RegisterMap.cs new file mode 100644 index 00000000..41cc8614 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/RegisterMap.cs @@ -0,0 +1,15 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Register; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class RegisterMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.UpdateFigureDataMessageEvent, + new UpdateFigureDataMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/RoomDirectoryMap.cs b/Vortex.Revisions/Revision20260701/Maps/RoomDirectoryMap.cs new file mode 100644 index 00000000..80d3c07f --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/RoomDirectoryMap.cs @@ -0,0 +1,15 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.RoomDirectory; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class RoomDirectoryMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.RoomNetworkOpenConnectionMessageEvent, + new RoomNetworkOpenConnectionMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/RoomMap.cs b/Vortex.Revisions/Revision20260701/Maps/RoomMap.cs new file mode 100644 index 00000000..dda8819b --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/RoomMap.cs @@ -0,0 +1,831 @@ +using Vortex.Primitives.Messages.Outgoing.Room.Action; +using Vortex.Primitives.Messages.Outgoing.Room.Bots; +using Vortex.Primitives.Messages.Outgoing.Room.Chat; +using Vortex.Primitives.Messages.Outgoing.Room.Engine; +using Vortex.Primitives.Messages.Outgoing.Room.Furniture; +using Vortex.Primitives.Messages.Outgoing.Room.Layout; +using Vortex.Primitives.Messages.Outgoing.Room.Permissions; +using Vortex.Primitives.Messages.Outgoing.Room.Pets; +using Vortex.Primitives.Messages.Outgoing.Room.Session; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Room.Action; +using Vortex.Revisions.Revision20260701.Parsers.Room.Avatar; +using Vortex.Revisions.Revision20260701.Parsers.Room.Bots; +using Vortex.Revisions.Revision20260701.Parsers.Room.Chat; +using Vortex.Revisions.Revision20260701.Parsers.Room.Engine; +using Vortex.Revisions.Revision20260701.Parsers.Room.Furniture; +using Vortex.Revisions.Revision20260701.Parsers.Room.Layout; +using Vortex.Revisions.Revision20260701.Parsers.Room.Pets; +using Vortex.Revisions.Revision20260701.Parsers.Room.Session; +using Vortex.Revisions.Revision20260701.Serializers.Room.Action; +using Vortex.Revisions.Revision20260701.Serializers.Room.Bots; +using Vortex.Revisions.Revision20260701.Serializers.Room.Chat; +using Vortex.Revisions.Revision20260701.Serializers.Room.Engine; +using Vortex.Revisions.Revision20260701.Serializers.Room.Furniture; +using Vortex.Revisions.Revision20260701.Serializers.Room.Layout; +using Vortex.Revisions.Revision20260701.Serializers.Room.Permissions; +using Vortex.Revisions.Revision20260701.Serializers.Room.Pets; +using Vortex.Revisions.Revision20260701.Serializers.Room.Session; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class RoomMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + // Room Action + builder.MapParser( + MessageEvent.AmbassadorAlertMessageEvent, + new AmbassadorAlertMessageParser() + ); + builder.MapParser(MessageEvent.AssignRightsMessageEvent, new AssignRightsMessageParser()); + builder.MapParser( + MessageEvent.BanUserWithDurationMessageEvent, + new BanUserWithDurationMessageParser() + ); + builder.MapParser(MessageEvent.KickUserMessageEvent, new KickUserMessageParser()); + builder.MapParser(MessageEvent.LetUserInMessageEvent, new LetUserInMessageParser()); + builder.MapParser(MessageEvent.MuteAllInRoomEvent, new MuteAllInRoomMessageParser()); + builder.MapParser(MessageEvent.MuteUserMessageEvent, new MuteUserMessageParser()); + builder.MapParser( + MessageEvent.RemoveAllRightsMessageEvent, + new RemoveAllRightsMessageParser() + ); + builder.MapParser(MessageEvent.RemoveRightsMessageEvent, new RemoveRightsMessageParser()); + builder.MapParser( + MessageEvent.UnbanUserFromRoomMessageEvent, + new UnbanUserFromRoomMessageParser() + ); + builder.MapParser(MessageEvent.UnmuteUserMessageEvent, new UnmuteUserMessageParser()); + + // Room Avatar + builder.MapParser( + MessageEvent.AvatarExpressionMessageEvent, + new AvatarExpressionMessageParser() + ); + builder.MapParser(MessageEvent.ChangeMottoMessageEvent, new ChangeMottoMessageParser()); + builder.MapParser(MessageEvent.ChangePostureMessageEvent, new ChangePostureMessageParser()); + builder.MapParser( + MessageEvent.CustomizeAvatarWithFurniMessageEvent, + new CustomizeAvatarWithFurniMessageParser() + ); + builder.MapParser(MessageEvent.DanceMessageEvent, new DanceMessageParser()); + builder.MapParser(MessageEvent.DropCarryItemMessageEvent, new DropCarryItemMessageParser()); + builder.MapParser(MessageEvent.LookToMessageEvent, new LookToMessageParser()); + builder.MapParser(MessageEvent.PassCarryItemMessageEvent, new PassCarryItemMessageParser()); + builder.MapParser( + MessageEvent.PassCarryItemToPetMessageEvent, + new PassCarryItemToPetMessageParser() + ); + builder.MapParser(MessageEvent.SignMessageEvent, new SignMessageParser()); + + // Room Bots + builder.MapParser(MessageEvent.CommandBotEvent, new CommandBotMessageParser()); + builder.MapParser( + MessageEvent.GetBotCommandConfigurationDataEvent, + new GetBotCommandConfigurationDataMessageParser() + ); + + // Room Chat + builder.MapParser(MessageEvent.CancelTypingMessageEvent, new CancelTypingMessageParser()); + builder.MapParser(MessageEvent.ChatMessageEvent, new ChatMessageParser()); + builder.MapParser(MessageEvent.ShoutMessageEvent, new ShoutMessageParser()); + builder.MapParser(MessageEvent.StartTypingMessageEvent, new StartTypingMessageParser()); + builder.MapParser(MessageEvent.WhisperMessageEvent, new WhisperMessageParser()); + + // Room Engine + builder.MapParser(MessageEvent.ClickCharacterEvent, new ClickCharacterMessageParser()); + builder.MapParser(MessageEvent.ClickFurniMessageEvent, new ClickFurniMessageParser()); + builder.MapParser( + MessageEvent.GetFurnitureAliasesMessageEvent, + new GetFurnitureAliasesMessageParser() + ); + builder.MapParser( + MessageEvent.GetRoomEntryDataMessageEvent, + new GetRoomEntryDataMessageParser() + ); + builder.MapParser(MessageEvent.GetItemDataMessageEvent, new GetItemDataMessageParser()); + builder.MapParser( + MessageEvent.GetPetCommandsMessageEvent, + new GetPetCommandsMessageParser() + ); + builder.MapParser( + MessageEvent.GiveSupplementToPetMessageEvent, + new GiveSupplementToPetMessageParser() + ); + builder.MapParser(MessageEvent.MountPetMessageEvent, new MountPetMessageParser()); + builder.MapParser(MessageEvent.MoveAvatarMessageEvent, new MoveAvatarMessageParser()); + builder.MapParser(MessageEvent.MoveObjectMessageEvent, new MoveObjectMessageParser()); + builder.MapParser(MessageEvent.MovePetMessageEvent, new MovePetMessageParser()); + builder.MapParser(MessageEvent.MoveWallItemMessageEvent, new MoveWallItemMessageParser()); + builder.MapParser( + MessageEvent.VortexGetFurniEditorDataMessageEvent, + new VortexGetFurniEditorDataMessageParser() + ); + builder.MapParser( + MessageEvent.VortexApplyFurniEditMessageEvent, + new VortexApplyFurniEditMessageParser() + ); + builder.MapParser( + MessageEvent.VortexGetFurniDefinitionMessageEvent, + new VortexGetFurniDefinitionMessageParser() + ); + builder.MapParser( + MessageEvent.VortexApplyFurniDefinitionMessageEvent, + new VortexApplyFurniDefinitionMessageParser() + ); + builder.MapParser(MessageEvent.PickupObjectMessageEvent, new PickupObjectMessageParser()); + builder.MapParser(MessageEvent.PlaceBotMessageEvent, new PlaceBotMessageParser()); + builder.MapParser(MessageEvent.PlaceObjectMessageEvent, new PlaceObjectMessageParser()); + builder.MapParser(MessageEvent.PlacePetMessageEvent, new PlacePetMessageParser()); + builder.MapParser( + MessageEvent.RemoveBotFromFlatMessageEvent, + new RemoveBotFromFlatMessageParser() + ); + builder.MapParser(MessageEvent.RemoveItemMessageEvent, new RemoveItemMessageParser()); + builder.MapParser( + MessageEvent.RemovePetFromFlatMessageEvent, + new RemovePetFromFlatMessageParser() + ); + builder.MapParser( + MessageEvent.RemoveSaddleFromPetMessageEvent, + new RemoveSaddleFromPetMessageParser() + ); + builder.MapParser( + MessageEvent.SetClothingChangeDataMessageEvent, + new SetClothingChangeDataMessageParser() + ); + builder.MapParser(MessageEvent.SetItemDataMessageEvent, new SetItemDataMessageParser()); + builder.MapParser(MessageEvent.SetObjectDataMessageEvent, new SetObjectDataMessageParser()); + builder.MapParser( + MessageEvent.TogglePetBreedingPermissionMessageEvent, + new TogglePetBreedingPermissionMessageParser() + ); + builder.MapParser( + MessageEvent.TogglePetRidingPermissionMessageEvent, + new TogglePetRidingPermissionMessageParser() + ); + builder.MapParser(MessageEvent.UseFurnitureMessageEvent, new UseFurnitureMessageParser()); + builder.MapParser(MessageEvent.UseWallItemMessageEvent, new UseWallItemMessageParser()); + + // Room Furniture + builder.MapParser( + MessageEvent.AddSpamWallPostItMessageEvent, + new AddSpamWallPostItMessageParser() + ); + builder.MapParser( + MessageEvent.ControlYoutubeDisplayPlaybackMessageEvent, + new ControlYoutubeDisplayPlaybackMessageParser() + ); + builder.MapParser( + MessageEvent.CreditFurniRedeemMessageEvent, + new CreditFurniRedeemMessageParser() + ); + builder.MapParser(MessageEvent.DiceOffMessageEvent, new DiceOffMessageParser()); + builder.MapParser( + MessageEvent.EnterOneWayDoorMessageEvent, + new EnterOneWayDoorMessageParser() + ); + builder.MapParser( + MessageEvent.ExtendRentOrBuyoutFurniMessageEvent, + new ExtendRentOrBuyoutFurniMessageParser() + ); + builder.MapParser( + MessageEvent.ExtendRentOrBuyoutStripItemMessageEvent, + new ExtendRentOrBuyoutStripItemMessageParser() + ); + builder.MapParser( + MessageEvent.GetGuildFurniContextMenuInfoMessageEvent, + new GetGuildFurniContextMenuInfoMessageParser() + ); + builder.MapParser( + MessageEvent.GetRentOrBuyoutOfferMessageEvent, + new GetRentOrBuyoutOfferMessageParser() + ); + builder.MapParser( + MessageEvent.GetYoutubeDisplayStatusMessageEvent, + new GetYoutubeDisplayStatusMessageParser() + ); + builder.MapParser( + MessageEvent.OpenMysteryTrophyMessageEvent, + new OpenMysteryTrophyMessageParser() + ); + builder.MapParser( + MessageEvent.OpenPetPackageMessageEvent, + new OpenPetPackageMessageParser() + ); + builder.MapParser(MessageEvent.PlacePostItMessageEvent, new PlacePostItMessageParser()); + builder.MapParser(MessageEvent.PresentOpenMessageEvent, new PresentOpenMessageParser()); + builder.MapParser( + MessageEvent.RentableSpaceCancelRentMessageEvent, + new RentableSpaceCancelRentMessageParser() + ); + builder.MapParser( + MessageEvent.RentableSpaceConfigureMessageEvent, + new ConfigureRentableSpaceMessageParser() + ); + builder.MapParser( + MessageEvent.RentableSpaceGetConfigMessageEvent, + new GetRentableSpaceConfigMessageParser() + ); + builder.MapParser( + MessageEvent.RentableSpaceRentMessageEvent, + new RentableSpaceRentMessageParser() + ); + builder.MapParser( + MessageEvent.RentableSpaceStatusMessageEvent, + new RentableSpaceStatusMessageParser() + ); + builder.MapParser( + MessageEvent.RoomDimmerChangeStateMessageEvent, + new RoomDimmerChangeStateMessageParser() + ); + builder.MapParser( + MessageEvent.RoomDimmerGetPresetsMessageEvent, + new RoomDimmerGetPresetsMessageParser() + ); + builder.MapParser( + MessageEvent.RoomDimmerSavePresetMessageEvent, + new RoomDimmerSavePresetMessageParser() + ); + builder.MapParser(MessageEvent.SetAreaHideDataEvent, new SetAreaHideDataMessageParser()); + builder.MapParser( + MessageEvent.SetCustomStackingHeightEvent, + new SetCustomStackingHeightMessageParser() + ); + builder.MapParser( + MessageEvent.SetMannequinFigureEvent, + new SetMannequinFigureMessageParser() + ); + builder.MapParser(MessageEvent.SetMannequinNameEvent, new SetMannequinNameMessageParser()); + builder.MapParser( + MessageEvent.SetRandomStateMessageEvent, + new SetRandomStateMessageParser() + ); + builder.MapParser( + MessageEvent.SetRoomBackgroundColorDataEvent, + new SetRoomBackgroundColorDataMessageParser() + ); + builder.MapParser( + MessageEvent.SetYoutubeDisplayPlaylistMessageEvent, + new SetYoutubeDisplayPlaylistMessageParser() + ); + builder.MapParser( + MessageEvent.SpinWheelOfFortuneMessageEvent, + new SpinWheelOfFortuneMessageParser() + ); + builder.MapParser(MessageEvent.ThrowDiceMessageEvent, new ThrowDiceMessageParser()); + + // Room Layout + builder.MapParser( + MessageEvent.GetOccupiedTilesMessageEvent, + new GetOccupiedTilesMessageParser() + ); + builder.MapParser( + MessageEvent.GetRoomEntryTileMessageEvent, + new GetRoomEntryTileMessageParser() + ); + builder.MapParser( + MessageEvent.UpdateFloorPropertiesMessageEvent, + new UpdateFloorPropertiesMessageParser() + ); + + // Room Pets + builder.MapParser(MessageEvent.BreedPetsMessageEvent, new BreedPetsMessageParser()); + builder.MapParser( + MessageEvent.CustomizePetWithFurniEvent, + new CustomizePetWithFurniMessageParser() + ); + builder.MapParser(MessageEvent.GetPetInfoMessageEvent, new GetPetInfoMessageParser()); + builder.MapParser(MessageEvent.PetSelectedMessageEvent, new PetSelectedMessageParser()); + builder.MapParser(MessageEvent.RespectPetMessageEvent, new RespectPetMessageParser()); + + // Room Session + builder.MapParser(MessageEvent.ChangeQueueMessageEvent, new ChangeQueueMessageParser()); + builder.MapParser( + MessageEvent.OpenFlatConnectionMessageEvent, + new OpenFlatConnectionMessageParser() + ); + builder.MapParser(MessageEvent.QuitMessageEvent, new QuitMessageParser()); + + // Room Action + builder.MapSerializer( + typeof(AvatarEffectMessageComposer), + new AvatarEffectMessageComposerSerializer(MessageComposer.AvatarEffectMessageComposer) + ); + builder.MapSerializer( + typeof(CarryObjectMessageComposer), + new CarryObjectMessageComposerSerializer(MessageComposer.CarryObjectMessageComposer) + ); + builder.MapSerializer( + typeof(DanceMessageComposer), + new DanceMessageComposerSerializer(MessageComposer.DanceMessageComposer) + ); + builder.MapSerializer( + typeof(ExpressionMessageComposer), + new ExpressionMessageComposerSerializer(MessageComposer.ExpressionMessageComposer) + ); + builder.MapSerializer( + typeof(SleepMessageComposer), + new SleepMessageComposerSerializer(MessageComposer.SleepMessageComposer) + ); + builder.MapSerializer( + typeof(UseObjectMessageComposer), + new UseObjectMessageComposerSerializer(MessageComposer.UseObjectMessageComposer) + ); + + // Room Bots + builder.MapSerializer( + typeof(BotCommandConfigurationMessageComposer), + new BotCommandConfigurationMessageComposerSerializer( + MessageComposer.BotCommandConfigurationComposer + ) + ); + builder.MapSerializer( + typeof(BotErrorMessageComposer), + new BotErrorMessageComposerSerializer(MessageComposer.BotErrorComposer) + ); + builder.MapSerializer( + typeof(BotForceOpenContextMenuMessageComposerSerializer), + new BotForceOpenContextMenuMessageComposerSerializer( + MessageComposer.BotForceOpenContextMenuComposer + ) + ); + builder.MapSerializer( + typeof(BotSkillListUpdateMessageComposer), + new BotSkillListUpdateMessageComposerSerializer( + MessageComposer.BotSkillListUpdateComposer + ) + ); + + // Room Chat + builder.MapSerializer( + typeof(ChatMessageComposer), + new ChatMessageComposerSerializer(MessageComposer.ChatMessageComposer) + ); + builder.MapSerializer( + typeof(FloodControlMessageComposer), + new FloodControlMessageComposerSerializer(MessageComposer.FloodControlMessageComposer) + ); + builder.MapSerializer( + typeof(RemainingMutePeriodMessageComposer), + new RemainingMutePeriodMessageComposerSerializer( + MessageComposer.RemainingMutePeriodComposer + ) + ); + builder.MapSerializer( + typeof(RoomChatSettingsMessageComposer), + new RoomChatSettingsMessageComposerSerializer( + MessageComposer.RoomChatSettingsMessageComposer + ) + ); + builder.MapSerializer( + typeof(RoomFilterSettingsMessageComposer), + new RoomFilterSettingsMessageComposerSerializer( + MessageComposer.RoomFilterSettingsMessageComposer + ) + ); + builder.MapSerializer( + typeof(ShoutMessageComposer), + new ShoutMessageComposerSerializer(MessageComposer.ShoutMessageComposer) + ); + builder.MapSerializer( + typeof(UserTypingMessageComposer), + new UserTypingMessageComposerSerializer(MessageComposer.UserTypingMessageComposer) + ); + builder.MapSerializer( + typeof(WhisperMessageComposer), + new WhisperMessageComposerSerializer(MessageComposer.WhisperMessageComposer) + ); + + // Room Engine + builder.MapSerializer( + typeof(BuildersClubPlacementWarningMessageComposer), + new BuildersClubPlacementWarningMessageComposerSerializer( + MessageComposer.BuildersClubPlacementWarningMessageComposer + ) + ); + builder.MapSerializer( + typeof(FavoriteMembershipUpdateMessageComposer), + new FavoriteMembershipUpdateMessageComposerSerializer( + MessageComposer.FavoriteMembershipUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(FloorHeightMapMessageComposer), + new FloorHeightMapMessageComposerSerializer( + MessageComposer.FloorHeightMapMessageComposer + ) + ); + builder.MapSerializer( + typeof(FurnitureAliasesMessageComposer), + new FurnitureAliasesMessageComposerSerializer( + MessageComposer.FurnitureAliasesMessageComposer + ) + ); + builder.MapSerializer( + typeof(HeightMapMessageComposer), + new HeightMapMessageComposerSerializer(MessageComposer.HeightMapMessageComposer) + ); + builder.MapSerializer( + typeof(HeightMapUpdateMessageComposer), + new HeightMapUpdateMessageComposerSerializer( + MessageComposer.HeightMapUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(ItemAddMessageComposer), + new ItemAddMessageComposerSerializer(MessageComposer.ItemAddMessageComposer) + ); + builder.MapSerializer( + typeof(ItemDataUpdateMessageComposer), + new ItemDataUpdateMessageComposerSerializer( + MessageComposer.ItemDataUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(ItemRemoveMessageComposer), + new ItemRemoveMessageComposerSerializer(MessageComposer.ItemRemoveMessageComposer) + ); + builder.MapSerializer( + typeof(ItemsMessageComposer), + new ItemsMessageComposerSerializer(MessageComposer.ItemsMessageComposer) + ); + builder.MapSerializer( + typeof(ItemsStateUpdateMessageComposer), + new ItemsStateUpdateMessageComposerSerializer( + MessageComposer.ItemsStateUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(ItemStateUpdateMessageComposer), + new ItemStateUpdateMessageComposerSerializer( + MessageComposer.ItemStateUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(ItemUpdateMessageComposer), + new ItemUpdateMessageComposerSerializer(MessageComposer.ItemUpdateMessageComposer) + ); + builder.MapSerializer( + typeof(ObjectAddMessageComposer), + new ObjectAddMessageComposerSerializer(MessageComposer.ObjectAddMessageComposer) + ); + builder.MapSerializer( + typeof(ObjectDataUpdateMessageComposer), + new ObjectDataUpdateMessageComposerSerializer( + MessageComposer.ObjectDataUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(ObjectRemoveConfirmMessageComposer), + new ObjectRemoveConfirmMessageComposerSerializer( + MessageComposer.ObjectRemoveConfirmMessageComposer + ) + ); + builder.MapSerializer( + typeof(ObjectRemoveMessageComposer), + new ObjectRemoveMessageComposerSerializer(MessageComposer.ObjectRemoveMessageComposer) + ); + builder.MapSerializer( + typeof(ObjectRemoveMultipleMessageComposer), + new ObjectRemoveMultipleMessageComposerSerializer( + MessageComposer.ObjectRemoveMultipleMessageComposer + ) + ); + builder.MapSerializer( + typeof(ObjectsDataUpdateMessageComposer), + new ObjectsDataUpdateMessageComposerSerializer( + MessageComposer.ObjectsDataUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(ObjectsMessageComposer), + new ObjectsMessageComposerSerializer(MessageComposer.ObjectsMessageComposer) + ); + builder.MapSerializer( + typeof(ObjectUpdateMessageComposer), + new ObjectUpdateMessageComposerSerializer(MessageComposer.ObjectUpdateMessageComposer) + ); + builder.MapSerializer( + typeof(VortexFurniEditorDataMessageComposer), + new VortexFurniEditorDataMessageComposerSerializer( + MessageComposer.VortexFurniEditorDataMessageComposer + ) + ); + builder.MapSerializer( + typeof(VortexFurniEditorRightsMessageComposer), + new VortexFurniEditorRightsMessageComposerSerializer( + MessageComposer.VortexFurniEditorRightsMessageComposer + ) + ); + builder.MapSerializer( + typeof(VortexFurniDefinitionMessageComposer), + new VortexFurniDefinitionMessageComposerSerializer( + MessageComposer.VortexFurniDefinitionMessageComposer + ) + ); + builder.MapSerializer( + typeof(RoomEntryInfoMessageComposer), + new RoomEntryInfoMessageComposerSerializer(MessageComposer.RoomEntryInfoMessageComposer) + ); + builder.MapSerializer( + typeof(RoomPropertyMessageComposer), + new RoomPropertyMessageComposerSerializer(MessageComposer.RoomPropertyMessageComposer) + ); + builder.MapSerializer( + typeof(RoomVisualizationSettingsMessageComposer), + new RoomVisualizationSettingsMessageComposerSerializer( + MessageComposer.RoomVisualizationSettingsComposer + ) + ); + builder.MapSerializer( + typeof(SlideObjectBundleMessageComposer), + new SlideObjectBundleMessageComposerSerializer( + MessageComposer.SlideObjectBundleMessageComposer + ) + ); + builder.MapSerializer( + typeof(SpecialRoomEffectMessageComposer), + new SpecialRoomEffectMessageComposerSerializer( + MessageComposer.SpecialRoomEffectMessageComposer + ) + ); + builder.MapSerializer( + typeof(UserChangeMessageComposer), + new UserChangeMessageComposerSerializer(MessageComposer.UserChangeMessageComposer) + ); + builder.MapSerializer( + typeof(UserRemoveMessageComposer), + new UserRemoveMessageComposerSerializer(MessageComposer.UserRemoveMessageComposer) + ); + builder.MapSerializer( + typeof(UsersMessageComposer), + new UsersMessageComposerSerializer(MessageComposer.UsersMessageComposer) + ); + builder.MapSerializer( + typeof(UserUpdateMessageComposer), + new UserUpdateMessageComposerSerializer(MessageComposer.UserUpdateMessageComposer) + ); + builder.MapSerializer( + typeof(WiredMovementsMessageComposer), + new WiredMovementsMessageComposerSerializer( + MessageComposer.WiredMovementsMessageComposer + ) + ); + + // Room Furniture + builder.MapSerializer( + typeof(AreaHideMessageComposer), + new AreaHideMessageComposerSerializer(MessageComposer.AreaHideMessageComposer) + ); + builder.MapSerializer( + typeof(CustomStackingHeightUpdateMessageComposer), + new CustomStackingHeightUpdateMessageComposerSerializer( + MessageComposer.CustomStackingHeightUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(CustomUserNotificationMessageComposer), + new CustomUserNotificationMessageComposerSerializer( + MessageComposer.CustomUserNotificationMessageComposer + ) + ); + builder.MapSerializer( + typeof(DiceValueMessageComposer), + new DiceValueMessageComposerSerializer(MessageComposer.DiceValueMessageComposer) + ); + builder.MapSerializer( + typeof(FurniRentOrBuyoutOfferMessageComposer), + new FurniRentOrBuyoutOfferMessageComposerSerializer( + MessageComposer.FurniRentOrBuyoutOfferMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildFurniContextMenuInfoMessageComposer), + new GuildFurniContextMenuInfoMessageComposerSerializer( + MessageComposer.GuildFurniContextMenuInfoMessageComposer + ) + ); + builder.MapSerializer( + typeof(OneWayDoorStatusMessageComposer), + new OneWayDoorStatusMessageComposerSerializer( + MessageComposer.OneWayDoorStatusMessageComposer + ) + ); + builder.MapSerializer( + typeof(OpenPetPackageRequestedMessageComposer), + new OpenPetPackageRequestedMessageComposerSerializer( + MessageComposer.OpenPetPackageRequestedMessageComposer + ) + ); + builder.MapSerializer( + typeof(OpenPetPackageResultMessageComposer), + new OpenPetPackageResultMessageComposerSerializer( + MessageComposer.OpenPetPackageResultMessageComposer + ) + ); + builder.MapSerializer( + typeof(PresentOpenedMessageComposer), + new PresentOpenedMessageComposerSerializer(MessageComposer.PresentOpenedMessageComposer) + ); + builder.MapSerializer( + typeof(RentableSpaceConfigMessageComposer), + new RentableSpaceConfigMessageComposerSerializer( + MessageComposer.RentableSpaceConfigMessageComposer + ) + ); + builder.MapSerializer( + typeof(RentableSpaceRentFailedMessageComposer), + new RentableSpaceRentFailedMessageComposerSerializer( + MessageComposer.RentableSpaceRentFailedMessageComposer + ) + ); + builder.MapSerializer( + typeof(RentableSpaceRentOkMessageComposer), + new RentableSpaceRentOkMessageComposerSerializer( + MessageComposer.RentableSpaceRentOkMessageComposer + ) + ); + builder.MapSerializer( + typeof(RentableSpaceStatusMessageComposer), + new RentableSpaceStatusMessageComposerSerializer( + MessageComposer.RentableSpaceStatusMessageComposer + ) + ); + builder.MapSerializer( + typeof(RequestSpamWallPostItMessageComposer), + new RequestSpamWallPostItMessageComposerSerializer( + MessageComposer.RequestSpamWallPostItMessageComposer + ) + ); + builder.MapSerializer( + typeof(RoomDimmerPresetsMessageComposer), + new RoomDimmerPresetsMessageComposerSerializer( + MessageComposer.RoomDimmerPresetsMessageComposer + ) + ); + builder.MapSerializer( + typeof(RoomMessageNotificationMessageComposer), + new RoomMessageNotificationMessageComposerSerializer( + MessageComposer.RoomMessageNotificationMessageComposer + ) + ); + builder.MapSerializer( + typeof(YoutubeControlVideoMessageComposer), + new YoutubeControlVideoMessageComposerSerializer( + MessageComposer.YoutubeControlVideoMessageComposer + ) + ); + builder.MapSerializer( + typeof(YoutubeDisplayPlaylistsMessageComposer), + new YoutubeDisplayPlaylistsMessageComposerSerializer( + MessageComposer.YoutubeDisplayPlaylistsMessageComposer + ) + ); + builder.MapSerializer( + typeof(YoutubeDisplayVideoMessageComposer), + new YoutubeDisplayVideoMessageComposerSerializer( + MessageComposer.YoutubeDisplayVideoMessageComposer + ) + ); + + // Room Layout + builder.MapSerializer( + typeof(RoomEntryTileMessageComposer), + new RoomEntryTileMessageComposerSerializer(MessageComposer.RoomEntryTileMessageComposer) + ); + builder.MapSerializer( + typeof(RoomOccupiedTilesMessageComposer), + new RoomOccupiedTilesMessageComposerSerializer( + MessageComposer.RoomOccupiedTilesMessageComposer + ) + ); + + // Room Permissions + builder.MapSerializer( + typeof(YouAreControllerMessageComposer), + new YouAreControllerMessageComposerSerializer( + MessageComposer.YouAreControllerMessageComposer + ) + ); + builder.MapSerializer( + typeof(YouAreNotControllerMessageComposer), + new YouAreNotControllerMessageComposerSerializer( + MessageComposer.YouAreNotControllerMessageComposer + ) + ); + builder.MapSerializer( + typeof(YouAreOwnerMessageComposer), + new YouAreOwnerMessageComposerSerializer(MessageComposer.YouAreOwnerMessageComposer) + ); + + // Room Pets + builder.MapSerializer( + typeof(PetBreedingResultEventMessageComposer), + new PetBreedingResultEventMessageComposerSerializer( + MessageComposer.PetBreedingResultComposer + ) + ); + builder.MapSerializer( + typeof(PetCommandsMessageComposer), + new PetCommandsMessageComposerSerializer(MessageComposer.PetCommandsMessageComposer) + ); + builder.MapSerializer( + typeof(PetExperienceMessageComposer), + new PetExperienceMessageComposerSerializer(MessageComposer.PetExperienceComposer) + ); + builder.MapSerializer( + typeof(PetFigureUpdateMessageComposer), + new PetFigureUpdateMessageComposerSerializer(MessageComposer.PetFigureUpdateComposer) + ); + builder.MapSerializer( + typeof(PetInfoMessageComposer), + new PetInfoMessageComposerSerializer(MessageComposer.PetInfoMessageComposer) + ); + builder.MapSerializer( + typeof(PetLevelUpdateMessageComposer), + new PetLevelUpdateMessageComposerSerializer(MessageComposer.PetLevelUpdateComposer) + ); + builder.MapSerializer( + typeof(PetPlacingErrorMessageComposer), + new PetPlacingErrorMessageComposerSerializer(MessageComposer.PetPlacingErrorComposer) + ); + builder.MapSerializer( + typeof(PetRespectFailedMessageComposer), + new PetRespectFailedMessageComposerSerializer(MessageComposer.PetRespectFailedComposer) + ); + builder.MapSerializer( + typeof(PetStatusUpdateMessageComposer), + new PetStatusUpdateMessageComposerSerializer(MessageComposer.PetStatusUpdateComposer) + ); + + // Room Session + builder.MapSerializer( + typeof(CantConnectMessageComposer), + new CantConnectMessageComposerSerializer(MessageComposer.CantConnectMessageComposer) + ); + builder.MapSerializer( + typeof(CloseConnectionMessageComposer), + new CloseConnectionMessageComposerSerializer( + MessageComposer.CloseConnectionMessageComposer + ) + ); + builder.MapSerializer( + typeof(FlatAccessibleMessageComposer), + new FlatAccessibleMessageComposerSerializer( + MessageComposer.FlatAccessibleMessageComposer + ) + ); + builder.MapSerializer( + typeof(GamePlayerValueMessageComposer), + new GamePlayerValueMessageComposerSerializer( + MessageComposer.GamePlayerValueMessageComposer + ) + ); + builder.MapSerializer( + typeof(HanditemConfigurationMessageComposer), + new HanditemConfigurationMessageComposerSerializer( + MessageComposer.HanditemConfigurationMessageComposer + ) + ); + builder.MapSerializer( + typeof(OpenConnectionMessageComposer), + new OpenConnectionMessageComposerSerializer( + MessageComposer.OpenConnectionMessageComposer + ) + ); + builder.MapSerializer( + typeof(RoomForwardMessageComposer), + new RoomForwardMessageComposerSerializer(MessageComposer.RoomForwardMessageComposer) + ); + builder.MapSerializer( + typeof(RoomQueueStatusMessageComposer), + new RoomQueueStatusMessageComposerSerializer( + MessageComposer.RoomQueueStatusMessageComposer + ) + ); + builder.MapSerializer( + typeof(RoomReadyMessageComposer), + new RoomReadyMessageComposerSerializer(MessageComposer.RoomReadyMessageComposer) + ); + builder.MapSerializer( + typeof(YouAreNotSpectatorMessageComposer), + new YouAreNotSpectatorMessageComposerSerializer( + MessageComposer.YouAreNotSpectatorMessageComposer + ) + ); + builder.MapSerializer( + typeof(YouArePlayingGameMessageComposer), + new YouArePlayingGameMessageComposerSerializer( + MessageComposer.YouArePlayingGameMessageComposer + ) + ); + builder.MapSerializer( + typeof(YouAreSpectatorMessageComposer), + new YouAreSpectatorMessageComposerSerializer( + MessageComposer.YouAreSpectatorMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/RoomSettingsMap.cs b/Vortex.Revisions/Revision20260701/Maps/RoomSettingsMap.cs new file mode 100644 index 00000000..9455f940 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/RoomSettingsMap.cs @@ -0,0 +1,48 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Configuration; +using Vortex.Revisions.Revision20260701.Parsers.RoomSettings; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class RoomSettingsMap : IRevisionMap +{ + private readonly ProtocolLimitsConfig _protocolLimits; + + public RoomSettingsMap(ProtocolLimitsConfig protocolLimits) + { + _protocolLimits = protocolLimits; + } + + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.DeleteRoomMessageEvent, new DeleteRoomMessageParser()); + builder.MapParser( + MessageEvent.GetBannedUsersFromRoomMessageEvent, + new GetBannedUsersFromRoomMessageParser() + ); + builder.MapParser( + MessageEvent.GetCustomRoomFilterMessageEvent, + new GetCustomRoomFilterMessageParser() + ); + builder.MapParser( + MessageEvent.GetFlatControllersMessageEvent, + new GetFlatControllersMessageParser() + ); + builder.MapParser( + MessageEvent.GetRoomSettingsMessageEvent, + new GetRoomSettingsMessageParser() + ); + builder.MapParser( + MessageEvent.SaveRoomSettingsMessageEvent, + new SaveRoomSettingsMessageParser(_protocolLimits.MaxRoomTags) + ); + builder.MapParser( + MessageEvent.UpdateRoomCategoryAndTradeSettingsEvent, + new UpdateRoomCategoryAndTradeSettingsMessageParser() + ); + builder.MapParser( + MessageEvent.UpdateRoomFilterMessageEvent, + new UpdateRoomFilterMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/SoundMap.cs b/Vortex.Revisions/Revision20260701/Maps/SoundMap.cs new file mode 100644 index 00000000..9c2ad9d2 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/SoundMap.cs @@ -0,0 +1,35 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Sound; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class SoundMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.AddJukeboxDiskEvent, new AddJukeboxDiskMessageParser()); + builder.MapParser( + MessageEvent.GetJukeboxPlayListMessageEvent, + new GetJukeboxPlayListMessageParser() + ); + builder.MapParser(MessageEvent.GetNowPlayingMessageEvent, new GetNowPlayingMessageParser()); + builder.MapParser( + MessageEvent.GetOfficialSongIdMessageEvent, + new GetOfficialSongIdMessageParser() + ); + builder.MapParser(MessageEvent.GetSongInfoMessageEvent, new GetSongInfoMessageParser()); + builder.MapParser( + MessageEvent.GetSoundMachinePlayListMessageEvent, + new GetSoundMachinePlayListMessageParser() + ); + builder.MapParser(MessageEvent.GetSoundSettingsEvent, new GetSoundSettingsMessageParser()); + builder.MapParser( + MessageEvent.GetUserSongDisksMessageEvent, + new GetUserSongDisksMessageParser() + ); + builder.MapParser( + MessageEvent.RemoveJukeboxDiskEvent, + new RemoveJukeboxDiskMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/TalentMap.cs b/Vortex.Revisions/Revision20260701/Maps/TalentMap.cs new file mode 100644 index 00000000..076671cd --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/TalentMap.cs @@ -0,0 +1,23 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Talent; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class TalentMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.GetTalentTrackLevelMessageEvent, + new GetTalentTrackLevelMessageParser() + ); + builder.MapParser( + MessageEvent.GetTalentTrackMessageEvent, + new GetTalentTrackMessageParser() + ); + builder.MapParser( + MessageEvent.GuideAdvertisementReadMessageEvent, + new GuideAdvertisementReadMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/TrackingMap.cs b/Vortex.Revisions/Revision20260701/Maps/TrackingMap.cs new file mode 100644 index 00000000..64363aa8 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/TrackingMap.cs @@ -0,0 +1,37 @@ +using Vortex.Primitives.Messages.Outgoing.Tracking; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Tracking; +using Vortex.Revisions.Revision20260701.Serializers.Tracking; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class TrackingMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.EventLogMessageEvent, new EventLogMessageParser()); + builder.MapParser( + MessageEvent.LagWarningReportMessageEvent, + new LagWarningReportMessageParser() + ); + builder.MapParser( + MessageEvent.LatencyPingReportMessageEvent, + new LatencyPingReportMessageParser() + ); + builder.MapParser( + MessageEvent.LatencyPingRequestMessageEvent, + new LatencyPingRequestMessageParser() + ); + builder.MapParser( + MessageEvent.PerformanceLogMessageEvent, + new PerformanceLogMessageParser() + ); + + builder.MapSerializer( + typeof(LatencyPingResponseMessage), + new LatencyPingResponseMessageSerializer( + MessageComposer.LatencyPingResponseMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/UserClassificationMap.cs b/Vortex.Revisions/Revision20260701/Maps/UserClassificationMap.cs new file mode 100644 index 00000000..abaeeed5 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/UserClassificationMap.cs @@ -0,0 +1,19 @@ +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.UserClassification; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class UserClassificationMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.PeerUsersClassificationMessageEvent, + new PeerUsersClassificationMessageParser() + ); + builder.MapParser( + MessageEvent.RoomUsersClassificationMessageEvent, + new RoomUsersClassificationMessageParser() + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/UserDefinedRoomEventsMap.cs b/Vortex.Revisions/Revision20260701/Maps/UserDefinedRoomEventsMap.cs new file mode 100644 index 00000000..cea68dcb --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/UserDefinedRoomEventsMap.cs @@ -0,0 +1,217 @@ +using Vortex.Primitives.Messages.Outgoing.Userdefinedroomevents; +using Vortex.Primitives.Messages.Outgoing.Userdefinedroomevents.Wiredmenu; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.UserDefinedRoomEvents; +using Vortex.Revisions.Revision20260701.Parsers.UserDefinedRoomEvents.Wiredmenu; +using Vortex.Revisions.Revision20260701.Serializers.UserDefinedRoomEvents; +using Vortex.Revisions.Revision20260701.Serializers.UserDefinedRoomEvents.Wiredmenu; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class UserDefinedRoomEventsMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser(MessageEvent.ApplySnapshotMessageEvent, new ApplySnapshotMessageParser()); + builder.MapParser(MessageEvent.OpenMessageEvent, new OpenMessageParser()); + builder.MapParser(MessageEvent.UpdateActionMessageEvent, new UpdateActionMessageParser()); + builder.MapParser(MessageEvent.UpdateAddonMessageEvent, new UpdateAddonMessageParser()); + builder.MapParser( + MessageEvent.UpdateConditionMessageEvent, + new UpdateConditionMessageParser() + ); + builder.MapParser( + MessageEvent.UpdateSelectorMessageEvent, + new UpdateSelectorMessageParser() + ); + builder.MapParser(MessageEvent.UpdateTriggerMessageEvent, new UpdateTriggerMessageParser()); + builder.MapParser( + MessageEvent.UpdateVariableMessageEvent, + new UpdateVariableMessageParser() + ); + + // Userdefinedroomevents Wiredmenu + builder.MapParser( + MessageEvent.WiredClearErrorLogsMessageEvent, + new WiredClearErrorLogsMessageParser() + ); + builder.MapParser( + MessageEvent.WiredGetAllVariableHoldersMessageEvent, + new WiredGetAllVariableHoldersMessageParser() + ); + builder.MapParser( + MessageEvent.WiredGetAllVariablesDiffsMessageEvent, + new WiredGetAllVariablesDiffsMessageParser() + ); + builder.MapParser( + MessageEvent.WiredGetAllVariablesHashMessageEvent, + new WiredGetAllVariablesHashMessageParser() + ); + builder.MapParser( + MessageEvent.WiredGetErrorLogsMessageEvent, + new WiredGetErrorLogsMessageParser() + ); + builder.MapParser(MessageEvent.WiredGetRoomLogsEvent, new WiredGetRoomLogsMessageParser()); + builder.MapParser( + MessageEvent.WiredGetRoomSettingsMessageEvent, + new WiredGetRoomSettingsMessageParser() + ); + builder.MapParser( + MessageEvent.WiredGetRoomStatsMessageEvent, + new WiredGetRoomStatsMessageParser() + ); + builder.MapParser( + MessageEvent.WiredGetUserPermanentVariablesEvent, + new WiredGetUserPermanentVariablesMessageParser() + ); + builder.MapParser( + MessageEvent.WiredGetVariableOwnersPageEvent, + new WiredGetVariableOwnersPageMessageParser() + ); + builder.MapParser( + MessageEvent.WiredGetVariablesForObjectMessageEvent, + new WiredGetVariablesForObjectMessageParser() + ); + builder.MapParser( + MessageEvent.WiredSetObjectVariableValueMessageEvent, + new WiredSetObjectVariableValueMessageParser() + ); + builder.MapParser( + MessageEvent.WiredSetPreferencesMessageEvent, + new WiredSetPreferencesMessageParser() + ); + builder.MapParser( + MessageEvent.WiredSetRoomSettingsMessageEvent, + new WiredSetRoomSettingsMessageParser() + ); + + builder.MapSerializer( + typeof(OpenEventMessageComposer), + new OpenEventMessageComposerSerializer(MessageComposer.OpenComposer) + ); + builder.MapSerializer( + typeof(WiredFurniActionEventMessageComposer), + new WiredFurniActionEventMessageComposerSerializer( + MessageComposer.WiredFurniActionComposer + ) + ); + builder.MapSerializer( + typeof(WiredFurniAddonEventMessageComposer), + new WiredFurniAddonEventMessageComposerSerializer( + MessageComposer.WiredFurniAddonComposer + ) + ); + builder.MapSerializer( + typeof(WiredFurniConditionEventMessageComposer), + new WiredFurniConditionEventMessageComposerSerializer( + MessageComposer.WiredFurniConditionComposer + ) + ); + builder.MapSerializer( + typeof(WiredFurniSelectorEventMessageComposer), + new WiredFurniSelectorEventMessageComposerSerializer( + MessageComposer.WiredFurniSelectorComposer + ) + ); + builder.MapSerializer( + typeof(WiredFurniTriggerEventMessageComposer), + new WiredFurniTriggerEventMessageComposerSerializer( + MessageComposer.WiredFurniTriggerComposer + ) + ); + builder.MapSerializer( + typeof(WiredFurniVariableEventMessageComposer), + new WiredFurniVariableEventMessageComposerSerializer( + MessageComposer.WiredFurniVariableComposer + ) + ); + builder.MapSerializer( + typeof(WiredRewardResultMessageComposer), + new WiredRewardResultMessageComposerSerializer( + MessageComposer.WiredRewardResultMessageComposer + ) + ); + builder.MapSerializer( + typeof(WiredSaveSuccessEventMessageComposer), + new WiredSaveSuccessEventMessageComposerSerializer( + MessageComposer.WiredSaveSuccessComposer + ) + ); + builder.MapSerializer( + typeof(WiredValidationErrorEventMessageComposer), + new WiredValidationErrorEventMessageComposerSerializer( + MessageComposer.WiredValidationErrorComposer + ) + ); + builder.MapSerializer( + typeof(WiredAllVariableHoldersEventMessageComposer), + new WiredAllVariableHoldersEventMessageComposerSerializer( + MessageComposer.WiredAllVariableHoldersComposer + ) + ); + builder.MapSerializer( + typeof(WiredAllVariablesDiffsEventMessageComposer), + new WiredAllVariablesDiffsEventMessageComposerSerializer( + MessageComposer.WiredAllVariablesDiffsComposer + ) + ); + builder.MapSerializer( + typeof(WiredAllVariablesHashEventMessageComposer), + new WiredAllVariablesHashEventMessageComposerSerializer( + MessageComposer.WiredAllVariablesHashComposer + ) + ); + builder.MapSerializer( + typeof(WiredErrorLogsEventMessageComposer), + new WiredErrorLogsEventMessageComposerSerializer(MessageComposer.WiredErrorLogsComposer) + ); + builder.MapSerializer( + typeof(WiredMenuErrorEventMessageComposer), + new WiredMenuErrorEventMessageComposerSerializer(MessageComposer.WiredMenuErrorComposer) + ); + builder.MapSerializer( + typeof(WiredPermissionsEventMessageComposer), + new WiredPermissionsEventMessageComposerSerializer( + MessageComposer.WiredPermissionsComposer + ) + ); + builder.MapSerializer( + typeof(WiredRoomSettingsEventMessageComposer), + new WiredRoomSettingsEventMessageComposerSerializer( + MessageComposer.WiredRoomSettingsComposer + ) + ); + builder.MapSerializer( + typeof(WiredRoomLogsComposer), + new WiredRoomLogsComposerSerializer(MessageComposer.WiredRoomLogsComposer) + ); + builder.MapSerializer( + typeof(WiredRoomStatsEventMessageComposer), + new WiredRoomStatsEventMessageComposerSerializer(MessageComposer.WiredRoomStatsComposer) + ); + builder.MapSerializer( + typeof(WiredSetUserPermanentVariableResultComposer), + new WiredSetUserPermanentVariableResultComposerSerializer( + MessageComposer.WiredSetUserPermanentVariableResultComposer + ) + ); + builder.MapSerializer( + typeof(WiredUserPermanentVariablesComposer), + new WiredUserPermanentVariablesComposerSerializer( + MessageComposer.WiredUserPermanentVariablesComposer + ) + ); + builder.MapSerializer( + typeof(WiredUserVariablesListComposer), + new WiredUserVariablesListComposerSerializer( + MessageComposer.WiredUserVariablesListComposer + ) + ); + builder.MapSerializer( + typeof(WiredVariablesForObjectEventMessageComposer), + new WiredVariablesForObjectEventMessageComposerSerializer( + MessageComposer.WiredVariablesForObjectComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/UsersMap.cs b/Vortex.Revisions/Revision20260701/Maps/UsersMap.cs new file mode 100644 index 00000000..36caf692 --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/UsersMap.cs @@ -0,0 +1,310 @@ +using Vortex.Primitives.Messages.Outgoing.Users; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Users; +using Vortex.Revisions.Revision20260701.Serializers.Users; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class UsersMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.AddAdminRightsToMemberMessageEvent, + new AddAdminRightsToMemberMessageParser() + ); + builder.MapParser( + MessageEvent.ApproveAllMembershipRequestsMessageEvent, + new ApproveAllMembershipRequestsMessageParser() + ); + builder.MapParser( + MessageEvent.ApproveMembershipRequestMessageEvent, + new ApproveMembershipRequestMessageParser() + ); + builder.MapParser(MessageEvent.ApproveNameMessageEvent, new ApproveNameMessageParser()); + builder.MapParser(MessageEvent.ChangeEmailEvent, new ChangeEmailMessageParser()); + builder.MapParser(MessageEvent.CreateGuildMessageEvent, new CreateGuildMessageParser()); + builder.MapParser( + MessageEvent.DeactivateGuildMessageEvent, + new DeactivateGuildMessageParser() + ); + builder.MapParser( + MessageEvent.DeselectFavouriteHabboGroupMessageEvent, + new DeselectFavouriteHabboGroupMessageParser() + ); + builder.MapParser(MessageEvent.GetEmailStatusEvent, new GetEmailStatusMessageParser()); + builder.MapParser( + MessageEvent.GetExtendedProfileByNameMessageEvent, + new GetExtendedProfileByNameMessageParser() + ); + builder.MapParser( + MessageEvent.GetExtendedProfileMessageEvent, + new GetExtendedProfileMessageParser() + ); + builder.MapParser(MessageEvent.RespectUserMessageEvent, new RespectUserMessageParser()); + builder.MapParser( + MessageEvent.GetGuildCreationInfoMessageEvent, + new GetGuildCreationInfoMessageParser() + ); + builder.MapParser( + MessageEvent.GetGuildEditInfoMessageEvent, + new GetGuildEditInfoMessageParser() + ); + builder.MapParser( + MessageEvent.GetGuildEditorDataMessageEvent, + new GetGuildEditorDataMessageParser() + ); + builder.MapParser( + MessageEvent.GetGuildMembershipsMessageEvent, + new GetGuildMembershipsMessageParser() + ); + builder.MapParser( + MessageEvent.GetGuildMembersMessageEvent, + new GetGuildMembersMessageParser() + ); + builder.MapParser( + MessageEvent.GetHabboGroupBadgesMessageEvent, + new GetHabboGroupBadgesMessageParser() + ); + builder.MapParser( + MessageEvent.GetHabboGroupDetailsMessageEvent, + new GetHabboGroupDetailsMessageParser() + ); + builder.MapParser(MessageEvent.BlockListInitEvent, new BlockListInitMessageParser()); + builder.MapParser( + MessageEvent.GetIgnoredUsersMessageEvent, + new GetIgnoredUsersMessageParser() + ); + builder.MapParser( + MessageEvent.GetMemberGuildItemCountMessageEvent, + new GetMemberGuildItemCountMessageParser() + ); + builder.MapParser(MessageEvent.GetMOTDMessageEvent, new GetMOTDMessageParser()); + builder.MapParser( + MessageEvent.GetRelationshipStatusInfoMessageEvent, + new GetRelationshipStatusInfoMessageParser() + ); + builder.MapParser( + MessageEvent.GetSelectedBadgesMessageEvent, + new GetSelectedBadgesMessageParser() + ); + builder.MapParser( + MessageEvent.GetUserNftChatStylesMessageEvent, + new GetUserNftChatStylesMessageParser() + ); + builder.MapParser(MessageEvent.IgnoreUserMessageEvent, new IgnoreUserMessageParser()); + builder.MapParser( + MessageEvent.JoinHabboGroupMessageEvent, + new JoinHabboGroupMessageParser() + ); + builder.MapParser(MessageEvent.KickMemberMessageEvent, new KickMemberMessageParser()); + builder.MapParser( + MessageEvent.RejectMembershipRequestMessageEvent, + new RejectMembershipRequestMessageParser() + ); + builder.MapParser( + MessageEvent.RemoveAdminRightsFromMemberMessageEvent, + new RemoveAdminRightsFromMemberMessageParser() + ); + builder.MapParser( + MessageEvent.ScrGetKickbackInfoMessageEvent, + new ScrGetKickbackInfoMessageParser() + ); + builder.MapParser( + MessageEvent.ScrGetUserInfoMessageEvent, + new ScrGetUserInfoMessageParser() + ); + builder.MapParser( + MessageEvent.SelectFavouriteHabboGroupMessageEvent, + new SelectFavouriteHabboGroupMessageParser() + ); + builder.MapParser(MessageEvent.BlockUserMessageEvent, new BlockUserMessageParser()); + builder.MapParser( + MessageEvent.UnblockGroupMemberMessageEvent, + new UnblockGroupMemberMessageParser() + ); + builder.MapParser(MessageEvent.UnblockUserMessageEvent, new UnblockUserMessageParser()); + builder.MapParser(MessageEvent.UnignoreUserMessageEvent, new UnignoreUserMessageParser()); + builder.MapParser( + MessageEvent.UpdateGuildBadgeMessageEvent, + new UpdateGuildBadgeMessageParser() + ); + builder.MapParser( + MessageEvent.UpdateGuildColorsMessageEvent, + new UpdateGuildColorsMessageParser() + ); + builder.MapParser( + MessageEvent.UpdateGuildIdentityMessageEvent, + new UpdateGuildIdentityMessageParser() + ); + builder.MapParser( + MessageEvent.UpdateGuildSettingsMessageEvent, + new UpdateGuildSettingsMessageParser() + ); + + builder.MapSerializer( + typeof(GuildCreatedMessageComposer), + new GuildCreatedMessageComposerSerializer(MessageComposer.GuildCreatedMessageComposer) + ); + builder.MapSerializer( + typeof(GuildCreationInfoMessageComposer), + new GuildCreationInfoMessageComposerSerializer( + MessageComposer.GuildCreationInfoMessageComposer + ) + ); + builder.MapSerializer( + typeof(HabboGroupDetailsMessageComposer), + new HabboGroupDetailsMessageComposerSerializer( + MessageComposer.HabboGroupDetailsMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildMembershipsMessageComposer), + new GuildMembershipsMessageComposerSerializer( + MessageComposer.GuildMembershipsMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildMembersMessageComposer), + new GuildMembersMessageComposerSerializer(MessageComposer.GuildMembersMessageComposer) + ); + builder.MapSerializer( + typeof(HabboGroupJoinFailedMessageComposer), + new HabboGroupJoinFailedMessageComposerSerializer( + MessageComposer.HabboGroupJoinFailedMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildEditInfoMessageComposer), + new GuildEditInfoMessageComposerSerializer(MessageComposer.GuildEditInfoMessageComposer) + ); + builder.MapSerializer( + typeof(GuildEditorDataMessageComposer), + new GuildEditorDataMessageComposerSerializer( + MessageComposer.GuildEditorDataMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildEditFailedMessageComposer), + new GuildEditFailedMessageComposerSerializer( + MessageComposer.GuildEditFailedMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildMembershipUpdatedMessageComposer), + new GuildMembershipUpdatedMessageComposerSerializer( + MessageComposer.GuildMembershipUpdatedMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildMembershipRejectedMessageComposer), + new GuildMembershipRejectedMessageComposerSerializer( + MessageComposer.GuildMembershipRejectedMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildMemberMgmtFailedMessageComposer), + new GuildMemberMgmtFailedMessageComposerSerializer( + MessageComposer.GuildMemberMgmtFailedMessageComposer + ) + ); + builder.MapSerializer( + typeof(HabboGroupDeactivatedMessageComposer), + new HabboGroupDeactivatedMessageComposerSerializer( + MessageComposer.HabboGroupDeactivatedMessageComposer + ) + ); + builder.MapSerializer( + typeof(GroupMembershipRequestedMessageComposer), + new GroupMembershipRequestedMessageComposerSerializer( + MessageComposer.GroupMembershipRequestedMessageComposer + ) + ); + builder.MapSerializer( + typeof(HabboGroupBadgesMessageComposer), + new HabboGroupBadgesMessageComposerSerializer( + MessageComposer.HabboGroupBadgesMessageComposer + ) + ); + builder.MapSerializer( + typeof(GuildMemberFurniCountInHQMessageComposer), + new GuildMemberFurniCountInHQMessageComposerSerializer( + MessageComposer.GuildMemberFurniCountInHQMessageComposer + ) + ); + builder.MapSerializer( + typeof(GroupDetailsChangedMessageComposer), + new GroupDetailsChangedMessageComposerSerializer( + MessageComposer.GroupDetailsChangedMessageComposer + ) + ); + builder.MapSerializer( + typeof(AccountSafetyLockStatusChangeMessageComposer), + new AccountSafetyLockStatusChangeMessageComposerSerializer( + MessageComposer.AccountSafetyLockStatusChangeMessageComposer + ) + ); + builder.MapSerializer( + typeof(ApproveNameMessageComposer), + new ApproveNameMessageComposerSerializer(MessageComposer.ApproveNameMessageComposer) + ); + builder.MapSerializer( + typeof(ChangeEmailResultEventMessageComposer), + new ChangeEmailResultEventMessageComposerSerializer( + MessageComposer.ChangeEmailResultComposer + ) + ); + builder.MapSerializer( + typeof(EmailStatusResultEventMessageComposer), + new EmailStatusResultEventMessageComposerSerializer( + MessageComposer.EmailStatusResultComposer + ) + ); + builder.MapSerializer( + typeof(ExtendedProfileMessageComposer), + new ExtendedProfileMessageComposerSerializer( + MessageComposer.ExtendedProfileMessageComposer + ) + ); + builder.MapSerializer( + typeof(ExtendedProfileChangedMessageComposer), + new ExtendedProfileChangedMessageComposerSerializer( + MessageComposer.ExtendedProfileChangedMessageComposer + ) + ); + builder.MapSerializer( + typeof(BlockListMessageComposer), + new BlockListMessageComposerSerializer(MessageComposer.BlockListMessageComposer) + ); + builder.MapSerializer( + typeof(BlockUserUpdateMessageComposer), + new BlockUserUpdateMessageComposerSerializer( + MessageComposer.BlockUserUpdateMessageComposer + ) + ); + builder.MapSerializer( + typeof(IgnoreResultMessageComposer), + new IgnoreResultMessageComposerSerializer(MessageComposer.IgnoreResultMessageComposer) + ); + builder.MapSerializer( + typeof(IgnoredUsersMessageComposer), + new IgnoredUsersMessageComposerSerializer(MessageComposer.IgnoredUsersMessageComposer) + ); + builder.MapSerializer( + typeof(RelationshipStatusInfoEventMessageComposer), + new RelationshipStatusInfoEventMessageComposerSerializer( + MessageComposer.RelationshipStatusInfoComposer + ) + ); + builder.MapSerializer( + typeof(ScrSendKickbackInfoMessageComposer), + new ScrSendKickbackInfoMessageComposerSerializer( + MessageComposer.ScrSendKickbackInfoMessageComposer + ) + ); + builder.MapSerializer( + typeof(ScrSendUserInfoMessageComposer), + new ScrSendUserInfoMessageSerializer(MessageComposer.ScrSendUserInfoComposer) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Maps/VaultMap.cs b/Vortex.Revisions/Revision20260701/Maps/VaultMap.cs new file mode 100644 index 00000000..cf790ccf --- /dev/null +++ b/Vortex.Revisions/Revision20260701/Maps/VaultMap.cs @@ -0,0 +1,42 @@ +using Vortex.Primitives.Messages.Outgoing.Vault; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Revisions.Revision20260701.Parsers.Vault; +using Vortex.Revisions.Revision20260701.Serializers.Vault; + +namespace Vortex.Revisions.Revision20260701.Maps; + +internal sealed class VaultMap : IRevisionMap +{ + public void RegisterInto(IRevisionMapBuilder builder) + { + builder.MapParser( + MessageEvent.CreditVaultStatusMessageEvent, + new CreditVaultStatusMessageParser() + ); + builder.MapParser( + MessageEvent.IncomeRewardClaimMessageEvent, + new IncomeRewardClaimMessageParser() + ); + builder.MapParser( + MessageEvent.IncomeRewardStatusMessageEvent, + new IncomeRewardStatusMessageParser() + ); + builder.MapParser( + MessageEvent.WithdrawCreditVaultMessageEvent, + new WithdrawCreditVaultMessageParser() + ); + + builder.MapSerializer( + typeof(IncomeRewardClaimResponseMessageComposer), + new IncomeRewardClaimResponseMessageComposerSerializer( + MessageComposer.IncomeRewardClaimResponseMessageComposer + ) + ); + builder.MapSerializer( + typeof(IncomeRewardStatusMessageComposer), + new IncomeRewardStatusMessageComposerSerializer( + MessageComposer.IncomeRewardStatusMessageComposer + ) + ); + } +} diff --git a/Vortex.Revisions/Revision20260701/Revision20260701.cs b/Vortex.Revisions/Revision20260701/Revision20260701.cs index a03a3612..342a78c3 100644 --- a/Vortex.Revisions/Revision20260701/Revision20260701.cs +++ b/Vortex.Revisions/Revision20260701/Revision20260701.cs @@ -1,3612 +1,59 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Options; -using Vortex.Primitives.Messages.Outgoing.Advertisement; -using Vortex.Primitives.Messages.Outgoing.Availability; -using Vortex.Primitives.Messages.Outgoing.Avatar; -using Vortex.Primitives.Messages.Outgoing.Callforhelp; -using Vortex.Primitives.Messages.Outgoing.Camera; -using Vortex.Primitives.Messages.Outgoing.Campaign; -using Vortex.Primitives.Messages.Outgoing.Catalog; -using Vortex.Primitives.Messages.Outgoing.Collectibles; -using Vortex.Primitives.Messages.Outgoing.Competition; -using Vortex.Primitives.Messages.Outgoing.FriendList; -using Vortex.Primitives.Messages.Outgoing.Groupforums; -using Vortex.Primitives.Messages.Outgoing.Handshake; -using Vortex.Primitives.Messages.Outgoing.Inventory.Achievements; -using Vortex.Primitives.Messages.Outgoing.Inventory.Avatareffect; -using Vortex.Primitives.Messages.Outgoing.Inventory.Badges; -using Vortex.Primitives.Messages.Outgoing.Inventory.Bots; -using Vortex.Primitives.Messages.Outgoing.Inventory.Clothing; -using Vortex.Primitives.Messages.Outgoing.Inventory.Furni; -using Vortex.Primitives.Messages.Outgoing.Inventory.Pets; -using Vortex.Primitives.Messages.Outgoing.Inventory.Purse; -using Vortex.Primitives.Messages.Outgoing.Inventory.Trading; -using Vortex.Primitives.Messages.Outgoing.Marketplace; -using Vortex.Primitives.Messages.Outgoing.Mysterybox; -using Vortex.Primitives.Messages.Outgoing.Navigator; -using Vortex.Primitives.Messages.Outgoing.NewNavigator; -using Vortex.Primitives.Messages.Outgoing.Nft; -using Vortex.Primitives.Messages.Outgoing.Notifications; -using Vortex.Primitives.Messages.Outgoing.Perk; -using Vortex.Primitives.Messages.Outgoing.Preferences; -using Vortex.Primitives.Messages.Outgoing.Quest; -using Vortex.Primitives.Messages.Outgoing.Room.Action; -using Vortex.Primitives.Messages.Outgoing.Room.Bots; -using Vortex.Primitives.Messages.Outgoing.Room.Chat; -using Vortex.Primitives.Messages.Outgoing.Room.Engine; -using Vortex.Primitives.Messages.Outgoing.Room.Furniture; -using Vortex.Primitives.Messages.Outgoing.Room.Layout; -using Vortex.Primitives.Messages.Outgoing.Room.Permissions; -using Vortex.Primitives.Messages.Outgoing.Room.Pets; -using Vortex.Primitives.Messages.Outgoing.Room.Session; -using Vortex.Primitives.Messages.Outgoing.Tracking; -using Vortex.Primitives.Messages.Outgoing.Userdefinedroomevents; -using Vortex.Primitives.Messages.Outgoing.Userdefinedroomevents.Wiredmenu; -using Vortex.Primitives.Messages.Outgoing.Users; -using Vortex.Primitives.Messages.Outgoing.Vault; using Vortex.Primitives.Networking.Revisions; -using Vortex.Primitives.Packets; using Vortex.Revisions.Configuration; -using Vortex.Revisions.Revision20260701.Parsers.Advertisement; -using Vortex.Revisions.Revision20260701.Parsers.Avatar; -using Vortex.Revisions.Revision20260701.Parsers.Camera; -using Vortex.Revisions.Revision20260701.Parsers.Campaign; -using Vortex.Revisions.Revision20260701.Parsers.Catalog; -using Vortex.Revisions.Revision20260701.Parsers.Collectibles; -using Vortex.Revisions.Revision20260701.Parsers.Competition; -using Vortex.Revisions.Revision20260701.Parsers.Crafting; -using Vortex.Revisions.Revision20260701.Parsers.FriendFurni; -using Vortex.Revisions.Revision20260701.Parsers.FriendList; -using Vortex.Revisions.Revision20260701.Parsers.Game.Arena; -using Vortex.Revisions.Revision20260701.Parsers.Game.Directory; -using Vortex.Revisions.Revision20260701.Parsers.Game.Ingame; -using Vortex.Revisions.Revision20260701.Parsers.Game.Lobby; -using Vortex.Revisions.Revision20260701.Parsers.Game.Score; -using Vortex.Revisions.Revision20260701.Parsers.Gifts; -using Vortex.Revisions.Revision20260701.Parsers.GroupForums; -using Vortex.Revisions.Revision20260701.Parsers.Handshake; -using Vortex.Revisions.Revision20260701.Parsers.Help; -using Vortex.Revisions.Revision20260701.Parsers.Hotlooks; -using Vortex.Revisions.Revision20260701.Parsers.Inventory.Achievements; -using Vortex.Revisions.Revision20260701.Parsers.Inventory.Avatareffect; -using Vortex.Revisions.Revision20260701.Parsers.Inventory.Badges; -using Vortex.Revisions.Revision20260701.Parsers.Inventory.Bots; -using Vortex.Revisions.Revision20260701.Parsers.Inventory.Furni; -using Vortex.Revisions.Revision20260701.Parsers.Inventory.Pets; -using Vortex.Revisions.Revision20260701.Parsers.Inventory.Purse; -using Vortex.Revisions.Revision20260701.Parsers.Inventory.Trading; -using Vortex.Revisions.Revision20260701.Parsers.LandingView; -using Vortex.Revisions.Revision20260701.Parsers.LandingView.Votes; -using Vortex.Revisions.Revision20260701.Parsers.Marketplace; -using Vortex.Revisions.Revision20260701.Parsers.Moderator; -using Vortex.Revisions.Revision20260701.Parsers.MysteryBox; -using Vortex.Revisions.Revision20260701.Parsers.Navigator; -using Vortex.Revisions.Revision20260701.Parsers.NewNavigator; -using Vortex.Revisions.Revision20260701.Parsers.Nft; -using Vortex.Revisions.Revision20260701.Parsers.Notifications; -using Vortex.Revisions.Revision20260701.Parsers.Nux; -using Vortex.Revisions.Revision20260701.Parsers.Poll; -using Vortex.Revisions.Revision20260701.Parsers.Preferences; -using Vortex.Revisions.Revision20260701.Parsers.Quest; -using Vortex.Revisions.Revision20260701.Parsers.Register; -using Vortex.Revisions.Revision20260701.Parsers.Room.Action; -using Vortex.Revisions.Revision20260701.Parsers.Room.Avatar; -using Vortex.Revisions.Revision20260701.Parsers.Room.Bots; -using Vortex.Revisions.Revision20260701.Parsers.Room.Chat; -using Vortex.Revisions.Revision20260701.Parsers.Room.Engine; -using Vortex.Revisions.Revision20260701.Parsers.Room.Furniture; -using Vortex.Revisions.Revision20260701.Parsers.Room.Layout; -using Vortex.Revisions.Revision20260701.Parsers.Room.Pets; -using Vortex.Revisions.Revision20260701.Parsers.Room.Session; -using Vortex.Revisions.Revision20260701.Parsers.RoomDirectory; -using Vortex.Revisions.Revision20260701.Parsers.RoomSettings; -using Vortex.Revisions.Revision20260701.Parsers.Sound; -using Vortex.Revisions.Revision20260701.Parsers.Talent; -using Vortex.Revisions.Revision20260701.Parsers.Tracking; -using Vortex.Revisions.Revision20260701.Parsers.UserClassification; -using Vortex.Revisions.Revision20260701.Parsers.UserDefinedRoomEvents; -using Vortex.Revisions.Revision20260701.Parsers.UserDefinedRoomEvents.Wiredmenu; -using Vortex.Revisions.Revision20260701.Parsers.Users; -using Vortex.Revisions.Revision20260701.Parsers.Vault; -using Vortex.Revisions.Revision20260701.Serializers.Advertisement; -using Vortex.Revisions.Revision20260701.Serializers.Availability; -using Vortex.Revisions.Revision20260701.Serializers.Avatar; -using Vortex.Revisions.Revision20260701.Serializers.CallForHelp; -using Vortex.Revisions.Revision20260701.Serializers.Camera; -using Vortex.Revisions.Revision20260701.Serializers.Campaign; -using Vortex.Revisions.Revision20260701.Serializers.Catalog; -using Vortex.Revisions.Revision20260701.Serializers.Collectibles; -using Vortex.Revisions.Revision20260701.Serializers.Competition; -using Vortex.Revisions.Revision20260701.Serializers.FriendList; -using Vortex.Revisions.Revision20260701.Serializers.GroupForums; -using Vortex.Revisions.Revision20260701.Serializers.Handshake; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Achievements; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Avatareffect; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Badges; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Bots; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Clothing; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Furni; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Pets; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Purse; -using Vortex.Revisions.Revision20260701.Serializers.Inventory.Trading; -using Vortex.Revisions.Revision20260701.Serializers.Marketplace; -using Vortex.Revisions.Revision20260701.Serializers.MysteryBox; -using Vortex.Revisions.Revision20260701.Serializers.Navigator; -using Vortex.Revisions.Revision20260701.Serializers.NewNavigator; -using Vortex.Revisions.Revision20260701.Serializers.Nft; -using Vortex.Revisions.Revision20260701.Serializers.Notifications; -using Vortex.Revisions.Revision20260701.Serializers.Perk; -using Vortex.Revisions.Revision20260701.Serializers.Preferences; -using Vortex.Revisions.Revision20260701.Serializers.Quest; -using Vortex.Revisions.Revision20260701.Serializers.Room.Action; -using Vortex.Revisions.Revision20260701.Serializers.Room.Bots; -using Vortex.Revisions.Revision20260701.Serializers.Room.Chat; -using Vortex.Revisions.Revision20260701.Serializers.Room.Engine; -using Vortex.Revisions.Revision20260701.Serializers.Room.Furniture; -using Vortex.Revisions.Revision20260701.Serializers.Room.Layout; -using Vortex.Revisions.Revision20260701.Serializers.Room.Permissions; -using Vortex.Revisions.Revision20260701.Serializers.Room.Pets; -using Vortex.Revisions.Revision20260701.Serializers.Room.Session; -using Vortex.Revisions.Revision20260701.Serializers.Tracking; -using Vortex.Revisions.Revision20260701.Serializers.UserDefinedRoomEvents; -using Vortex.Revisions.Revision20260701.Serializers.UserDefinedRoomEvents.Wiredmenu; -using Vortex.Revisions.Revision20260701.Serializers.Users; -using Vortex.Revisions.Revision20260701.Serializers.Vault; +using Vortex.Revisions.Revision20260701.Maps; namespace Vortex.Revisions.Revision20260701; -public class Revision20260701(IOptions protocolLimits) : IRevision -{ - public string Revision => "WIN63-202607011411-782849652"; - - #region Incoming - public IDictionary Parsers { get; } = - new Dictionary - { - #region Advertisement - { MessageEvent.GetInterstitialMessageEvent, new GetInterstitialMessageParser() }, - { MessageEvent.InterstitialShownMessageEvent, new InterstitialShownMessageParser() }, - #endregion - - #region Avatar - { - MessageEvent.ChangeUserNameInRoomMessageEvent, - new ChangeUserNameInRoomMessageParser() - }, - { MessageEvent.ChangeUserNameMessageEvent, new ChangeUserNameMessageParser() }, - { MessageEvent.CheckUserNameMessageEvent, new CheckUserNameMessageParser() }, - { MessageEvent.GetWardrobeMessageEvent, new GetWardrobeMessageParser() }, - { MessageEvent.SaveWardrobeOutfitMessageEvent, new SaveWardrobeOutfitMessageParser() }, - #endregion - - #region Camera - { MessageEvent.PhotoCompetitionMessageEvent, new PhotoCompetitionMessageParser() }, - { MessageEvent.PublishPhotoMessageEvent, new PublishPhotoMessageParser() }, - { MessageEvent.PurchasePhotoMessageEvent, new PurchasePhotoMessageParser() }, - { MessageEvent.RenderRoomMessageEvent, new RenderRoomMessageParser() }, - { - MessageEvent.RequestCameraConfigurationMessageEvent, - new RequestCameraConfigurationMessageParser() - }, - #endregion - - #region Campaign - { - MessageEvent.OpenCampaignCalendarDoorAsStaffEvent, - new OpenCampaignCalendarDoorAsStaffMessageParser() - }, - { - MessageEvent.OpenCampaignCalendarDoorEvent, - new OpenCampaignCalendarDoorMessageParser() - }, - #endregion - - #region Catalog - { - MessageEvent.BuildersClubPlaceRoomItemMessageEvent, - new BuildersClubPlaceRoomItemMessageParser() - }, - { - MessageEvent.BuildersClubPlaceWallItemMessageEvent, - new BuildersClubPlaceWallItemMessageParser() - }, - { - MessageEvent.BuildersClubQueryFurniCountMessageEvent, - new BuildersClubQueryFurniCountMessageParser() - }, - // charge firework? - { MessageEvent.GetBonusRareInfoMessageEvent, new GetBonusRareInfoMessageParser() }, - { - MessageEvent.GetBundleDiscountRulesetEvent, - new GetBundleDiscountRulesetMessageParser() - }, - { - MessageEvent.GetBundleDiscountRulesetVortexEvent, - new GetBundleDiscountRulesetMessageParser() - }, - { MessageEvent.GetCatalogIndexEvent, new GetCatalogIndexMessageParser() }, - { MessageEvent.GetCatalogPageEvent, new GetCatalogPageMessageParser() }, - { - MessageEvent.GetCatalogPageWithEarliestExpiryEvent, - new GetCatalogPageWithEarliestExpiryMessageParser() - }, - { MessageEvent.GetClubGiftMessageEvent, new GetClubGiftInfoMessageParser() }, - { MessageEvent.GetClubGiftInfoRequestMessageEvent, new GetClubGiftInfoMessageParser() }, - { MessageEvent.GetClubOffersMessageEvent, new GetClubOffersMessageParser() }, - { - MessageEvent.GetGiftWrappingConfigurationEvent, - new GetGiftWrappingConfigurationMessageParser() - }, - { - MessageEvent.GetGiftWrappingConfigurationVortexEvent, - new GetGiftWrappingConfigurationMessageParser() - }, - { - MessageEvent.GetHabboClubExtendOfferMessageEvent, - new GetHabboClubExtendOfferMessageParser() - }, - { MessageEvent.GetIsOfferGiftableEvent, new GetIsOfferGiftableMessageParser() }, - { - MessageEvent.GetLimitedOfferAppearingNextEvent, - new GetLimitedOfferAppearingNextMessageParser() - }, - { MessageEvent.GetNextTargetedOfferEvent, new GetNextTargetedOfferMessageParser() }, - { MessageEvent.GetProductOfferEvent, new GetProductOfferMessageParser() }, - { MessageEvent.GetRoomAdPurchaseInfoEvent, new GetRoomAdPurchaseInfoMessageParser() }, - { - MessageEvent.GetSeasonalCalendarDailyEvent, - new GetSeasonalCalendarDailyOfferMessageParser() - }, - { MessageEvent.GetSellablePetPalettesEvent, new GetSellablePetPalettesMessageParser() }, - { - MessageEvent.MarkCatalogNewAdditionsPageOpenedEvent, - new MarkCatalogNewAdditionsPageOpenedMessageParser() - }, - { - MessageEvent.PurchaseBasicMembershipExtensionEvent, - new PurchaseBasicMembershipExtensionMessageParser() - }, - { - MessageEvent.PurchaseFromCatalogAsGiftEvent, - new PurchaseFromCatalogAsGiftMessageParser() - }, - { MessageEvent.PurchaseFromCatalogEvent, new PurchaseFromCatalogMessageParser() }, - { MessageEvent.PurchaseRoomAdMessageEvent, new PurchaseRoomAdMessageParser() }, - { MessageEvent.PurchaseTargetedOfferEvent, new PurchaseTargetedOfferMessageParser() }, - { - MessageEvent.PurchaseVipMembershipExtensionEvent, - new PurchaseVipMembershipExtensionMessageParser() - }, - { MessageEvent.RedeemVoucherMessageEvent, new RedeemVoucherMessageParser() }, - { - MessageEvent.RoomAdPurchaseInitiatedEvent, - new RoomAdPurchaseInitiatedMessageParser() - }, - { MessageEvent.SelectClubGiftEvent, new SelectClubGiftMessageParser() }, - { MessageEvent.SetTargetedOfferStateEvent, new SetTargetedOfferStateMessageParser() }, - { - MessageEvent.ShopTargetedOfferViewedEvent, - new ShopTargetedOfferViewedMessageParser() - }, - #endregion - - #region Collectibles - { - MessageEvent.GetCollectibleMintableItemTypesMessageEvent, - new GetCollectibleMintableItemTypesMessageParser() - }, - { - MessageEvent.GetCollectibleMintingEnabledMessageEvent, - new GetCollectibleMintingEnabledMessageParser() - }, - { - MessageEvent.GetCollectibleMintTokensMessageEvent, - new GetCollectibleMintTokensMessageParser() - }, - { - MessageEvent.GetCollectibleWalletAddressesMessageEvent, - new GetCollectibleWalletAddressesMessageParser() - }, - { MessageEvent.GetCollectorScoreMessageEvent, new GetCollectorScoreMessageParser() }, - { MessageEvent.GetMintTokenOffersMessageEvent, new GetMintTokenOffersMessageParser() }, - { MessageEvent.GetNftCollectionsMessageEvent, new GetNftCollectionsMessageParser() }, - { MessageEvent.GetNftTransferFeeMessageEvent, new GetNftTransferFeeMessageParser() }, - { MessageEvent.MintItemMessageEvent, new MintItemMessageParser() }, - { - MessageEvent.NftCollectiblesClaimBonusItemMessageEvent, - new NftCollectiblesClaimBonusItemMessageParser() - }, - { - MessageEvent.NftCollectiblesClaimRewardItemMessageEvent, - new NftCollectiblesClaimRewardItemMessageParser() - }, - { MessageEvent.NftTransferAssetsMessageEvent, new NftTransferAssetsMessageParser() }, - { MessageEvent.PurchaseMintTokenMessageEvent, new PurchaseMintTokenMessageParser() }, - #endregion - - #region Competition - { - MessageEvent.ForwardToACompetitionRoomMessageEvent, - new ForwardToACompetitionRoomMessageParser() - }, - { - MessageEvent.ForwardToASubmittableRoomMessageEvent, - new ForwardToASubmittableRoomMessageParser() - }, - { - MessageEvent.ForwardToRandomCompetitionRoomMessageEvent, - new ForwardToRandomCompetitionRoomMessageParser() - }, - { - MessageEvent.GetCurrentTimingCodeMessageEvent, - new GetCurrentTimingCodeMessageParser() - }, - { - MessageEvent.GetIsUserPartOfCompetitionMessageEvent, - new GetIsUserPartOfCompetitionMessageParser() - }, - { MessageEvent.GetSecondsUntilMessageEvent, new GetSecondsUntilMessageParser() }, - { - MessageEvent.RoomCompetitionInitMessageEvent, - new RoomCompetitionInitMessageParser() - }, - { - MessageEvent.SubmitRoomToCompetitionMessageEvent, - new SubmitRoomToCompetitionMessageParser() - }, - { MessageEvent.VoteForRoomMessageEvent, new VoteForRoomMessageParser() }, - #endregion - - #region Crafting - { MessageEvent.CraftEvent, new CraftMessageParser() }, - { MessageEvent.CraftSecretEvent, new CraftSecretMessageParser() }, - { MessageEvent.GetCraftableProductsEvent, new GetCraftableProductsMessageParser() }, - { MessageEvent.GetCraftingRecipeEvent, new GetCraftingRecipeMessageParser() }, - { - MessageEvent.GetCraftingRecipesAvailableEvent, - new GetCraftingRecipesAvailableMessageParser() - }, - #endregion - - #region Friendfurni - { - MessageEvent.FriendFurniConfirmLockMessageEvent, - new FriendFurniConfirmLockMessageParser() - }, - #endregion - - #region FriendList - { MessageEvent.AcceptFriendMessageEvent, new AcceptFriendMessageParser() }, - { MessageEvent.DeclineFriendMessageEvent, new DeclineFriendMessageParser() }, - { MessageEvent.FindNewFriendsMessageEvent, new FindNewFriendsMessageParser() }, - { MessageEvent.FollowFriendMessageEvent, new FollowFriendMessageParser() }, - { MessageEvent.FriendListUpdateMessageEvent, new FriendListUpdateMessageParser() }, - { MessageEvent.GetFriendRequestsMessageEvent, new GetFriendRequestsMessageParser() }, - { MessageEvent.GetMessengerHistoryEvent, new GetMessengerHistoryMessageParser() }, - { MessageEvent.HabboSearchMessageEvent, new HabboSearchMessageParser() }, - { MessageEvent.MessengerInitMessageEvent, new MessengerInitMessageParser() }, - { - MessageEvent.RemoveFriendMessageEvent, - new RemoveFriendMessageParser(protocolLimits.Value.MaxFriendRemovalIds) - }, - { MessageEvent.RequestFriendMessageEvent, new RequestFriendMessageParser() }, - { MessageEvent.SendMsgMessageEvent, new SendMsgMessageParser() }, - { MessageEvent.SendRoomInviteMessageEvent, new SendRoomInviteMessageParser() }, - { MessageEvent.VisitUserMessageEvent, new VisitUserMessageParser() }, - #endregion - - #region Game - - #region Game Arena - { MessageEvent.Game2ExitGameMessageEvent, new Game2ExitGameMessageParser() }, - { MessageEvent.Game2GameChatMessageEvent, new Game2GameChatMessageParser() }, - { - MessageEvent.Game2LoadStageReadyMessageEvent, - new Game2LoadStageReadyMessageParser() - }, - { MessageEvent.Game2PlayAgainMessageEvent, new Game2PlayAgainMessageParser() }, - #endregion - - #region Game Directory - { - MessageEvent.Game2CheckGameDirectoryStatusMessageEvent, - new Game2CheckGameDirectoryStatusMessageParser() - }, - { - MessageEvent.Game2GetAccountGameStatusMessageEvent, - new Game2GetAccountGameStatusMessageParser() - }, - { MessageEvent.Game2LeaveGameMessageEvent, new Game2LeaveGameMessageParser() }, - { MessageEvent.Game2QuickJoinGameMessageEvent, new Game2QuickJoinGameMessageParser() }, - { MessageEvent.Game2StartSnowWarMessageEvent, new Game2StartSnowWarMessageParser() }, - #endregion - - #region Game Ingame - { MessageEvent.Game2MakeSnowballMessageEvent, new Game2MakeSnowballMessageParser() }, - { - MessageEvent.Game2RequestFullStatusUpdateMessageEvent, - new Game2RequestFullStatusUpdateMessageParser() - }, - { - MessageEvent.Game2SetUserMoveTargetMessageEvent, - new Game2SetUserMoveTargetMessageParser() - }, - { - MessageEvent.Game2ThrowSnowballAtHumanMessageEvent, - new Game2ThrowSnowballAtHumanMessageParser() - }, - { - MessageEvent.Game2ThrowSnowballAtPositionMessageEvent, - new Game2ThrowSnowballAtPositionMessageParser() - }, - #endregion - - #region Game Lobby - { - MessageEvent.GetResolutionAchievementsMessageEvent, - new GetResolutionAchievementsMessageParser() - }, - /* { - MessageEvent.GetUserGameAchievementsMessageEvent, - new GetUserGameAchievementsMessageParser() - }, */ - #endregion - - #region Game Score - { - MessageEvent.Game2GetFriendsLeaderboardEvent, - new Game2GetFriendsLeaderboardMessageParser() - }, - { - MessageEvent.Game2GetTotalGroupLeaderboardEvent, - new Game2GetTotalGroupLeaderboardMessageParser() - }, - { - MessageEvent.Game2GetTotalLeaderboardEvent, - new Game2GetTotalLeaderboardMessageParser() - }, - { - MessageEvent.Game2GetWeeklyFriendsLeaderboardEvent, - new Game2GetWeeklyFriendsLeaderboardMessageParser() - }, - { - MessageEvent.Game2GetWeeklyGroupLeaderboardEvent, - new Game2GetWeeklyGroupLeaderboardMessageParser() - }, - { - MessageEvent.Game2GetWeeklyLeaderboardEvent, - new Game2GetWeeklyLeaderboardMessageParser() - }, - /* { - MessageEvent.GetFriendsWeeklyCompetitiveLeaderboardMessageEvent, - new GetFriendsWeeklyCompetitiveLeaderboardMessageParser() - }, */ - - /* { - MessageEvent.GetWeeklyCompetitiveLeaderboardMessageEvent, - new GetWeeklyCompetitiveLeaderboardMessageParser() - }, - { - MessageEvent.GetWeeklyGameRewardMessageEvent, - new GetWeeklyGameRewardMessageParser() - }, - { - MessageEvent.GetWeeklyGameRewardWinnersMessageEvent, - new GetWeeklyGameRewardWinnersMessageParser() - }, */ - #endregion - - #endregion - - #region Gifts - { - MessageEvent.ResetPhoneNumberStateMessageEvent, - new ResetPhoneNumberStateMessageParser() - }, - { - MessageEvent.SetPhoneNumberVerificationStatusMessageEvent, - new SetPhoneNumberVerificationStatusMessageParser() - }, - { MessageEvent.TryPhoneNumberMessageEvent, new TryPhoneNumberMessageParser() }, - { MessageEvent.VerifyCodeMessageEvent, new VerifyCodeMessageParser() }, - #endregion - - #region GroupForums - { MessageEvent.GetForumsListMessageEvent, new GetForumsListMessageParser() }, - { MessageEvent.GetForumStatsMessageEvent, new GetForumStatsMessageParser() }, - { MessageEvent.GetMessagesMessageEvent, new GetMessagesMessageParser() }, - { MessageEvent.GetThreadMessageEvent, new GetThreadMessageParser() }, - { MessageEvent.GetThreadsMessageEvent, new GetThreadsMessageParser() }, - { - MessageEvent.GetUnreadForumsCountMessageEvent, - new GetUnreadForumsCountMessageParser() - }, - { MessageEvent.ModerateMessageMessageEvent, new ModerateMessageMessageParser() }, - { MessageEvent.ModerateThreadMessageEvent, new ModerateThreadMessageParser() }, - { MessageEvent.PostMessageMessageEvent, new PostMessageMessageParser() }, - { - MessageEvent.UpdateForumReadMarkerMessageEvent, - new UpdateForumReadMarkerMessageParser() - }, - { - MessageEvent.UpdateForumSettingsMessageEvent, - new UpdateForumSettingsMessageParser() - }, - { MessageEvent.UpdateThreadMessageEvent, new UpdateThreadMessageParser() }, - #endregion - - #region Handshake - { MessageEvent.ClientHelloMessageEvent, new ClientHelloMessageParser() }, - { - MessageEvent.CompleteDiffieHandshakeMessageEvent, - new CompleteDiffieHandshakeMessageParser() - }, - { MessageEvent.DisconnectMessageEvent, new DisconnectMessageParser() }, - { MessageEvent.InfoRetrieveMessageEvent, new InfoRetrieveMessageParser() }, - { - MessageEvent.InitDiffieHandshakeMessageEvent, - new InitDiffieHandshakeMessageParser() - }, - { MessageEvent.PongMessageEvent, new PongMessageParser() }, - { MessageEvent.SSOTicketMessageEvent, new SSOTicketMessageParser() }, - { MessageEvent.UniqueIDMessageEvent, new UniqueIdMessageParser() }, - { MessageEvent.VersionCheckMessageEvent, new VersionCheckMessageParser() }, - #endregion - - #region Help - { - MessageEvent.CallForHelpFromForumMessageMessageEvent, - new CallForHelpFromForumMessageMessageParser() - }, - { - MessageEvent.CallForHelpFromForumThreadMessageEvent, - new CallForHelpFromForumThreadMessageParser() - }, - { MessageEvent.CallForHelpFromIMMessageEvent, new CallForHelpFromIMMessageParser() }, - { - MessageEvent.CallForHelpFromPhotoMessageEvent, - new CallForHelpFromPhotoMessageParser() - }, - { - MessageEvent.CallForHelpFromSelfieMessageEvent, - new CallForHelpFromSelfieMessageParser() - }, - { MessageEvent.CallForHelpMessageEvent, new CallForHelpMessageParser() }, - { - MessageEvent.ChatReviewGuideDecidesOnOfferMessageEvent, - new ChatReviewGuideDecidesOnOfferMessageParser() - }, - { - MessageEvent.ChatReviewGuideDetachedMessageEvent, - new ChatReviewGuideDetachedMessageParser() - }, - { - MessageEvent.ChatReviewGuideVoteMessageEvent, - new ChatReviewGuideVoteMessageParser() - }, - { - MessageEvent.ChatReviewSessionCreateMessageEvent, - new ChatReviewSessionCreateMessageParser() - }, - { - MessageEvent.DeletePendingCallsForHelpMessageEvent, - new DeletePendingCallsForHelpMessageParser() - }, - { MessageEvent.GetCfhStatusMessageEvent, new GetCfhStatusMessageParser() }, - { - MessageEvent.GetGuideReportingStatusMessageEvent, - new GetGuideReportingStatusMessageParser() - }, - { - MessageEvent.GetPendingCallsForHelpMessageEvent, - new GetPendingCallsForHelpMessageParser() - }, - { MessageEvent.GetQuizQuestionsEvent, new GetQuizQuestionsMessageParser() }, - { MessageEvent.GuideSessionCreateMessageEvent, new GuideSessionCreateMessageParser() }, - { - MessageEvent.GuideSessionFeedbackMessageEvent, - new GuideSessionFeedbackMessageParser() - }, - { - MessageEvent.GuideSessionGetRequesterRoomMessageEvent, - new GuideSessionGetRequesterRoomMessageParser() - }, - { - MessageEvent.GuideSessionGuideDecidesMessageEvent, - new GuideSessionGuideDecidesMessageParser() - }, - { - MessageEvent.GuideSessionInviteRequesterMessageEvent, - new GuideSessionInviteRequesterMessageParser() - }, - { - MessageEvent.GuideSessionIsTypingMessageEvent, - new GuideSessionIsTypingMessageParser() - }, - { - MessageEvent.GuideSessionMessageMessageEvent, - new GuideSessionMessageMessageParser() - }, - { - MessageEvent.GuideSessionOnDutyUpdateMessageEvent, - new GuideSessionOnDutyUpdateMessageParser() - }, - { MessageEvent.GuideSessionReportMessageEvent, new GuideSessionReportMessageParser() }, - { - MessageEvent.GuideSessionRequesterCancelsMessageEvent, - new GuideSessionRequesterCancelsMessageParser() - }, - { - MessageEvent.GuideSessionResolvedMessageEvent, - new GuideSessionResolvedMessageParser() - }, - { MessageEvent.PostQuizAnswersEvent, new PostQuizAnswersMessageParser() }, - #endregion - - #region Hotlooks - { MessageEvent.GetHotLooksMessageEvent, new GetHotLooksMessageParser() }, - #endregion - - #region Inventory - - #region Inventory Achievements - { MessageEvent.GetAchievementsEvent, new GetAchievementsMessageParser() }, - #endregion - - #region Inventory Avatar Effects - { MessageEvent.AvatarEffectActivatedEvent, new AvatarEffectActivatedMessageParser() }, - { MessageEvent.AvatarEffectSelectedEvent, new AvatarEffectSelectedMessageParser() }, - #endregion - - #region Inventory Badges - { MessageEvent.GetBadgePointLimitsEvent, new GetBadgePointLimitsMessageParser() }, - { MessageEvent.GetBadgesEvent, new GetBadgesMessageParser() }, - { - MessageEvent.GetIsBadgeRequestFulfilledEvent, - new GetIsBadgeRequestFulfilledMessageParser() - }, - { MessageEvent.RequestABadgeEvent, new RequestABadgeMessageParser() }, - { MessageEvent.SetActivatedBadgesEvent, new SetActivatedBadgesMessageParser() }, - #endregion - - #region Inventory Bots - { MessageEvent.GetBotInventoryEvent, new GetBotInventoryMessageParser() }, - #endregion - - #region Inventory Furni - { MessageEvent.RequestFurniInventoryEvent, new RequestFurniInventoryMessageParser() }, - { - MessageEvent.RequestFurniInventoryWhenNotInRoomEvent, - new RequestFurniInventoryWhenNotInRoomMessageParser() - }, - { MessageEvent.RequestRoomPropertySet, new RequestRoomPropertySetMessageParser() }, - #endregion - - #region Inventory Pets - { MessageEvent.CancelPetBreedingEvent, new CancelPetBreedingMessageParser() }, - { MessageEvent.ConfirmPetBreedingEvent, new ConfirmPetBreedingMessageParser() }, - { MessageEvent.GetPetInventoryEvent, new GetPetInventoryMessageParser() }, - #endregion - - #region Inventory Purse - { MessageEvent.GetCreditsInfoEvent, new GetCreditsInfoMessageParser() }, - #endregion - - #region Inventory Trading - { MessageEvent.AcceptTradingEvent, new AcceptTradingMessageParser() }, - { - MessageEvent.AddItemsToTradeEvent, - new AddItemsToTradeMessageParser(protocolLimits.Value.MaxTradeItems) - }, - { MessageEvent.AddItemToTradeEvent, new AddItemToTradeMessageParser() }, - { MessageEvent.CloseTradingEvent, new CloseTradingMessageParser() }, - { MessageEvent.ConfirmAcceptTradingEvent, new ConfirmAcceptTradingMessageParser() }, - { MessageEvent.ConfirmDeclineTradingEvent, new ConfirmDeclineTradingMessageParser() }, - { MessageEvent.OpenTradingEvent, new OpenTradingMessageParser() }, - { MessageEvent.RemoveItemFromTradeEvent, new RemoveItemFromTradeMessageParser() }, - { MessageEvent.SilverFeeMessageEvent, new SilverFeeMessageParser() }, - { MessageEvent.UnacceptTradingEvent, new UnacceptTradingMessageParser() }, - #endregion - - #endregion - - #region Landingview - { MessageEvent.CommunityGoalVoteMessageEvent, new CommunityGoalVoteMessageParser() }, - { MessageEvent.GetPromoArticlesMessageEvent, new GetPromoArticlesMessageParser() }, - #endregion - - #region Marketplace - { - MessageEvent.BuyMarketplaceOfferMessageEvent, - new BuyMarketplaceOfferMessageParser() - }, - { - MessageEvent.BuyMarketplaceTokensMessageEvent, - new BuyMarketplaceTokensMessageParser() - }, - { - MessageEvent.CancelMarketplaceOfferMessageEvent, - new CancelMarketplaceOfferMessageParser() - }, - { - MessageEvent.GetMarketplaceCanMakeOfferMessageEvent, - new GetMarketplaceCanMakeOfferMessageParser() - }, - { - MessageEvent.GetMarketplaceConfigurationMessageEvent, - new GetMarketplaceConfigurationMessageParser() - }, - { - MessageEvent.GetMarketplaceItemStatsEvent, - new GetMarketplaceItemStatsMessageParser() - }, - { - MessageEvent.GetMarketplaceOffersMessageEvent, - new GetMarketplaceOffersMessageParser() - }, - { - MessageEvent.GetMarketplaceOwnOffersMessageEvent, - new GetMarketplaceOwnOffersMessageParser() - }, - { MessageEvent.MakeOfferMessageEvent, new MakeOfferMessageParser() }, - { - MessageEvent.RedeemMarketplaceOfferCreditsMessageEvent, - new RedeemMarketplaceOfferCreditsMessageParser() - }, - #endregion - - #region Moderator - { - MessageEvent.CloseIssueDefaultActionMessageEvent, - new CloseIssueDefaultActionMessageParser() - }, - { MessageEvent.CloseIssuesMessageEvent, new CloseIssuesMessageParser() }, - { MessageEvent.DefaultSanctionMessageEvent, new DefaultSanctionMessageParser() }, - { MessageEvent.GetCfhChatlogMessageEvent, new GetCfhChatlogMessageParser() }, - { - MessageEvent.GetModeratorRoomInfoMessageEvent, - new GetModeratorRoomInfoMessageParser() - }, - { - MessageEvent.GetModeratorUserInfoMessageEvent, - new GetModeratorUserInfoMessageParser() - }, - { MessageEvent.GetRoomChatlogMessageEvent, new GetRoomChatlogMessageParser() }, - { MessageEvent.GetRoomVisitsMessageEvent, new GetRoomVisitsMessageParser() }, - { MessageEvent.GetUserChatlogMessageEvent, new GetUserChatlogMessageParser() }, - { MessageEvent.ModAlertMessageEvent, new ModAlertMessageParser() }, - { MessageEvent.ModBanMessageEvent, new ModBanMessageParser() }, - { MessageEvent.ModerateRoomMessageEvent, new ModerateRoomMessageParser() }, - { MessageEvent.ModeratorActionMessageEvent, new ModeratorActionMessageParser() }, - { MessageEvent.ModKickMessageEvent, new ModKickMessageParser() }, - { MessageEvent.ModMessageMessageEvent, new ModMessageMessageParser() }, - { MessageEvent.ModMuteMessageEvent, new ModMuteMessageParser() }, - { MessageEvent.ModToolPreferencesEvent, new ModToolPreferencesMessageParser() }, - { MessageEvent.ModToolSanctionEvent, new ModToolSanctionMessageParser() }, - { MessageEvent.ModTradingLockMessageEvent, new ModTradingLockMessageParser() }, - { MessageEvent.PickIssuesMessageEvent, new PickIssuesMessageParser() }, - { MessageEvent.ReleaseIssuesMessageEvent, new ReleaseIssuesMessageParser() }, - #endregion - - #region Mysterybox - { - MessageEvent.MysteryBoxWaitingCanceledMessageEvent, - new MysteryBoxWaitingCanceledMessageParser() - }, - #endregion - - #region Navigator - { MessageEvent.AddFavouriteRoomMessageEvent, new AddFavouriteRoomMessageParser() }, - { MessageEvent.CancelEventMessageEvent, new CancelEventMessageParser() }, - { MessageEvent.CanCreateRoomMessageEvent, new CanCreateRoomMessageParser() }, - { - MessageEvent.CompetitionRoomsSearchMessageEvent, - new CompetitionRoomsSearchMessageParser() - }, - { - MessageEvent.ConvertGlobalRoomIdMessageEvent, - new ConvertGlobalRoomIdMessageParser() - }, - { MessageEvent.CreateFlatMessageEvent, new CreateFlatMessageParser() }, - { - MessageEvent.DeleteFavouriteRoomMessageEvent, - new DeleteFavouriteRoomMessageParser() - }, - { MessageEvent.EditEventMessageEvent, new EditEventMessageParser() }, - { - MessageEvent.ForwardToARandomPromotedRoomMessageEvent, - new ForwardToARandomPromotedRoomMessageParser() - }, - { MessageEvent.ForwardToSomeRoomMessageEvent, new ForwardToSomeRoomMessageParser() }, - { MessageEvent.GetGuestRoomMessageEvent, new GetGuestRoomMessageParser() }, - { MessageEvent.GetOfficialRoomsMessageEvent, new GetOfficialRoomsMessageParser() }, - { MessageEvent.GetPopularRoomTagsMessageEvent, new GetPopularRoomTagsMessageParser() }, - { MessageEvent.GetUserEventCatsMessageEvent, new GetUserEventCatsMessageParser() }, - { MessageEvent.GetUserFlatCatsMessageEvent, new GetUserFlatCatsMessageParser() }, - { MessageEvent.GuildBaseSearchMessageEvent, new GuildBaseSearchMessageParser() }, - { - MessageEvent.MyFavouriteRoomsSearchMessageEvent, - new MyFavouriteRoomsSearchMessageParser() - }, - { - MessageEvent.MyFrequentRoomHistorySearchMessageEvent, - new MyFrequentRoomHistorySearchMessageParser() - }, - { - MessageEvent.MyFriendsRoomsSearchMessageEvent, - new MyFriendsRoomsSearchMessageParser() - }, - { MessageEvent.MyGuildBasesSearchMessageEvent, new MyGuildBasesSearchMessageParser() }, - { MessageEvent.MyRecommendedRoomsMessageEvent, new MyRecommendedRoomsMessageParser() }, - { - MessageEvent.MyRoomHistorySearchMessageEvent, - new MyRoomHistorySearchMessageParser() - }, - { MessageEvent.MyRoomRightsSearchMessageEvent, new MyRoomRightsSearchMessageParser() }, - { MessageEvent.MyRoomsSearchMessageEvent, new MyRoomsSearchMessageParser() }, - { MessageEvent.PopularRoomsSearchMessageEvent, new PopularRoomsSearchMessageParser() }, - { MessageEvent.RateFlatMessageEvent, new RateFlatMessageParser() }, - { - MessageEvent.RemoveOwnRoomRightsRoomMessageEvent, - new RemoveOwnRoomRightsRoomMessageParser() - }, - { - MessageEvent.RoomAdEventTabAdClickedEvent, - new RoomAdEventTabAdClickedMessageParser() - }, - { MessageEvent.RoomAdEventTabViewedEvent, new RoomAdEventTabViewedMessageParser() }, - { MessageEvent.RoomAdSearchMessageEvent, new RoomAdSearchMessageParser() }, - { - MessageEvent.RoomsWhereMyFriendsAreSearchMessageEvent, - new RoomsWhereMyFriendsAreSearchMessageParser() - }, - { - MessageEvent.RoomsWithHighestScoreSearchMessageEvent, - new RoomsWithHighestScoreSearchMessageParser() - }, - { MessageEvent.RoomTextSearchMessageEvent, new RoomTextSearchMessageParser() }, - { MessageEvent.SetRoomSessionTagsMessageEvent, new SetRoomSessionTagsMessageParser() }, - { MessageEvent.ToggleStaffPickMessageEvent, new ToggleStaffPickMessageParser() }, - { MessageEvent.UpdateHomeRoomMessageEvent, new UpdateHomeRoomMessageParser() }, - #endregion - - #region NewNavigator - { - MessageEvent.NavigatorAddCollapsedCategoryMessageEvent, - new NavigatorAddCollapsedCategoryMessageParser() - }, - { - MessageEvent.NavigatorAddSavedSearchEvent, - new NavigatorAddSavedSearchMessageParser() - }, - { - MessageEvent.NavigatorDeleteSavedSearchEvent, - new NavigatorDeleteSavedSearchMessageParser() - }, - { - MessageEvent.NavigatorRemoveCollapsedCategoryMessageEvent, - new NavigatorRemoveCollapsedCategoryMessageParser() - }, - { - MessageEvent.NavigatorSetSearchCodeViewModeMessageEvent, - new NavigatorSetSearchCodeViewModeMessageParser() - }, - { MessageEvent.NewNavigatorInitEvent, new NewNavigatorInitMessageParser() }, - { MessageEvent.NewNavigatorSearchEvent, new NewNavigatorSearchMessageParser() }, - #endregion - - #region Nft - { MessageEvent.GetNftCreditsMessageEvent, new GetNftCreditsMessageParser() }, - { - MessageEvent.GetSelectedNftWardrobeOutfitMessageEvent, - new GetSelectedNftWardrobeOutfitMessageParser() - }, - { MessageEvent.GetSilverMessageEvent, new GetSilverMessageParser() }, - { MessageEvent.GetUserNftWardrobeMessageEvent, new GetUserNftWardrobeMessageParser() }, - { - MessageEvent.SaveUserNftWardrobeMessageEvent, - new SaveUserNftWardrobeMessageParser() - }, - #endregion - - #region Notifications - { MessageEvent.ResetUnseenItemIdsEvent, new ResetUnseenItemIdsMessageParser() }, - { MessageEvent.ResetUnseenItemsEvent, new ResetUnseenItemsMessageParser() }, - #endregion - - #region Nux - { - MessageEvent.NewUserExperienceGetGiftsMessageEvent, - new NewUserExperienceGetGiftsMessageParser() - }, - { - MessageEvent.NewUserExperienceScriptProceedEvent, - new NewUserExperienceScriptProceedMessageParser() - }, - { MessageEvent.SelectInitialRoomEvent, new SelectInitialRoomMessageParser() }, - #endregion - - #region Poll - { MessageEvent.PollAnswerEvent, new PollAnswerMessageParser() }, - { MessageEvent.PollRejectEvent, new PollRejectMessageParser() }, - { MessageEvent.PollStartEvent, new PollStartMessageParser() }, - #endregion - - #region Preferences - { MessageEvent.SetChatPreferencesMessageEvent, new SetChatPreferencesMessageParser() }, - { MessageEvent.SetChatStylePreferenceEvent, new SetChatStylePreferenceMessageParser() }, - { - MessageEvent.SetIgnoreRoomInvitesMessageEvent, - new SetIgnoreRoomInvitesMessageParser() - }, - { - MessageEvent.SetNewNavigatorWindowPreferencesMessageEvent, - new SetNewNavigatorWindowPreferencesMessageParser() - }, - { - MessageEvent.SetRoomCameraPreferencesMessageEvent, - new SetRoomCameraPreferencesMessageParser() - }, - { MessageEvent.SetSoundSettingsEvent, new SetSoundSettingsMessageParser() }, - { MessageEvent.SetUIFlagsMessageEvent, new SetUIFlagsMessageParser() }, - #endregion - - #region Quest - { MessageEvent.AcceptQuestMessageEvent, new AcceptQuestMessageParser() }, - { MessageEvent.ActivateQuestMessageEvent, new ActivateQuestMessageParser() }, - { MessageEvent.CancelQuestMessageEvent, new CancelQuestMessageParser() }, - { - MessageEvent.FriendRequestQuestCompleteMessageEvent, - new FriendRequestQuestCompleteMessageParser() - }, - { - MessageEvent.GetCommunityGoalHallOfFameMessageEvent, - new GetCommunityGoalHallOfFameMessageParser() - }, - { - MessageEvent.GetCommunityGoalProgressMessageEvent, - new GetCommunityGoalProgressMessageParser() - }, - { - MessageEvent.GetConcurrentUsersGoalProgressMessageEvent, - new GetConcurrentUsersGoalProgressMessageParser() - }, - { - MessageEvent.GetConcurrentUsersRewardMessageEvent, - new GetConcurrentUsersRewardMessageParser() - }, - { MessageEvent.GetDailyTasksEvent, new GetDailyTasksMessageParser() }, - { MessageEvent.GetDailyQuestMessageEvent, new GetDailyQuestMessageParser() }, - { MessageEvent.GetQuestsMessageEvent, new GetQuestsMessageParser() }, - { - MessageEvent.GetSeasonalQuestsOnlyMessageEvent, - new GetSeasonalQuestsOnlyMessageParser() - }, - { MessageEvent.OpenQuestTrackerMessageEvent, new OpenQuestTrackerMessageParser() }, - { MessageEvent.RejectQuestMessageEvent, new RejectQuestMessageParser() }, - { MessageEvent.StartCampaignMessageEvent, new StartCampaignMessageParser() }, - #endregion - - #region Register - { MessageEvent.UpdateFigureDataMessageEvent, new UpdateFigureDataMessageParser() }, - #endregion - - #region Room - - #region Room Action - { MessageEvent.AmbassadorAlertMessageEvent, new AmbassadorAlertMessageParser() }, - { MessageEvent.AssignRightsMessageEvent, new AssignRightsMessageParser() }, - { - MessageEvent.BanUserWithDurationMessageEvent, - new BanUserWithDurationMessageParser() - }, - { MessageEvent.KickUserMessageEvent, new KickUserMessageParser() }, - { MessageEvent.LetUserInMessageEvent, new LetUserInMessageParser() }, - { MessageEvent.MuteAllInRoomEvent, new MuteAllInRoomMessageParser() }, - { MessageEvent.MuteUserMessageEvent, new MuteUserMessageParser() }, - { MessageEvent.RemoveAllRightsMessageEvent, new RemoveAllRightsMessageParser() }, - { MessageEvent.RemoveRightsMessageEvent, new RemoveRightsMessageParser() }, - { MessageEvent.UnbanUserFromRoomMessageEvent, new UnbanUserFromRoomMessageParser() }, - { MessageEvent.UnmuteUserMessageEvent, new UnmuteUserMessageParser() }, - #endregion - - #region Room Avatar - { MessageEvent.AvatarExpressionMessageEvent, new AvatarExpressionMessageParser() }, - { MessageEvent.ChangeMottoMessageEvent, new ChangeMottoMessageParser() }, - { MessageEvent.ChangePostureMessageEvent, new ChangePostureMessageParser() }, - { - MessageEvent.CustomizeAvatarWithFurniMessageEvent, - new CustomizeAvatarWithFurniMessageParser() - }, - { MessageEvent.DanceMessageEvent, new DanceMessageParser() }, - { MessageEvent.DropCarryItemMessageEvent, new DropCarryItemMessageParser() }, - { MessageEvent.LookToMessageEvent, new LookToMessageParser() }, - { MessageEvent.PassCarryItemMessageEvent, new PassCarryItemMessageParser() }, - { MessageEvent.PassCarryItemToPetMessageEvent, new PassCarryItemToPetMessageParser() }, - { MessageEvent.SignMessageEvent, new SignMessageParser() }, - #endregion - - #region Room Bots - { MessageEvent.CommandBotEvent, new CommandBotMessageParser() }, - { - MessageEvent.GetBotCommandConfigurationDataEvent, - new GetBotCommandConfigurationDataMessageParser() - }, - #endregion - - #region Room Chat - { MessageEvent.CancelTypingMessageEvent, new CancelTypingMessageParser() }, - { MessageEvent.ChatMessageEvent, new ChatMessageParser() }, - { MessageEvent.ShoutMessageEvent, new ShoutMessageParser() }, - { MessageEvent.StartTypingMessageEvent, new StartTypingMessageParser() }, - { MessageEvent.WhisperMessageEvent, new WhisperMessageParser() }, - #endregion - - #region Room Engine - { MessageEvent.ClickCharacterEvent, new ClickCharacterMessageParser() }, - { MessageEvent.ClickFurniMessageEvent, new ClickFurniMessageParser() }, - { - MessageEvent.GetFurnitureAliasesMessageEvent, - new GetFurnitureAliasesMessageParser() - }, - { MessageEvent.GetRoomEntryDataMessageEvent, new GetRoomEntryDataMessageParser() }, - { MessageEvent.GetItemDataMessageEvent, new GetItemDataMessageParser() }, - { MessageEvent.GetPetCommandsMessageEvent, new GetPetCommandsMessageParser() }, - { - MessageEvent.GiveSupplementToPetMessageEvent, - new GiveSupplementToPetMessageParser() - }, - { MessageEvent.MountPetMessageEvent, new MountPetMessageParser() }, - { MessageEvent.MoveAvatarMessageEvent, new MoveAvatarMessageParser() }, - { MessageEvent.MoveObjectMessageEvent, new MoveObjectMessageParser() }, - { MessageEvent.MovePetMessageEvent, new MovePetMessageParser() }, - { MessageEvent.MoveWallItemMessageEvent, new MoveWallItemMessageParser() }, - { - MessageEvent.VortexGetFurniEditorDataMessageEvent, - new VortexGetFurniEditorDataMessageParser() - }, - { - MessageEvent.VortexApplyFurniEditMessageEvent, - new VortexApplyFurniEditMessageParser() - }, - { - MessageEvent.VortexGetFurniDefinitionMessageEvent, - new VortexGetFurniDefinitionMessageParser() - }, - { - MessageEvent.VortexApplyFurniDefinitionMessageEvent, - new VortexApplyFurniDefinitionMessageParser() - }, - { MessageEvent.PickupObjectMessageEvent, new PickupObjectMessageParser() }, - { MessageEvent.PlaceBotMessageEvent, new PlaceBotMessageParser() }, - { MessageEvent.PlaceObjectMessageEvent, new PlaceObjectMessageParser() }, - { MessageEvent.PlacePetMessageEvent, new PlacePetMessageParser() }, - { MessageEvent.RemoveBotFromFlatMessageEvent, new RemoveBotFromFlatMessageParser() }, - { MessageEvent.RemoveItemMessageEvent, new RemoveItemMessageParser() }, - { MessageEvent.RemovePetFromFlatMessageEvent, new RemovePetFromFlatMessageParser() }, - { - MessageEvent.RemoveSaddleFromPetMessageEvent, - new RemoveSaddleFromPetMessageParser() - }, - { - MessageEvent.SetClothingChangeDataMessageEvent, - new SetClothingChangeDataMessageParser() - }, - { MessageEvent.SetItemDataMessageEvent, new SetItemDataMessageParser() }, - { MessageEvent.SetObjectDataMessageEvent, new SetObjectDataMessageParser() }, - { - MessageEvent.TogglePetBreedingPermissionMessageEvent, - new TogglePetBreedingPermissionMessageParser() - }, - { - MessageEvent.TogglePetRidingPermissionMessageEvent, - new TogglePetRidingPermissionMessageParser() - }, - { MessageEvent.UseFurnitureMessageEvent, new UseFurnitureMessageParser() }, - { MessageEvent.UseWallItemMessageEvent, new UseWallItemMessageParser() }, - #endregion - - #region Room Furniture - { MessageEvent.AddSpamWallPostItMessageEvent, new AddSpamWallPostItMessageParser() }, - { - MessageEvent.ControlYoutubeDisplayPlaybackMessageEvent, - new ControlYoutubeDisplayPlaybackMessageParser() - }, - { MessageEvent.CreditFurniRedeemMessageEvent, new CreditFurniRedeemMessageParser() }, - { MessageEvent.DiceOffMessageEvent, new DiceOffMessageParser() }, - { MessageEvent.EnterOneWayDoorMessageEvent, new EnterOneWayDoorMessageParser() }, - { - MessageEvent.ExtendRentOrBuyoutFurniMessageEvent, - new ExtendRentOrBuyoutFurniMessageParser() - }, - { - MessageEvent.ExtendRentOrBuyoutStripItemMessageEvent, - new ExtendRentOrBuyoutStripItemMessageParser() - }, - { - MessageEvent.GetGuildFurniContextMenuInfoMessageEvent, - new GetGuildFurniContextMenuInfoMessageParser() - }, - { - MessageEvent.GetRentOrBuyoutOfferMessageEvent, - new GetRentOrBuyoutOfferMessageParser() - }, - { - MessageEvent.GetYoutubeDisplayStatusMessageEvent, - new GetYoutubeDisplayStatusMessageParser() - }, - { MessageEvent.OpenMysteryTrophyMessageEvent, new OpenMysteryTrophyMessageParser() }, - { MessageEvent.OpenPetPackageMessageEvent, new OpenPetPackageMessageParser() }, - { MessageEvent.PlacePostItMessageEvent, new PlacePostItMessageParser() }, - { MessageEvent.PresentOpenMessageEvent, new PresentOpenMessageParser() }, - { - MessageEvent.RentableSpaceCancelRentMessageEvent, - new RentableSpaceCancelRentMessageParser() - }, - { - MessageEvent.RentableSpaceConfigureMessageEvent, - new ConfigureRentableSpaceMessageParser() - }, - { - MessageEvent.RentableSpaceGetConfigMessageEvent, - new GetRentableSpaceConfigMessageParser() - }, - { MessageEvent.RentableSpaceRentMessageEvent, new RentableSpaceRentMessageParser() }, - { - MessageEvent.RentableSpaceStatusMessageEvent, - new RentableSpaceStatusMessageParser() - }, - { - MessageEvent.RoomDimmerChangeStateMessageEvent, - new RoomDimmerChangeStateMessageParser() - }, - { - MessageEvent.RoomDimmerGetPresetsMessageEvent, - new RoomDimmerGetPresetsMessageParser() - }, - { - MessageEvent.RoomDimmerSavePresetMessageEvent, - new RoomDimmerSavePresetMessageParser() - }, - { MessageEvent.SetAreaHideDataEvent, new SetAreaHideDataMessageParser() }, - { - MessageEvent.SetCustomStackingHeightEvent, - new SetCustomStackingHeightMessageParser() - }, - { MessageEvent.SetMannequinFigureEvent, new SetMannequinFigureMessageParser() }, - { MessageEvent.SetMannequinNameEvent, new SetMannequinNameMessageParser() }, - { MessageEvent.SetRandomStateMessageEvent, new SetRandomStateMessageParser() }, - { - MessageEvent.SetRoomBackgroundColorDataEvent, - new SetRoomBackgroundColorDataMessageParser() - }, - { - MessageEvent.SetYoutubeDisplayPlaylistMessageEvent, - new SetYoutubeDisplayPlaylistMessageParser() - }, - { MessageEvent.SpinWheelOfFortuneMessageEvent, new SpinWheelOfFortuneMessageParser() }, - { MessageEvent.ThrowDiceMessageEvent, new ThrowDiceMessageParser() }, - #endregion - - #region Room Layout - { MessageEvent.GetOccupiedTilesMessageEvent, new GetOccupiedTilesMessageParser() }, - { MessageEvent.GetRoomEntryTileMessageEvent, new GetRoomEntryTileMessageParser() }, - { - MessageEvent.UpdateFloorPropertiesMessageEvent, - new UpdateFloorPropertiesMessageParser() - }, - #endregion - - #region Room Pets - { MessageEvent.BreedPetsMessageEvent, new BreedPetsMessageParser() }, - { MessageEvent.CustomizePetWithFurniEvent, new CustomizePetWithFurniMessageParser() }, - { MessageEvent.GetPetInfoMessageEvent, new GetPetInfoMessageParser() }, - { MessageEvent.PetSelectedMessageEvent, new PetSelectedMessageParser() }, - { MessageEvent.RespectPetMessageEvent, new RespectPetMessageParser() }, - #endregion - - #region Room Session - { MessageEvent.ChangeQueueMessageEvent, new ChangeQueueMessageParser() }, - { MessageEvent.OpenFlatConnectionMessageEvent, new OpenFlatConnectionMessageParser() }, - { MessageEvent.QuitMessageEvent, new QuitMessageParser() }, - #endregion - - #endregion - - #region Roomdirectory - { - MessageEvent.RoomNetworkOpenConnectionMessageEvent, - new RoomNetworkOpenConnectionMessageParser() - }, - #endregion - - #region RoomSettings - { MessageEvent.DeleteRoomMessageEvent, new DeleteRoomMessageParser() }, - { - MessageEvent.GetBannedUsersFromRoomMessageEvent, - new GetBannedUsersFromRoomMessageParser() - }, - { - MessageEvent.GetCustomRoomFilterMessageEvent, - new GetCustomRoomFilterMessageParser() - }, - { MessageEvent.GetFlatControllersMessageEvent, new GetFlatControllersMessageParser() }, - { MessageEvent.GetRoomSettingsMessageEvent, new GetRoomSettingsMessageParser() }, - { - MessageEvent.SaveRoomSettingsMessageEvent, - new SaveRoomSettingsMessageParser(protocolLimits.Value.MaxRoomTags) - }, - { - MessageEvent.UpdateRoomCategoryAndTradeSettingsEvent, - new UpdateRoomCategoryAndTradeSettingsMessageParser() - }, - { MessageEvent.UpdateRoomFilterMessageEvent, new UpdateRoomFilterMessageParser() }, - #endregion - - #region Sound - { MessageEvent.AddJukeboxDiskEvent, new AddJukeboxDiskMessageParser() }, - { MessageEvent.GetJukeboxPlayListMessageEvent, new GetJukeboxPlayListMessageParser() }, - { MessageEvent.GetNowPlayingMessageEvent, new GetNowPlayingMessageParser() }, - { MessageEvent.GetOfficialSongIdMessageEvent, new GetOfficialSongIdMessageParser() }, - { MessageEvent.GetSongInfoMessageEvent, new GetSongInfoMessageParser() }, - { - MessageEvent.GetSoundMachinePlayListMessageEvent, - new GetSoundMachinePlayListMessageParser() - }, - { MessageEvent.GetSoundSettingsEvent, new GetSoundSettingsMessageParser() }, - { MessageEvent.GetUserSongDisksMessageEvent, new GetUserSongDisksMessageParser() }, - { MessageEvent.RemoveJukeboxDiskEvent, new RemoveJukeboxDiskMessageParser() }, - #endregion - - #region Talent - { - MessageEvent.GetTalentTrackLevelMessageEvent, - new GetTalentTrackLevelMessageParser() - }, - { MessageEvent.GetTalentTrackMessageEvent, new GetTalentTrackMessageParser() }, - { - MessageEvent.GuideAdvertisementReadMessageEvent, - new GuideAdvertisementReadMessageParser() - }, - #endregion - - #region Tracking - { MessageEvent.EventLogMessageEvent, new EventLogMessageParser() }, - { MessageEvent.LagWarningReportMessageEvent, new LagWarningReportMessageParser() }, - { MessageEvent.LatencyPingReportMessageEvent, new LatencyPingReportMessageParser() }, - { MessageEvent.LatencyPingRequestMessageEvent, new LatencyPingRequestMessageParser() }, - { MessageEvent.PerformanceLogMessageEvent, new PerformanceLogMessageParser() }, - #endregion - - #region Userclassification - { - MessageEvent.PeerUsersClassificationMessageEvent, - new PeerUsersClassificationMessageParser() - }, - { - MessageEvent.RoomUsersClassificationMessageEvent, - new RoomUsersClassificationMessageParser() - }, - #endregion - - #region Userdefinedroomevents - - { MessageEvent.ApplySnapshotMessageEvent, new ApplySnapshotMessageParser() }, - { MessageEvent.OpenMessageEvent, new OpenMessageParser() }, - { MessageEvent.UpdateActionMessageEvent, new UpdateActionMessageParser() }, - { MessageEvent.UpdateAddonMessageEvent, new UpdateAddonMessageParser() }, - { MessageEvent.UpdateConditionMessageEvent, new UpdateConditionMessageParser() }, - { MessageEvent.UpdateSelectorMessageEvent, new UpdateSelectorMessageParser() }, - { MessageEvent.UpdateTriggerMessageEvent, new UpdateTriggerMessageParser() }, - { MessageEvent.UpdateVariableMessageEvent, new UpdateVariableMessageParser() }, - #region Userdefinedroomevents Wiredmenu - { - MessageEvent.WiredClearErrorLogsMessageEvent, - new WiredClearErrorLogsMessageParser() - }, - { - MessageEvent.WiredGetAllVariableHoldersMessageEvent, - new WiredGetAllVariableHoldersMessageParser() - }, - { - MessageEvent.WiredGetAllVariablesDiffsMessageEvent, - new WiredGetAllVariablesDiffsMessageParser() - }, - { - MessageEvent.WiredGetAllVariablesHashMessageEvent, - new WiredGetAllVariablesHashMessageParser() - }, - { MessageEvent.WiredGetErrorLogsMessageEvent, new WiredGetErrorLogsMessageParser() }, - { MessageEvent.WiredGetRoomLogsEvent, new WiredGetRoomLogsMessageParser() }, - { - MessageEvent.WiredGetRoomSettingsMessageEvent, - new WiredGetRoomSettingsMessageParser() - }, - { MessageEvent.WiredGetRoomStatsMessageEvent, new WiredGetRoomStatsMessageParser() }, - { - MessageEvent.WiredGetUserPermanentVariablesEvent, - new WiredGetUserPermanentVariablesMessageParser() - }, - { - MessageEvent.WiredGetVariableOwnersPageEvent, - new WiredGetVariableOwnersPageMessageParser() - }, - { - MessageEvent.WiredGetVariablesForObjectMessageEvent, - new WiredGetVariablesForObjectMessageParser() - }, - { - MessageEvent.WiredSetObjectVariableValueMessageEvent, - new WiredSetObjectVariableValueMessageParser() - }, - { - MessageEvent.WiredSetPreferencesMessageEvent, - new WiredSetPreferencesMessageParser() - }, - { - MessageEvent.WiredSetRoomSettingsMessageEvent, - new WiredSetRoomSettingsMessageParser() - }, - #endregion - - #endregion - - #region Users - { - MessageEvent.AddAdminRightsToMemberMessageEvent, - new AddAdminRightsToMemberMessageParser() - }, - { - MessageEvent.ApproveAllMembershipRequestsMessageEvent, - new ApproveAllMembershipRequestsMessageParser() - }, - { - MessageEvent.ApproveMembershipRequestMessageEvent, - new ApproveMembershipRequestMessageParser() - }, - { MessageEvent.ApproveNameMessageEvent, new ApproveNameMessageParser() }, - { MessageEvent.ChangeEmailEvent, new ChangeEmailMessageParser() }, - { MessageEvent.CreateGuildMessageEvent, new CreateGuildMessageParser() }, - { MessageEvent.DeactivateGuildMessageEvent, new DeactivateGuildMessageParser() }, - { - MessageEvent.DeselectFavouriteHabboGroupMessageEvent, - new DeselectFavouriteHabboGroupMessageParser() - }, - { MessageEvent.GetEmailStatusEvent, new GetEmailStatusMessageParser() }, - { - MessageEvent.GetExtendedProfileByNameMessageEvent, - new GetExtendedProfileByNameMessageParser() - }, - { MessageEvent.GetExtendedProfileMessageEvent, new GetExtendedProfileMessageParser() }, - { MessageEvent.RespectUserMessageEvent, new RespectUserMessageParser() }, - { - MessageEvent.GetGuildCreationInfoMessageEvent, - new GetGuildCreationInfoMessageParser() - }, - { MessageEvent.GetGuildEditInfoMessageEvent, new GetGuildEditInfoMessageParser() }, - { MessageEvent.GetGuildEditorDataMessageEvent, new GetGuildEditorDataMessageParser() }, - { - MessageEvent.GetGuildMembershipsMessageEvent, - new GetGuildMembershipsMessageParser() - }, - { MessageEvent.GetGuildMembersMessageEvent, new GetGuildMembersMessageParser() }, - { - MessageEvent.GetHabboGroupBadgesMessageEvent, - new GetHabboGroupBadgesMessageParser() - }, - { - MessageEvent.GetHabboGroupDetailsMessageEvent, - new GetHabboGroupDetailsMessageParser() - }, - { MessageEvent.BlockListInitEvent, new BlockListInitMessageParser() }, - { MessageEvent.GetIgnoredUsersMessageEvent, new GetIgnoredUsersMessageParser() }, - { - MessageEvent.GetMemberGuildItemCountMessageEvent, - new GetMemberGuildItemCountMessageParser() - }, - { MessageEvent.GetMOTDMessageEvent, new GetMOTDMessageParser() }, - { - MessageEvent.GetRelationshipStatusInfoMessageEvent, - new GetRelationshipStatusInfoMessageParser() - }, - { MessageEvent.GetSelectedBadgesMessageEvent, new GetSelectedBadgesMessageParser() }, - { - MessageEvent.GetUserNftChatStylesMessageEvent, - new GetUserNftChatStylesMessageParser() - }, - { MessageEvent.IgnoreUserMessageEvent, new IgnoreUserMessageParser() }, - { MessageEvent.JoinHabboGroupMessageEvent, new JoinHabboGroupMessageParser() }, - { MessageEvent.KickMemberMessageEvent, new KickMemberMessageParser() }, - { - MessageEvent.RejectMembershipRequestMessageEvent, - new RejectMembershipRequestMessageParser() - }, - { - MessageEvent.RemoveAdminRightsFromMemberMessageEvent, - new RemoveAdminRightsFromMemberMessageParser() - }, - { MessageEvent.ScrGetKickbackInfoMessageEvent, new ScrGetKickbackInfoMessageParser() }, - { MessageEvent.ScrGetUserInfoMessageEvent, new ScrGetUserInfoMessageParser() }, - { - MessageEvent.SelectFavouriteHabboGroupMessageEvent, - new SelectFavouriteHabboGroupMessageParser() - }, - { MessageEvent.BlockUserMessageEvent, new BlockUserMessageParser() }, - { MessageEvent.UnblockGroupMemberMessageEvent, new UnblockGroupMemberMessageParser() }, - { MessageEvent.UnblockUserMessageEvent, new UnblockUserMessageParser() }, - { MessageEvent.UnignoreUserMessageEvent, new UnignoreUserMessageParser() }, - { MessageEvent.UpdateGuildBadgeMessageEvent, new UpdateGuildBadgeMessageParser() }, - { MessageEvent.UpdateGuildColorsMessageEvent, new UpdateGuildColorsMessageParser() }, - { - MessageEvent.UpdateGuildIdentityMessageEvent, - new UpdateGuildIdentityMessageParser() - }, - { - MessageEvent.UpdateGuildSettingsMessageEvent, - new UpdateGuildSettingsMessageParser() - }, - #endregion - - #region Vault - { MessageEvent.CreditVaultStatusMessageEvent, new CreditVaultStatusMessageParser() }, - { MessageEvent.IncomeRewardClaimMessageEvent, new IncomeRewardClaimMessageParser() }, - { MessageEvent.IncomeRewardStatusMessageEvent, new IncomeRewardStatusMessageParser() }, - { - MessageEvent.WithdrawCreditVaultMessageEvent, - new WithdrawCreditVaultMessageParser() - }, - #endregion - }; - #endregion - - #region Outgoing - public IDictionary Serializers { get; } = - new Dictionary +public sealed class Revision20260701(IOptions protocolLimits) + : RevisionBase( + new IRevisionMap[] { - #region Advertisement - { - typeof(InterstitialMessageComposer), - new InterstitialMessageComposerSerializer( - MessageComposer.InterstitialMessageComposer - ) - }, - { - typeof(RoomAdErrorEventMessageComposer), - new RoomAdErrorEventMessageComposerSerializer(MessageComposer.RoomAdErrorComposer) - }, - #endregion - - #region Availability - { - typeof(AvailabilityStatusMessageComposer), - new AvailabilityStatusMessageComposerSerializer( - MessageComposer.AvailabilityStatusMessageComposer - ) - }, - { - typeof(InfoHotelClosedMessageComposer), - new InfoHotelClosedMessageComposerSerializer( - MessageComposer.InfoHotelClosedMessageComposer - ) - }, - { - typeof(InfoHotelClosingMessageComposer), - new InfoHotelClosingMessageComposerSerializer( - MessageComposer.InfoHotelClosingMessageComposer - ) - }, - { - typeof(LoginFailedHotelClosedMessageComposer), - new LoginFailedHotelClosedMessageComposerSerializer( - MessageComposer.LoginFailedHotelClosedMessageComposer - ) - }, - { - typeof(MaintenanceStatusMessageComposer), - new MaintenanceStatusMessageComposerSerializer( - MessageComposer.MaintenanceStatusMessageComposer - ) - }, - #endregion - - #region Avatar - { - typeof(ChangeUserNameResultMessageComposer), - new ChangeUserNameResultMessageComposerSerializer( - MessageComposer.ChangeUserNameResultMessageComposer - ) - }, - { - typeof(CheckUserNameResultMessageComposer), - new CheckUserNameResultMessageComposerSerializer( - MessageComposer.CheckUserNameResultMessageComposer - ) - }, - { - typeof(FigureUpdateEventMessageComposer), - new FigureUpdateEventMessageComposerSerializer(MessageComposer.FigureUpdateComposer) - }, - { - typeof(WardrobeMessageComposer), - new WardrobeMessageComposerSerializer(MessageComposer.WardrobeMessageComposer) - }, - #endregion - - #region Callforhelp - { - typeof(CfhSanctionMessageComposer), - new CfhSanctionMessageComposerSerializer(MessageComposer.CfhSanctionMessageComposer) - }, - { - typeof(CfhTopicsInitMessageComposer), - new CfhTopicsInitMessageComposerSerializer( - MessageComposer.CfhTopicsInitMessageComposer - ) - }, - { - typeof(SanctionStatusEventMessageComposer), - new SanctionStatusEventMessageComposerSerializer( - MessageComposer.SanctionStatusComposer - ) - }, - #endregion - - #region Camera - { - typeof(CameraPublishStatusMessageComposer), - new CameraPublishStatusMessageComposerSerializer( - MessageComposer.CameraPublishStatusMessageComposer - ) - }, - { - typeof(CameraPurchaseOKMessageComposer), - new CameraPurchaseOKMessageComposerSerializer( - MessageComposer.CameraPurchaseOKMessageComposer - ) - }, - { - typeof(CameraStorageUrlMessageComposer), - new CameraStorageUrlMessageComposerSerializer( - MessageComposer.CameraStorageUrlMessageComposer - ) - }, - { - typeof(CompetitionStatusMessageComposer), - new CompetitionStatusMessageComposerSerializer( - MessageComposer.CompetitionStatusMessageComposer - ) - }, - { - typeof(InitCameraMessageComposer), - new InitCameraMessageComposerSerializer(MessageComposer.InitCameraMessageComposer) - }, - { - typeof(ThumbnailStatusMessageComposer), - new ThumbnailStatusMessageComposerSerializer( - MessageComposer.ThumbnailStatusMessageComposer - ) - }, - #endregion - - #region Campaign - { - typeof(CampaignCalendarDataMessageComposer), - new CampaignCalendarDataMessageComposerSerializer( - MessageComposer.CampaignCalendarDataMessageComposer - ) - }, - { - typeof(CampaignCalendarDoorOpenedMessageComposer), - new CampaignCalendarDataMessageComposerSerializer( - MessageComposer.CampaignCalendarDoorOpenedMessageComposer - ) - }, - #endregion - - #region Catalog - { - typeof(BonusRareInfoMessageComposer), - new BonusRareInfoMessageComposerSerializer( - MessageComposer.BonusRareInfoMessageComposer - ) - }, - { - typeof(BuildersClubFurniCountMessageComposer), - new BuildersClubFurniCountMessageComposerSerializer( - MessageComposer.BuildersClubFurniCountMessageComposer - ) - }, - { - typeof(BuildersClubSubscriptionStatusMessageComposer), - new BuildersClubSubscriptionStatusMessageComposerSerializer( - MessageComposer.BuildersClubSubscriptionStatusMessageComposer - ) - }, - { - typeof(BundleDiscountRulesetMessageComposer), - new BundleDiscountRulesetMessageComposerSerializer( - MessageComposer.BundleDiscountRulesetMessageComposer - ) - }, - { - typeof(CatalogIndexMessageComposer), - new CatalogIndexMessageComposerSerializer( - MessageComposer.CatalogIndexMessageComposer - ) - }, - { - typeof(CatalogPageMessageComposer), - new CatalogPageMessageComposerSerializer(MessageComposer.CatalogPageMessageComposer) - }, - { - typeof(CatalogPageWithEarliestExpiryMessageComposer), - new CatalogPageWithEarliestExpiryMessageComposerSerializer( - MessageComposer.CatalogPageWithEarliestExpiryMessageComposer - ) - }, - { - typeof(CatalogPublishedMessageComposer), - new CatalogPublishedMessageComposerSerializer( - MessageComposer.CatalogPublishedMessageComposer - ) - }, - { - typeof(ClubGiftInfoEventMessageComposer), - new ClubGiftInfoEventMessageComposerSerializer(MessageComposer.ClubGiftInfoComposer) - }, - { - typeof(ClubGiftSelectedEventMessageComposer), - new ClubGiftSelectedEventMessageComposerSerializer( - MessageComposer.ClubGiftSelectedComposer - ) - }, - { - typeof(GiftReceiverNotFoundEventMessageComposer), - new GiftReceiverNotFoundEventMessageComposerSerializer( - MessageComposer.GiftReceiverNotFoundComposer - ) - }, - { - typeof(GiftWrappingConfigurationEventMessageComposer), - new GiftWrappingConfigurationEventMessageComposerSerializer( - MessageComposer.GiftWrappingConfigurationComposer - ) - }, - { - typeof(HabboClubExtendOfferMessageComposer), - new HabboClubExtendOfferMessageComposerSerializer( - MessageComposer.HabboClubExtendOfferMessageComposer - ) - }, - { - typeof(HabboClubOffersMessageComposer), - new HabboClubOffersMessageComposerSerializer( - MessageComposer.HabboClubOffersMessageComposer - ) - }, - { - typeof(IsOfferGiftableEventMessageComposer), - new IsOfferGiftableEventMessageComposerSerializer( - MessageComposer.IsOfferGiftableEventMessageComposer - ) - }, - { - typeof(LimitedEditionSoldOutEventMessageComposer), - new LimitedEditionSoldOutEventMessageComposerSerializer( - MessageComposer.LimitedEditionSoldOutComposer - ) - }, - { - typeof(LimitedOfferAppearingNextMessageComposer), - new LimitedOfferAppearingNextMessageComposerSerializer( - MessageComposer.LimitedOfferAppearingNextMessageComposer - ) - }, - { - typeof(NotEnoughBalanceMessageComposer), - new NotEnoughBalanceMessageComposerSerializer( - MessageComposer.NotEnoughBalanceMessageComposer - ) - }, - { - typeof(ProductOfferEventMessageComposer), - new ProductOfferEventMessageComposerSerializer(MessageComposer.ProductOfferComposer) - }, - { - typeof(PurchaseErrorMessageComposer), - new PurchaseErrorMessageComposerSerializer( - MessageComposer.PurchaseErrorMessageComposer - ) - }, - { - typeof(PurchaseNotAllowedMessageComposer), - new PurchaseNotAllowedMessageComposerSerializer( - MessageComposer.PurchaseNotAllowedMessageComposer - ) - }, - { - typeof(PurchaseOKMessageComposer), - new PurchaseOKMessageComposerSerializer(MessageComposer.PurchaseOKMessageComposer) - }, - { - typeof(RoomAdPurchaseInfoEventMessageComposer), - new RoomAdPurchaseInfoEventMessageComposerSerializer( - MessageComposer.RoomAdPurchaseInfoComposer - ) - }, - { - typeof(SeasonalCalendarDailyOfferMessageComposer), - new SeasonalCalendarDailyOfferMessageComposerSerializer( - MessageComposer.SeasonalCalendarDailyOfferMessageComposer - ) - }, - { - typeof(SellablePetPalettesMessageComposer), - new SellablePetPalettesMessageComposerSerializer( - MessageComposer.SellablePetPalettesMessageComposer - ) - }, - { - typeof(SnowWarGameTokensMessageMessageComposer), - new SnowWarGameTokensMessageMessageComposerSerializer( - MessageComposer.SnowWarGameTokensMessageComposer - ) - }, - { - typeof(TargetedOfferEventMessageComposer), - new TargetedOfferEventMessageComposerSerializer( - MessageComposer.TargetedOfferComposer - ) - }, - { - typeof(TargetedOfferNotFoundEventMessageComposer), - new TargetedOfferNotFoundEventMessageComposerSerializer( - MessageComposer.TargetedOfferNotFoundComposer - ) - }, - { - typeof(VoucherRedeemErrorMessageComposer), - new VoucherRedeemErrorMessageComposerSerializer( - MessageComposer.VoucherRedeemErrorMessageComposer - ) - }, - { - typeof(VoucherRedeemOkMessageComposer), - new VoucherRedeemOkMessageComposerSerializer( - MessageComposer.VoucherRedeemOkMessageComposer - ) - }, - #endregion - - #region Collectibles - { - typeof(CollectableMintableItemTypesMessageComposer), - new CollectableMintableItemTypesMessageComposerSerializer( - MessageComposer.CollectableMintableItemTypesMessageComposer - ) - }, - { - typeof(CollectibleMintableItemResultMessageComposer), - new CollectibleMintableItemResultMessageComposerSerializer( - MessageComposer.CollectibleMintableItemResultMessageComposer - ) - }, - { - typeof(CollectibleMintingEnabledMessageComposer), - new CollectibleMintingEnabledMessageComposerSerializer( - MessageComposer.CollectibleMintingEnabledMessageComposer - ) - }, - { - typeof(CollectibleMintTokenCountMessageComposer), - new CollectibleMintTokenCountMessageComposerSerializer( - MessageComposer.CollectibleMintTokenCountMessageComposer - ) - }, - { - typeof(CollectibleMintTokenOffersMessageComposer), - new CollectibleMintTokenOffersMessageComposerSerializer( - MessageComposer.CollectibleMintTokenOffersMessageComposer - ) - }, - { - typeof(CollectibleWalletAddressesMessageComposer), - new CollectibleWalletAddressesMessageComposerSerializer( - MessageComposer.CollectibleWalletAddressesMessageComposer - ) - }, - { - typeof(EmeraldBalanceMessageComposer), - new EmeraldBalanceMessageComposerSerializer( - MessageComposer.EmeraldBalanceMessageComposer - ) - }, - { - typeof(NftBonusItemClaimResultMessageComposer), - new NftBonusItemClaimResultMessageComposerSerializer( - MessageComposer.NftBonusItemClaimResultMessageComposer - ) - }, - { - typeof(NftCollectionsMessageComposer), - new NftCollectionsMessageComposerSerializer( - MessageComposer.NftCollectionsMessageComposer - ) - }, - { - typeof(NftCollectionsScoreMessageComposer), - new NftCollectionsScoreMessageComposerSerializer( - MessageComposer.NftCollectionsScoreMessageComposer - ) - }, - { - typeof(NftRewardItemClaimResultMessageComposer), - new NftRewardItemClaimResultMessageComposerSerializer( - MessageComposer.NftRewardItemClaimResultMessageComposer - ) - }, - { - typeof(NftTransferAssetsResultMessageComposer), - new NftTransferAssetsResultMessageComposerSerializer( - MessageComposer.NftTransferAssetsResultMessageComposer - ) - }, - { - typeof(NftTransferFeeMessageComposer), - new NftTransferFeeMessageComposerSerializer( - MessageComposer.NftTransferFeeMessageComposer - ) - }, - { - typeof(SilverBalanceMessageComposer), - new SilverBalanceMessageComposerSerializer( - MessageComposer.SilverBalanceMessageComposer - ) - }, - { - typeof(UserNftChatStylesMessageComposer), - new UserNftChatStylesMessageComposerSerializer( - MessageComposer.UserNftChatStylesMessageComposer - ) - }, - #endregion - - #region Competition - { - typeof(CurrentTimingCodeMessageComposer), - new CurrentTimingCodeMessageComposerSerializer( - MessageComposer.CurrentTimingCodeMessageComposer - ) - }, - #endregion - - #region FriendList - { - typeof(AcceptFriendResultMessageComposer), - new AcceptFriendResultMessageSerializer(MessageComposer.AcceptFriendResultComposer) - }, - { - typeof(ConsoleMessageHistoryMessageComposer), - new ConsoleMessageHistoryMessageSerializer( - MessageComposer.ConsoleMessageHistoryComposer - ) - }, - { - typeof(FollowFriendFailedMessageComposer), - new FollowFriendFailedMessageSerializer(MessageComposer.FollowFriendFailedComposer) - }, - { - typeof(FriendListFragmentMessageComposer), - new FriendListFragmentMessageSerializer( - MessageComposer.FriendListFragmentMessageComposer - ) - }, - { - typeof(FriendListUpdateMessageComposer), - new FriendListUpdateMessageSerializer(MessageComposer.FriendListUpdateComposer) - }, - { - typeof(FriendNotificationMessageComposer), - new FriendNotificationMessageSerializer(MessageComposer.FriendNotificationComposer) - }, - { - typeof(FriendRequestsMessageComposer), - new FriendRequestsMessageSerializer(MessageComposer.FriendRequestsComposer) - }, - { - typeof(HabboSearchResultMessageComposer), - new HabboSearchResultMessageSerializer(MessageComposer.HabboSearchResultComposer) - }, - { - typeof(InstantMessageErrorMessageComposer), - new InstantMessageErrorMessageSerializer( - MessageComposer.InstantMessageErrorComposer - ) - }, - { - typeof(MessengerErrorMessageComposer), - new MessengerErrorMessageSerializer(MessageComposer.MessengerErrorComposer) - }, - { - typeof(MessengerInitMessageComposer), - new MessengerInitMessageSerializer(MessageComposer.MessengerInitComposer) - }, - { - typeof(MiniMailNewMessageComposer), - new MiniMailNewMessageSerializer(MessageComposer.MiniMailNewMessageComposer) - }, - { - typeof(MiniMailUnreadCountMessageComposer), - new MiniMailUnreadCountMessageSerializer( - MessageComposer.MiniMailUnreadCountComposer - ) - }, - { - typeof(NewConsoleMessageMessageComposer), - new NewConsoleMessageMessageSerializer(MessageComposer.NewConsoleMessageComposer) - }, - { - typeof(NewFriendRequestMessageComposer), - new NewFriendRequestMessageSerializer(MessageComposer.NewFriendRequestComposer) - }, - { - typeof(RoomInviteErrorMessageComposer), - new RoomInviteErrorMessageSerializer(MessageComposer.RoomInviteErrorComposer) - }, - { - typeof(RoomInviteMessageComposer), - new RoomInviteMessageSerializer(MessageComposer.RoomInviteComposer) - }, - { - typeof(FindFriendsProcessResultMessageComposer), - new FindFriendsProcessResultMessageComposerSerializer( - MessageComposer.FindFriendsProcessResultComposer - ) - }, - #endregion - - #region Groupforums - { - typeof(UnreadForumsCountMessageComposer), - new UnreadForumsCountMessageComposerSerializer( - MessageComposer.UnreadForumsCountMessageComposer - ) - }, - { - typeof(ForumsListMessageComposer), - new ForumsListMessageComposerSerializer(MessageComposer.ForumsListMessageComposer) - }, - { - typeof(ForumDataMessageComposer), - new ForumDataMessageComposerSerializer(MessageComposer.ForumDataMessageComposer) - }, - { - typeof(ForumThreadsMessageComposer), - new ForumThreadsMessageComposerSerializer( - MessageComposer.ForumThreadsMessageComposer - ) - }, - { - typeof(ThreadMessagesMessageComposer), - new ThreadMessagesMessageComposerSerializer( - MessageComposer.ThreadMessagesMessageComposer - ) - }, - { - typeof(PostThreadMessageComposer), - new PostThreadMessageComposerSerializer(MessageComposer.PostThreadMessageComposer) - }, - { - typeof(PostMessageMessageComposer), - new PostMessageMessageComposerSerializer(MessageComposer.PostMessageMessageComposer) - }, - { - typeof(UpdateThreadMessageComposer), - new UpdateThreadMessageComposerSerializer( - MessageComposer.UpdateThreadMessageComposer - ) - }, - { - typeof(UpdateMessageMessageComposer), - new UpdateMessageMessageComposerSerializer( - MessageComposer.UpdateMessageMessageComposer - ) - }, - #endregion - - #region Inventory - - #region Inventory Achievements - { - typeof(AchievementEventMessageComposer), - new AchievementEventMessageComposerSerializer(MessageComposer.AchievementComposer) - }, - { - typeof(AchievementsEventMessageComposer), - new AchievementsEventMessageComposerSerializer(MessageComposer.AchievementsComposer) - }, - { - typeof(AchievementsScoreEventMessageComposer), - new AchievementsScoreEventMessageComposerSerializer( - MessageComposer.AchievementsScoreComposer - ) - }, - #endregion - - #region Inventory Avatareffect - { - typeof(AvatarEffectActivatedMessageComposer), - new AvatarEffectActivatedMessageComposerSerializer( - MessageComposer.AvatarEffectActivatedMessageComposer - ) - }, - { - typeof(AvatarEffectAddedMessageComposer), - new AvatarEffectAddedMessageComposerSerializer( - MessageComposer.AvatarEffectAddedMessageComposer - ) - }, - { - typeof(AvatarEffectExpiredMessageComposer), - new AvatarEffectExpiredMessageComposerSerializer( - MessageComposer.AvatarEffectExpiredMessageComposer - ) - }, - { - typeof(AvatarEffectSelectedMessageComposer), - new AvatarEffectSelectedMessageComposerSerializer( - MessageComposer.AvatarEffectSelectedMessageComposer - ) - }, - { - typeof(AvatarEffectsMessageComposer), - new AvatarEffectsMessageComposerSerializer( - MessageComposer.AvatarEffectsMessageComposer - ) - }, - #endregion - - #region Inventory Badges - { - typeof(BadgePointLimitsEventMessageComposer), - new BadgePointLimitsEventMessageComposerSerializer( - MessageComposer.BadgePointLimitsComposer - ) - }, - { - typeof(BadgeReceivedEventMessageComposer), - new BadgeReceivedEventMessageComposerSerializer( - MessageComposer.BadgeReceivedComposer - ) - }, - { - typeof(BadgesEventMessageComposer), - new BadgesEventMessageComposerSerializer(MessageComposer.BadgesComposer) - }, - { - typeof(HabboUserBadgesMessageComposer), - new HabboUserBadgesMessageComposerSerializer( - MessageComposer.HabboUserBadgesMessageComposer - ) - }, - { - typeof(IsBadgeRequestFulfilledEventMessageComposer), - new IsBadgeRequestFulfilledEventMessageComposerSerializer( - MessageComposer.IsBadgeRequestFulfilledComposer - ) - }, - #endregion - - #region Inventory Bots - { - typeof(BotAddedToInventoryEventMessageComposer), - new BotAddedToInventoryEventMessageComposerSerializer( - MessageComposer.BotAddedToInventoryComposer - ) - }, - { - typeof(BotInventoryEventMessageComposer), - new BotInventoryEventMessageComposerSerializer(MessageComposer.BotInventoryComposer) - }, - { - typeof(BotRemovedFromInventoryEventMessageComposer), - new BotRemovedFromInventoryEventMessageComposerSerializer( - MessageComposer.BotRemovedFromInventoryComposer - ) - }, - #endregion - - - #region Inventory Clothing - { - typeof(FigureSetIdsEventMessageComposer), - new FigureSetIdsEventMessageComposerSerializer(MessageComposer.FigureSetIdsComposer) - }, - #endregion - - #region Inventory Furni - { - typeof(FurniListAddOrUpdateEventMessageComposer), - new FurniListAddOrUpdateEventMessageComposerSerializer( - MessageComposer.FurniListAddOrUpdateComposer - ) - }, - { - typeof(FurniListEventMessageComposer), - new FurniListEventMessageComposerSerializer(MessageComposer.FurniListComposer) - }, - { - typeof(FurniListInvalidateEventMessageComposer), - new FurniListInvalidateEventMessageComposerSerializer( - MessageComposer.FurniListInvalidateComposer - ) - }, - { - typeof(FurniListRemoveEventMessageComposer), - new FurniListRemoveEventMessageComposerSerializer( - MessageComposer.FurniListRemoveComposer - ) - }, - { - typeof(PostItPlacedEventMessageComposer), - new PostItPlacedEventMessageComposerSerializer(MessageComposer.PostItPlacedComposer) - }, - #endregion - - #region Inventory Pets - { - typeof(ConfirmBreedingRequestEventMessageComposer), - new ConfirmBreedingRequestEventMessageComposerSerializer( - MessageComposer.ConfirmBreedingRequestComposer - ) - }, - { - typeof(ConfirmBreedingResultEventMessageComposer), - new ConfirmBreedingResultEventMessageComposerSerializer( - MessageComposer.ConfirmBreedingResultComposer - ) - }, - { - typeof(GoToBreedingNestFailureEventMessageComposer), - new GoToBreedingNestFailureEventMessageComposerSerializer( - MessageComposer.GoToBreedingNestFailureComposer - ) - }, - { - typeof(NestBreedingSuccessEventMessageComposer), - new NestBreedingSuccessEventMessageComposerSerializer( - MessageComposer.NestBreedingSuccessComposer - ) - }, - { - typeof(PetAddedToInventoryEventMessageComposer), - new PetAddedToInventoryEventMessageComposerSerializer( - MessageComposer.PetAddedToInventoryComposer - ) - }, - { - typeof(PetBreedingEventMessageComposer), - new PetBreedingEventMessageComposerSerializer(MessageComposer.PetBreedingComposer) - }, - { - typeof(PetInventoryEventMessageComposer), - new PetInventoryEventMessageComposerSerializer(MessageComposer.PetInventoryComposer) - }, - { - typeof(PetReceivedMessageComposer), - new PetReceivedMessageComposerSerializer(MessageComposer.PetReceivedMessageComposer) - }, - { - typeof(PetRemovedFromInventoryEventMessageComposer), - new PetRemovedFromInventoryEventMessageComposerSerializer( - MessageComposer.PetRemovedFromInventoryComposer - ) - }, - #endregion - - #region Inventory Purse - { - typeof(CreditBalanceEventMessageComposer), - new CreditBalanceEventMessageComposerSerializer( - MessageComposer.CreditBalanceComposer - ) - }, - #endregion - - #region Inventory Trading - { - typeof(TradeOpenFailedEventPaserMessageComposer), - new TradeOpenFailedEventPaserMessageComposerSerializer( - MessageComposer.TradeOpenFailedComposer - ) - }, - { - typeof(TradeSilverFeeMessageComposer), - new TradeSilverFeeMessageComposerSerializer( - MessageComposer.TradeSilverFeeMessageComposer - ) - }, - { - typeof(TradeSilverSetMessageComposer), - new TradeSilverSetMessageComposerSerializer( - MessageComposer.TradeSilverSetMessageComposer - ) - }, - { - typeof(TradingAcceptEventMessageComposer), - new TradingAcceptEventMessageComposerSerializer( - MessageComposer.TradingAcceptComposer - ) - }, - { - typeof(TradingCloseEventMessageComposer), - new TradingCloseEventMessageComposerSerializer(MessageComposer.TradingCloseComposer) - }, - { - typeof(TradingCompletedEventMessageComposer), - new TradingCompletedEventMessageComposerSerializer( - MessageComposer.TradingCompletedComposer - ) - }, - { - typeof(TradingConfirmationEventMessageComposer), - new TradingConfirmationEventMessageComposerSerializer( - MessageComposer.TradingConfirmationComposer - ) - }, - { - typeof(TradingItemListEventMessageComposer), - new TradingItemListEventMessageComposerSerializer( - MessageComposer.TradingItemListComposer - ) - }, - { - typeof(TradingNotOpenEventMessageComposer), - new TradingNotOpenEventMessageComposerSerializer( - MessageComposer.TradingNotOpenComposer - ) - }, - { - typeof(TradingOpenEventMessageComposer), - new TradingOpenEventMessageComposerSerializer(MessageComposer.TradingOpenComposer) - }, - { - typeof(TradingOtherNotAllowedEventMessageComposer), - new TradingOtherNotAllowedEventMessageComposerSerializer( - MessageComposer.TradingOtherNotAllowedComposer - ) - }, - { - typeof(TradingYouAreNotAllowedEventMessageComposer), - new TradingYouAreNotAllowedEventMessageComposerSerializer( - MessageComposer.TradingYouAreNotAllowedComposer - ) - }, - #endregion - - #endregion - - #region Handshake - { - typeof(AuthenticationOKMessage), - new AuthenticationOKMessageSerializer( - MessageComposer.AuthenticationOKMessageComposer - ) - }, - { - typeof(CompleteDiffieHandshakeMessageComposer), - new CompleteDiffieHandshakeMessageSerializer( - MessageComposer.CompleteDiffieHandshakeComposer - ) - }, - { - typeof(GenericErrorMessage), - new GenericErrorMessageSerializer(MessageComposer.GenericErrorComposer) - }, - { - typeof(InitDiffieHandshakeMessageComposer), - new InitDiffieHandshakeMessageSerializer( - MessageComposer.InitDiffieHandshakeComposer - ) - }, - { - typeof(IsFirstLoginOfDayMessage), - new IsFirstLoginOfDayMessageSerializer(MessageComposer.IsFirstLoginOfDayComposer) - }, - { - typeof(NoobnessLevelMessage), - new NoobnessLevelMessageSerializer(MessageComposer.NoobnessLevelMessageComposer) - }, - { typeof(PingMessage), new PingMessageSerializer(MessageComposer.PingMessageComposer) }, - { - typeof(UniqueMachineIdMessage), - new UniqueMachineIdMessageSerializer(MessageComposer.UniqueMachineIDComposer) - }, - { - typeof(UserObjectMessage), - new UserObjectMessageSerializer(MessageComposer.UserObjectComposer) - }, - { - typeof(UserRightsMessage), - new UserRightsMessageSerializer(MessageComposer.UserRightsMessageComposer) - }, - #endregion - - #region Marketplace - { - typeof(MarketplaceBuyOfferResultEventMessageComposer), - new MarketplaceBuyOfferResultEventMessageComposerSerializer( - MessageComposer.MarketplaceBuyOfferResultComposer - ) - }, - { - typeof(MarketplaceCancelOfferResultEventMessageComposer), - new MarketplaceCancelOfferResultEventMessageComposerSerializer( - MessageComposer.MarketplaceCancelOfferResultComposer - ) - }, - { - typeof(MarketplaceCanMakeOfferResultMessageComposer), - new MarketplaceCanMakeOfferResultMessageComposerSerializer( - MessageComposer.MarketplaceCanMakeOfferResult - ) - }, - { - typeof(MarketplaceConfigurationEventMessageComposer), - new MarketplaceConfigurationEventMessageComposerSerializer( - MessageComposer.MarketplaceConfigurationComposer - ) - }, - { - typeof(MarketplaceItemStatsEventMessageComposer), - new MarketplaceItemStatsEventMessageComposerSerializer( - MessageComposer.MarketplaceItemStatsComposer - ) - }, - { - typeof(MarketplaceMakeOfferResultMessageComposer), - new MarketplaceMakeOfferResultMessageComposerSerializer( - MessageComposer.MarketplaceMakeOfferResult - ) - }, - { - typeof(MarketPlaceOffersEventMessageComposer), - new MarketPlaceOffersEventMessageComposerSerializer( - MessageComposer.MarketPlaceOffersComposer - ) - }, - { - typeof(MarketPlaceOwnOffersEventMessageComposer), - new MarketPlaceOwnOffersEventMessageComposerSerializer( - MessageComposer.MarketPlaceOwnOffersComposer - ) - }, - #endregion - - #region Mysterybox - { - typeof(CancelMysteryBoxWaitMessageComposer), - new CancelMysteryBoxWaitMessageComposerSerializer( - MessageComposer.CancelMysteryBoxWaitMessageComposer - ) - }, - { - typeof(GotMysteryBoxPrizeMessageComposer), - new GotMysteryBoxPrizeMessageComposerSerializer( - MessageComposer.GotMysteryBoxPrizeMessageComposer - ) - }, - { - typeof(MysteryBoxKeysMessageComposer), - new MysteryBoxKeysMessageComposerSerializer( - MessageComposer.MysteryBoxKeysMessageComposer - ) - }, - { - typeof(ShowMysteryBoxWaitMessageComposer), - new ShowMysteryBoxWaitMessageComposerSerializer( - MessageComposer.ShowMysteryBoxWaitMessageComposer - ) - }, - #endregion - - #region Navigator - { - typeof(CanCreateRoomEventMessageComposer), - new CanCreateRoomEventMessageComposerSerializer( - MessageComposer.CanCreateRoomEventComposer - ) - }, - { - typeof(CanCreateRoomMessageComposer), - new CanCreateRoomMessageComposerSerializer(MessageComposer.CanCreateRoomComposer) - }, - { - typeof(CategoriesWithVisitorCountMessageComposer), - new CategoriesWithVisitorCountMessageComposerSerializer( - MessageComposer.CategoriesWithVisitorCountComposer - ) - }, - { - typeof(CompetitionRoomsDataMessageComposer), - new CompetitionRoomsDataMessageComposerSerializer( - MessageComposer.CompetitionRoomsDataMessageComposer - ) - }, - { - typeof(ConvertedRoomIdMessageComposer), - new ConvertedRoomIdMessageComposerSerializer( - MessageComposer.ConvertedRoomIdComposer - ) - }, - { - typeof(DoorbellMessageComposer), - new DoorbellMessageComposerSerializer(MessageComposer.DoorbellMessageComposer) - }, - { - typeof(FavouriteChangedMessageComposer), - new FavouriteChangedMessageComposerSerializer( - MessageComposer.FavouriteChangedComposer - ) - }, - { - typeof(FavouritesMessageComposer), - new FavouritesMessageSerializer(MessageComposer.FavouritesComposer) - }, - { - typeof(FlatAccessDeniedMessageComposer), - new FlatAccessDeniedMessageComposerSerializer( - MessageComposer.FlatAccessDeniedMessageComposer - ) - }, - { - typeof(FlatCreatedMessageComposer), - new FlatCreatedMessageComposerSerializer(MessageComposer.FlatCreatedComposer) - }, - { - typeof(GetGuestRoomResultMessageComposer), - new GetGuestRoomResultMessageComposerSerializer( - MessageComposer.GetGuestRoomResultComposer - ) - }, - { - typeof(GuestRoomSearchResultMessageComposer), - new GuestRoomSearchResultMessageComposerSerializer( - MessageComposer.GuestRoomSearchResultComposer - ) - }, - { - typeof(NavigatorSettingsMessageComposer), - new NavigatorSettingsMessageComposerSerializer( - MessageComposer.NavigatorSettingsComposer - ) - }, - { - typeof(OfficialRoomsMessageComposer), - new OfficialRoomsMessageComposerSerializer(MessageComposer.OfficialRoomsComposer) - }, - { - typeof(PopularRoomTagsResultMessageComposer), - new PopularRoomTagsResultMessageComposerSerializer( - MessageComposer.PopularRoomTagsResultComposer - ) - }, - { - typeof(RoomEventCancelMessageComposer), - new RoomEventCancelMessageComposerSerializer( - MessageComposer.RoomEventCancelComposer - ) - }, - { - typeof(RoomEventMessageComposer), - new RoomEventMessageComposerSerializer(MessageComposer.RoomEventComposer) - }, - { - typeof(RoomInfoUpdatedMessageComposer), - new RoomInfoUpdatedMessageComposerSerializer( - MessageComposer.RoomInfoUpdatedComposer - ) - }, - { - typeof(RoomRatingMessageComposer), - new RoomRatingMessageComposerSerializer(MessageComposer.RoomRatingComposer) - }, - { - typeof(UserEventCatsMessageComposer), - new UserEventCatsMessageComposerSerializer(MessageComposer.UserEventCatsComposer) - }, - { - typeof(UserFlatCatsMessageComposer), - new UserFlatCatsMessageComposerSerializer(MessageComposer.UserFlatCatsComposer) - }, - #endregion - - #region NewNavigator - { - typeof(NavigatorCollapsedCategoriesMessage), - new NavigatorCollapsedCategoriesMessageSerializer( - MessageComposer.NavigatorCollapsedCategoriesMessageComposer - ) - }, - { - typeof(NavigatorLiftedRoomsMessage), - new NavigatorLiftedRoomsMessageSerializer( - MessageComposer.NavigatorLiftedRoomsComposer - ) - }, - { - typeof(NavigatorMetaDataMessage), - new NavigatorMetaDataMessageSerializer(MessageComposer.NavigatorMetaDataComposer) - }, - { - typeof(NavigatorSavedSearchesMessage), - new NavigatorSavedSearchesMessageSerializer( - MessageComposer.NavigatorSavedSearchesComposer - ) - }, - { - typeof(NavigatorSearchResultBlocksMessageComposer), - new NavigatorSearchResultBlocksMessageSerializer( - MessageComposer.NavigatorSearchResultBlocksComposer - ) - }, - { - typeof(NewNavigatorPreferencesMessageComposer), - new NewNavigatorPreferencesMessageSerializer( - MessageComposer.NewNavigatorPreferencesComposer - ) - }, - #endregion - - #region Notifications - { - typeof(ActivityPointsMessageComposer), - new ActivityPointsMessageComposerSerializer( - MessageComposer.ActivityPointsMessageComposer - ) - }, - { - typeof(ClubGiftNotificationEventMessageComposer), - new ClubGiftNotificationEventMessageComposerSerializer( - MessageComposer.ClubGiftNotificationComposer - ) - }, - { - typeof(ElementPointerMessageComposer), - new ElementPointerMessageComposerSerializer( - MessageComposer.ElementPointerMessageComposer - ) - }, - { - typeof(HabboAchievementNotificationMessageComposer), - new HabboAchievementNotificationMessageComposerSerializer( - MessageComposer.HabboAchievementNotificationMessageComposer - ) - }, - { - typeof(HabboActivityPointNotificationMessageComposer), - new HabboActivityPointNotificationMessageComposerSerializer( - MessageComposer.HabboActivityPointNotificationMessageComposer - ) - }, - { - typeof(HabboBroadcastMessageComposer), - new HabboBroadcastMessageComposerSerializer( - MessageComposer.HabboBroadcastMessageComposer - ) - }, - { - typeof(InfoFeedEnableMessageComposer), - new InfoFeedEnableMessageComposerSerializer( - MessageComposer.InfoFeedEnableMessageComposer - ) - }, - { - typeof(MOTDNotificationEventMessageComposer), - new MOTDNotificationEventMessageComposerSerializer( - MessageComposer.MOTDNotificationComposer - ) - }, - { - typeof(NotificationDialogMessageComposer), - new NotificationDialogMessageComposerSerializer( - MessageComposer.NotificationDialogMessageComposer - ) - }, - { - typeof(OfferRewardDeliveredMessageComposer), - new OfferRewardDeliveredMessageComposerSerializer( - MessageComposer.OfferRewardDeliveredMessageComposer - ) - }, - { - typeof(PetLevelNotificationEventMessageComposer), - new PetLevelNotificationEventMessageComposerSerializer( - MessageComposer.PetLevelNotificationComposer - ) - }, - { - typeof(RestoreClientMessageComposer), - new RestoreClientMessageComposerSerializer( - MessageComposer.RestoreClientMessageComposer - ) - }, - { - typeof(UnseenItemsEventMessageComposer), - new AccountPreferencesEventMessageComposerSerializer( - MessageComposer.UnseenItemsComposer - ) - }, - #endregion - - #region Perk - { - typeof(PerkAllowancesMessageComposer), - new PerkAllowancesMessageComposerSerializer( - MessageComposer.PerkAllowancesMessageComposer - ) - }, - #endregion - - #region Preferences - { - typeof(AccountPreferencesEventMessageComposer), - new AccountPreferencesEventMessageComposerSerializer( - MessageComposer.AccountPreferencesComposer - ) - }, - #endregion - - #region Quest - { - typeof(QuestsMessageComposer), - new QuestsMessageComposerSerializer(MessageComposer.QuestsMessageComposer) - }, - { - typeof(QuestMessageComposer), - new QuestMessageComposerSerializer(MessageComposer.QuestMessageComposer) - }, - { - typeof(QuestDailyMessageComposer), - new QuestDailyMessageComposerSerializer(MessageComposer.QuestDailyMessageComposer) - }, - { - typeof(SeasonalQuestsMessageComposer), - new SeasonalQuestsMessageComposerSerializer( - MessageComposer.SeasonalQuestsMessageComposer - ) - }, - { - typeof(QuestCompletedMessageComposer), - new QuestCompletedMessageComposerSerializer( - MessageComposer.QuestCompletedMessageComposer - ) - }, - { - typeof(QuestCancelledMessageComposer), - new QuestCancelledMessageComposerSerializer( - MessageComposer.QuestCancelledMessageComposer - ) - }, - { - typeof(EpicPopupMessageComposer), - new EpicPopupMessageComposerSerializer(MessageComposer.EpicPopupMessageComposer) - }, - { - typeof(CommunityGoalHallOfFameMessageComposer), - new CommunityGoalHallOfFameMessageComposerSerializer( - MessageComposer.CommunityGoalHallOfFameMessageComposer - ) - }, - { - typeof(CommunityGoalProgressMessageComposer), - new CommunityGoalProgressMessageComposerSerializer( - MessageComposer.CommunityGoalProgressMessageComposer - ) - }, - { - typeof(ConcurrentUsersGoalProgressMessageComposer), - new ConcurrentUsersGoalProgressMessageComposerSerializer( - MessageComposer.ConcurrentUsersGoalProgressMessageComposer - ) - }, - #endregion - - #region Room - - #region Room Action - { - typeof(AvatarEffectMessageComposer), - new AvatarEffectMessageComposerSerializer( - MessageComposer.AvatarEffectMessageComposer - ) - }, - { - typeof(CarryObjectMessageComposer), - new CarryObjectMessageComposerSerializer(MessageComposer.CarryObjectMessageComposer) - }, - { - typeof(DanceMessageComposer), - new DanceMessageComposerSerializer(MessageComposer.DanceMessageComposer) - }, - { - typeof(ExpressionMessageComposer), - new ExpressionMessageComposerSerializer(MessageComposer.ExpressionMessageComposer) - }, - { - typeof(SleepMessageComposer), - new SleepMessageComposerSerializer(MessageComposer.SleepMessageComposer) - }, - { - typeof(UseObjectMessageComposer), - new UseObjectMessageComposerSerializer(MessageComposer.UseObjectMessageComposer) - }, - #endregion - - #region Room Bots - { - typeof(BotCommandConfigurationMessageComposer), - new BotCommandConfigurationMessageComposerSerializer( - MessageComposer.BotCommandConfigurationComposer - ) - }, - { - typeof(BotErrorMessageComposer), - new BotErrorMessageComposerSerializer(MessageComposer.BotErrorComposer) - }, - { - typeof(BotForceOpenContextMenuMessageComposerSerializer), - new BotForceOpenContextMenuMessageComposerSerializer( - MessageComposer.BotForceOpenContextMenuComposer - ) - }, - { - typeof(BotSkillListUpdateMessageComposer), - new BotSkillListUpdateMessageComposerSerializer( - MessageComposer.BotSkillListUpdateComposer - ) - }, - #endregion - - #region Room Chat - { - typeof(ChatMessageComposer), - new ChatMessageComposerSerializer(MessageComposer.ChatMessageComposer) - }, - { - typeof(FloodControlMessageComposer), - new FloodControlMessageComposerSerializer( - MessageComposer.FloodControlMessageComposer - ) - }, - { - typeof(RemainingMutePeriodMessageComposer), - new RemainingMutePeriodMessageComposerSerializer( - MessageComposer.RemainingMutePeriodComposer - ) - }, - { - typeof(RoomChatSettingsMessageComposer), - new RoomChatSettingsMessageComposerSerializer( - MessageComposer.RoomChatSettingsMessageComposer - ) - }, - { - typeof(RoomFilterSettingsMessageComposer), - new RoomFilterSettingsMessageComposerSerializer( - MessageComposer.RoomFilterSettingsMessageComposer - ) - }, - { - typeof(ShoutMessageComposer), - new ShoutMessageComposerSerializer(MessageComposer.ShoutMessageComposer) - }, - { - typeof(UserTypingMessageComposer), - new UserTypingMessageComposerSerializer(MessageComposer.UserTypingMessageComposer) - }, - { - typeof(WhisperMessageComposer), - new WhisperMessageComposerSerializer(MessageComposer.WhisperMessageComposer) - }, - #endregion - - #region Room Engine - { - typeof(BuildersClubPlacementWarningMessageComposer), - new BuildersClubPlacementWarningMessageComposerSerializer( - MessageComposer.BuildersClubPlacementWarningMessageComposer - ) - }, - { - typeof(FavoriteMembershipUpdateMessageComposer), - new FavoriteMembershipUpdateMessageComposerSerializer( - MessageComposer.FavoriteMembershipUpdateMessageComposer - ) - }, - { - typeof(FloorHeightMapMessageComposer), - new FloorHeightMapMessageComposerSerializer( - MessageComposer.FloorHeightMapMessageComposer - ) - }, - { - typeof(FurnitureAliasesMessageComposer), - new FurnitureAliasesMessageComposerSerializer( - MessageComposer.FurnitureAliasesMessageComposer - ) - }, - { - typeof(HeightMapMessageComposer), - new HeightMapMessageComposerSerializer(MessageComposer.HeightMapMessageComposer) - }, - { - typeof(HeightMapUpdateMessageComposer), - new HeightMapUpdateMessageComposerSerializer( - MessageComposer.HeightMapUpdateMessageComposer - ) - }, - { - typeof(ItemAddMessageComposer), - new ItemAddMessageComposerSerializer(MessageComposer.ItemAddMessageComposer) - }, - { - typeof(ItemDataUpdateMessageComposer), - new ItemDataUpdateMessageComposerSerializer( - MessageComposer.ItemDataUpdateMessageComposer - ) - }, - { - typeof(ItemRemoveMessageComposer), - new ItemRemoveMessageComposerSerializer(MessageComposer.ItemRemoveMessageComposer) - }, - { - typeof(ItemsMessageComposer), - new ItemsMessageComposerSerializer(MessageComposer.ItemsMessageComposer) - }, - { - typeof(ItemsStateUpdateMessageComposer), - new ItemsStateUpdateMessageComposerSerializer( - MessageComposer.ItemsStateUpdateMessageComposer - ) - }, - { - typeof(ItemStateUpdateMessageComposer), - new ItemStateUpdateMessageComposerSerializer( - MessageComposer.ItemStateUpdateMessageComposer - ) - }, - { - typeof(ItemUpdateMessageComposer), - new ItemUpdateMessageComposerSerializer(MessageComposer.ItemUpdateMessageComposer) - }, - { - typeof(ObjectAddMessageComposer), - new ObjectAddMessageComposerSerializer(MessageComposer.ObjectAddMessageComposer) - }, - { - typeof(ObjectDataUpdateMessageComposer), - new ObjectDataUpdateMessageComposerSerializer( - MessageComposer.ObjectDataUpdateMessageComposer - ) - }, - { - typeof(ObjectRemoveConfirmMessageComposer), - new ObjectRemoveConfirmMessageComposerSerializer( - MessageComposer.ObjectRemoveConfirmMessageComposer - ) - }, - { - typeof(ObjectRemoveMessageComposer), - new ObjectRemoveMessageComposerSerializer( - MessageComposer.ObjectRemoveMessageComposer - ) - }, - { - typeof(ObjectRemoveMultipleMessageComposer), - new ObjectRemoveMultipleMessageComposerSerializer( - MessageComposer.ObjectRemoveMultipleMessageComposer - ) - }, - { - typeof(ObjectsDataUpdateMessageComposer), - new ObjectsDataUpdateMessageComposerSerializer( - MessageComposer.ObjectsDataUpdateMessageComposer - ) - }, - { - typeof(ObjectsMessageComposer), - new ObjectsMessageComposerSerializer(MessageComposer.ObjectsMessageComposer) - }, - { - typeof(ObjectUpdateMessageComposer), - new ObjectUpdateMessageComposerSerializer( - MessageComposer.ObjectUpdateMessageComposer - ) - }, - { - typeof(VortexFurniEditorDataMessageComposer), - new VortexFurniEditorDataMessageComposerSerializer( - MessageComposer.VortexFurniEditorDataMessageComposer - ) - }, - { - typeof(VortexFurniEditorRightsMessageComposer), - new VortexFurniEditorRightsMessageComposerSerializer( - MessageComposer.VortexFurniEditorRightsMessageComposer - ) - }, - { - typeof(VortexFurniDefinitionMessageComposer), - new VortexFurniDefinitionMessageComposerSerializer( - MessageComposer.VortexFurniDefinitionMessageComposer - ) - }, - { - typeof(RoomEntryInfoMessageComposer), - new RoomEntryInfoMessageComposerSerializer( - MessageComposer.RoomEntryInfoMessageComposer - ) - }, - { - typeof(RoomPropertyMessageComposer), - new RoomPropertyMessageComposerSerializer( - MessageComposer.RoomPropertyMessageComposer - ) - }, - { - typeof(RoomVisualizationSettingsMessageComposer), - new RoomVisualizationSettingsMessageComposerSerializer( - MessageComposer.RoomVisualizationSettingsComposer - ) - }, - { - typeof(SlideObjectBundleMessageComposer), - new SlideObjectBundleMessageComposerSerializer( - MessageComposer.SlideObjectBundleMessageComposer - ) - }, - { - typeof(SpecialRoomEffectMessageComposer), - new SpecialRoomEffectMessageComposerSerializer( - MessageComposer.SpecialRoomEffectMessageComposer - ) - }, - { - typeof(UserChangeMessageComposer), - new UserChangeMessageComposerSerializer(MessageComposer.UserChangeMessageComposer) - }, - { - typeof(UserRemoveMessageComposer), - new UserRemoveMessageComposerSerializer(MessageComposer.UserRemoveMessageComposer) - }, - { - typeof(UsersMessageComposer), - new UsersMessageComposerSerializer(MessageComposer.UsersMessageComposer) - }, - { - typeof(UserUpdateMessageComposer), - new UserUpdateMessageComposerSerializer(MessageComposer.UserUpdateMessageComposer) - }, - { - typeof(WiredMovementsMessageComposer), - new WiredMovementsMessageComposerSerializer( - MessageComposer.WiredMovementsMessageComposer - ) - }, - #endregion - - #region Room Furniture - { - typeof(AreaHideMessageComposer), - new AreaHideMessageComposerSerializer(MessageComposer.AreaHideMessageComposer) - }, - { - typeof(CustomStackingHeightUpdateMessageComposer), - new CustomStackingHeightUpdateMessageComposerSerializer( - MessageComposer.CustomStackingHeightUpdateMessageComposer - ) - }, - { - typeof(CustomUserNotificationMessageComposer), - new CustomUserNotificationMessageComposerSerializer( - MessageComposer.CustomUserNotificationMessageComposer - ) - }, - { - typeof(DiceValueMessageComposer), - new DiceValueMessageComposerSerializer(MessageComposer.DiceValueMessageComposer) - }, - { - typeof(FurniRentOrBuyoutOfferMessageComposer), - new FurniRentOrBuyoutOfferMessageComposerSerializer( - MessageComposer.FurniRentOrBuyoutOfferMessageComposer - ) - }, - { - typeof(GuildFurniContextMenuInfoMessageComposer), - new GuildFurniContextMenuInfoMessageComposerSerializer( - MessageComposer.GuildFurniContextMenuInfoMessageComposer - ) - }, - { - typeof(OneWayDoorStatusMessageComposer), - new OneWayDoorStatusMessageComposerSerializer( - MessageComposer.OneWayDoorStatusMessageComposer - ) - }, - { - typeof(OpenPetPackageRequestedMessageComposer), - new OpenPetPackageRequestedMessageComposerSerializer( - MessageComposer.OpenPetPackageRequestedMessageComposer - ) - }, - { - typeof(OpenPetPackageResultMessageComposer), - new OpenPetPackageResultMessageComposerSerializer( - MessageComposer.OpenPetPackageResultMessageComposer - ) - }, - { - typeof(PresentOpenedMessageComposer), - new PresentOpenedMessageComposerSerializer( - MessageComposer.PresentOpenedMessageComposer - ) - }, - { - typeof(RentableSpaceConfigMessageComposer), - new RentableSpaceConfigMessageComposerSerializer( - MessageComposer.RentableSpaceConfigMessageComposer - ) - }, - { - typeof(RentableSpaceRentFailedMessageComposer), - new RentableSpaceRentFailedMessageComposerSerializer( - MessageComposer.RentableSpaceRentFailedMessageComposer - ) - }, - { - typeof(RentableSpaceRentOkMessageComposer), - new RentableSpaceRentOkMessageComposerSerializer( - MessageComposer.RentableSpaceRentOkMessageComposer - ) - }, - { - typeof(RentableSpaceStatusMessageComposer), - new RentableSpaceStatusMessageComposerSerializer( - MessageComposer.RentableSpaceStatusMessageComposer - ) - }, - { - typeof(RequestSpamWallPostItMessageComposer), - new RequestSpamWallPostItMessageComposerSerializer( - MessageComposer.RequestSpamWallPostItMessageComposer - ) - }, - { - typeof(RoomDimmerPresetsMessageComposer), - new RoomDimmerPresetsMessageComposerSerializer( - MessageComposer.RoomDimmerPresetsMessageComposer - ) - }, - { - typeof(RoomMessageNotificationMessageComposer), - new RoomMessageNotificationMessageComposerSerializer( - MessageComposer.RoomMessageNotificationMessageComposer - ) - }, - { - typeof(YoutubeControlVideoMessageComposer), - new YoutubeControlVideoMessageComposerSerializer( - MessageComposer.YoutubeControlVideoMessageComposer - ) - }, - { - typeof(YoutubeDisplayPlaylistsMessageComposer), - new YoutubeDisplayPlaylistsMessageComposerSerializer( - MessageComposer.YoutubeDisplayPlaylistsMessageComposer - ) - }, - { - typeof(YoutubeDisplayVideoMessageComposer), - new YoutubeDisplayVideoMessageComposerSerializer( - MessageComposer.YoutubeDisplayVideoMessageComposer - ) - }, - #endregion - - #region Room Layout - { - typeof(RoomEntryTileMessageComposer), - new RoomEntryTileMessageComposerSerializer( - MessageComposer.RoomEntryTileMessageComposer - ) - }, - { - typeof(RoomOccupiedTilesMessageComposer), - new RoomOccupiedTilesMessageComposerSerializer( - MessageComposer.RoomOccupiedTilesMessageComposer - ) - }, - #endregion - - #region Room Permissions - { - typeof(YouAreControllerMessageComposer), - new YouAreControllerMessageComposerSerializer( - MessageComposer.YouAreControllerMessageComposer - ) - }, - { - typeof(YouAreNotControllerMessageComposer), - new YouAreNotControllerMessageComposerSerializer( - MessageComposer.YouAreNotControllerMessageComposer - ) - }, - { - typeof(YouAreOwnerMessageComposer), - new YouAreOwnerMessageComposerSerializer(MessageComposer.YouAreOwnerMessageComposer) - }, - #endregion - - #region Room Pets - { - typeof(PetBreedingResultEventMessageComposer), - new PetBreedingResultEventMessageComposerSerializer( - MessageComposer.PetBreedingResultComposer - ) - }, - { - typeof(PetCommandsMessageComposer), - new PetCommandsMessageComposerSerializer(MessageComposer.PetCommandsMessageComposer) - }, - { - typeof(PetExperienceMessageComposer), - new PetExperienceMessageComposerSerializer(MessageComposer.PetExperienceComposer) - }, - { - typeof(PetFigureUpdateMessageComposer), - new PetFigureUpdateMessageComposerSerializer( - MessageComposer.PetFigureUpdateComposer - ) - }, - { - typeof(PetInfoMessageComposer), - new PetInfoMessageComposerSerializer(MessageComposer.PetInfoMessageComposer) - }, - { - typeof(PetLevelUpdateMessageComposer), - new PetLevelUpdateMessageComposerSerializer(MessageComposer.PetLevelUpdateComposer) - }, - { - typeof(PetPlacingErrorMessageComposer), - new PetPlacingErrorMessageComposerSerializer( - MessageComposer.PetPlacingErrorComposer - ) - }, - { - typeof(PetRespectFailedMessageComposer), - new PetRespectFailedMessageComposerSerializer( - MessageComposer.PetRespectFailedComposer - ) - }, - { - typeof(PetStatusUpdateMessageComposer), - new PetStatusUpdateMessageComposerSerializer( - MessageComposer.PetStatusUpdateComposer - ) - }, - #endregion - - #region Room Session - { - typeof(CantConnectMessageComposer), - new CantConnectMessageComposerSerializer(MessageComposer.CantConnectMessageComposer) - }, - { - typeof(CloseConnectionMessageComposer), - new CloseConnectionMessageComposerSerializer( - MessageComposer.CloseConnectionMessageComposer - ) - }, - { - typeof(FlatAccessibleMessageComposer), - new FlatAccessibleMessageComposerSerializer( - MessageComposer.FlatAccessibleMessageComposer - ) - }, - { - typeof(GamePlayerValueMessageComposer), - new GamePlayerValueMessageComposerSerializer( - MessageComposer.GamePlayerValueMessageComposer - ) - }, - { - typeof(HanditemConfigurationMessageComposer), - new HanditemConfigurationMessageComposerSerializer( - MessageComposer.HanditemConfigurationMessageComposer - ) - }, - { - typeof(OpenConnectionMessageComposer), - new OpenConnectionMessageComposerSerializer( - MessageComposer.OpenConnectionMessageComposer - ) - }, - { - typeof(RoomForwardMessageComposer), - new RoomForwardMessageComposerSerializer(MessageComposer.RoomForwardMessageComposer) - }, - { - typeof(RoomQueueStatusMessageComposer), - new RoomQueueStatusMessageComposerSerializer( - MessageComposer.RoomQueueStatusMessageComposer - ) - }, - { - typeof(RoomReadyMessageComposer), - new RoomReadyMessageComposerSerializer(MessageComposer.RoomReadyMessageComposer) - }, - { - typeof(YouAreNotSpectatorMessageComposer), - new YouAreNotSpectatorMessageComposerSerializer( - MessageComposer.YouAreNotSpectatorMessageComposer - ) - }, - { - typeof(YouArePlayingGameMessageComposer), - new YouArePlayingGameMessageComposerSerializer( - MessageComposer.YouArePlayingGameMessageComposer - ) - }, - { - typeof(YouAreSpectatorMessageComposer), - new YouAreSpectatorMessageComposerSerializer( - MessageComposer.YouAreSpectatorMessageComposer - ) - }, - #endregion - - #endregion - - #region Tracking - { - typeof(LatencyPingResponseMessage), - new LatencyPingResponseMessageSerializer( - MessageComposer.LatencyPingResponseMessageComposer - ) - }, - #endregion - - #region Userdefinedroomevents - { - typeof(OpenEventMessageComposer), - new OpenEventMessageComposerSerializer(MessageComposer.OpenComposer) - }, - { - typeof(WiredFurniActionEventMessageComposer), - new WiredFurniActionEventMessageComposerSerializer( - MessageComposer.WiredFurniActionComposer - ) - }, - { - typeof(WiredFurniAddonEventMessageComposer), - new WiredFurniAddonEventMessageComposerSerializer( - MessageComposer.WiredFurniAddonComposer - ) - }, - { - typeof(WiredFurniConditionEventMessageComposer), - new WiredFurniConditionEventMessageComposerSerializer( - MessageComposer.WiredFurniConditionComposer - ) - }, - { - typeof(WiredFurniSelectorEventMessageComposer), - new WiredFurniSelectorEventMessageComposerSerializer( - MessageComposer.WiredFurniSelectorComposer - ) - }, - { - typeof(WiredFurniTriggerEventMessageComposer), - new WiredFurniTriggerEventMessageComposerSerializer( - MessageComposer.WiredFurniTriggerComposer - ) - }, - { - typeof(WiredFurniVariableEventMessageComposer), - new WiredFurniVariableEventMessageComposerSerializer( - MessageComposer.WiredFurniVariableComposer - ) - }, - { - typeof(WiredRewardResultMessageComposer), - new WiredRewardResultMessageComposerSerializer( - MessageComposer.WiredRewardResultMessageComposer - ) - }, - { - typeof(WiredSaveSuccessEventMessageComposer), - new WiredSaveSuccessEventMessageComposerSerializer( - MessageComposer.WiredSaveSuccessComposer - ) - }, - { - typeof(WiredValidationErrorEventMessageComposer), - new WiredValidationErrorEventMessageComposerSerializer( - MessageComposer.WiredValidationErrorComposer - ) - }, - { - typeof(WiredAllVariableHoldersEventMessageComposer), - new WiredAllVariableHoldersEventMessageComposerSerializer( - MessageComposer.WiredAllVariableHoldersComposer - ) - }, - { - typeof(WiredAllVariablesDiffsEventMessageComposer), - new WiredAllVariablesDiffsEventMessageComposerSerializer( - MessageComposer.WiredAllVariablesDiffsComposer - ) - }, - { - typeof(WiredAllVariablesHashEventMessageComposer), - new WiredAllVariablesHashEventMessageComposerSerializer( - MessageComposer.WiredAllVariablesHashComposer - ) - }, - { - typeof(WiredErrorLogsEventMessageComposer), - new WiredErrorLogsEventMessageComposerSerializer( - MessageComposer.WiredErrorLogsComposer - ) - }, - { - typeof(WiredMenuErrorEventMessageComposer), - new WiredMenuErrorEventMessageComposerSerializer( - MessageComposer.WiredMenuErrorComposer - ) - }, - { - typeof(WiredPermissionsEventMessageComposer), - new WiredPermissionsEventMessageComposerSerializer( - MessageComposer.WiredPermissionsComposer - ) - }, - { - typeof(WiredRoomSettingsEventMessageComposer), - new WiredRoomSettingsEventMessageComposerSerializer( - MessageComposer.WiredRoomSettingsComposer - ) - }, - { - typeof(WiredRoomLogsComposer), - new WiredRoomLogsComposerSerializer(MessageComposer.WiredRoomLogsComposer) - }, - { - typeof(WiredRoomStatsEventMessageComposer), - new WiredRoomStatsEventMessageComposerSerializer( - MessageComposer.WiredRoomStatsComposer - ) - }, - { - typeof(WiredSetUserPermanentVariableResultComposer), - new WiredSetUserPermanentVariableResultComposerSerializer( - MessageComposer.WiredSetUserPermanentVariableResultComposer - ) - }, - { - typeof(WiredUserPermanentVariablesComposer), - new WiredUserPermanentVariablesComposerSerializer( - MessageComposer.WiredUserPermanentVariablesComposer - ) - }, - { - typeof(WiredUserVariablesListComposer), - new WiredUserVariablesListComposerSerializer( - MessageComposer.WiredUserVariablesListComposer - ) - }, - { - typeof(WiredVariablesForObjectEventMessageComposer), - new WiredVariablesForObjectEventMessageComposerSerializer( - MessageComposer.WiredVariablesForObjectComposer - ) - }, - #endregion - - #region Users - { - typeof(GuildCreatedMessageComposer), - new GuildCreatedMessageComposerSerializer( - MessageComposer.GuildCreatedMessageComposer - ) - }, - { - typeof(GuildCreationInfoMessageComposer), - new GuildCreationInfoMessageComposerSerializer( - MessageComposer.GuildCreationInfoMessageComposer - ) - }, - { - typeof(HabboGroupDetailsMessageComposer), - new HabboGroupDetailsMessageComposerSerializer( - MessageComposer.HabboGroupDetailsMessageComposer - ) - }, - { - typeof(GuildMembershipsMessageComposer), - new GuildMembershipsMessageComposerSerializer( - MessageComposer.GuildMembershipsMessageComposer - ) - }, - { - typeof(GuildMembersMessageComposer), - new GuildMembersMessageComposerSerializer( - MessageComposer.GuildMembersMessageComposer - ) - }, - { - typeof(HabboGroupJoinFailedMessageComposer), - new HabboGroupJoinFailedMessageComposerSerializer( - MessageComposer.HabboGroupJoinFailedMessageComposer - ) - }, - { - typeof(GuildEditInfoMessageComposer), - new GuildEditInfoMessageComposerSerializer( - MessageComposer.GuildEditInfoMessageComposer - ) - }, - { - typeof(GuildEditorDataMessageComposer), - new GuildEditorDataMessageComposerSerializer( - MessageComposer.GuildEditorDataMessageComposer - ) - }, - { - typeof(GuildEditFailedMessageComposer), - new GuildEditFailedMessageComposerSerializer( - MessageComposer.GuildEditFailedMessageComposer - ) - }, - { - typeof(GuildMembershipUpdatedMessageComposer), - new GuildMembershipUpdatedMessageComposerSerializer( - MessageComposer.GuildMembershipUpdatedMessageComposer - ) - }, - { - typeof(GuildMembershipRejectedMessageComposer), - new GuildMembershipRejectedMessageComposerSerializer( - MessageComposer.GuildMembershipRejectedMessageComposer - ) - }, - { - typeof(GuildMemberMgmtFailedMessageComposer), - new GuildMemberMgmtFailedMessageComposerSerializer( - MessageComposer.GuildMemberMgmtFailedMessageComposer - ) - }, - { - typeof(HabboGroupDeactivatedMessageComposer), - new HabboGroupDeactivatedMessageComposerSerializer( - MessageComposer.HabboGroupDeactivatedMessageComposer - ) - }, - { - typeof(GroupMembershipRequestedMessageComposer), - new GroupMembershipRequestedMessageComposerSerializer( - MessageComposer.GroupMembershipRequestedMessageComposer - ) - }, - { - typeof(HabboGroupBadgesMessageComposer), - new HabboGroupBadgesMessageComposerSerializer( - MessageComposer.HabboGroupBadgesMessageComposer - ) - }, - { - typeof(GuildMemberFurniCountInHQMessageComposer), - new GuildMemberFurniCountInHQMessageComposerSerializer( - MessageComposer.GuildMemberFurniCountInHQMessageComposer - ) - }, - { - typeof(GroupDetailsChangedMessageComposer), - new GroupDetailsChangedMessageComposerSerializer( - MessageComposer.GroupDetailsChangedMessageComposer - ) - }, - { - typeof(AccountSafetyLockStatusChangeMessageComposer), - new AccountSafetyLockStatusChangeMessageComposerSerializer( - MessageComposer.AccountSafetyLockStatusChangeMessageComposer - ) - }, - { - typeof(ApproveNameMessageComposer), - new ApproveNameMessageComposerSerializer(MessageComposer.ApproveNameMessageComposer) - }, - { - typeof(ChangeEmailResultEventMessageComposer), - new ChangeEmailResultEventMessageComposerSerializer( - MessageComposer.ChangeEmailResultComposer - ) - }, - { - typeof(EmailStatusResultEventMessageComposer), - new EmailStatusResultEventMessageComposerSerializer( - MessageComposer.EmailStatusResultComposer - ) - }, - { - typeof(ExtendedProfileMessageComposer), - new ExtendedProfileMessageComposerSerializer( - MessageComposer.ExtendedProfileMessageComposer - ) - }, - { - typeof(ExtendedProfileChangedMessageComposer), - new ExtendedProfileChangedMessageComposerSerializer( - MessageComposer.ExtendedProfileChangedMessageComposer - ) - }, - { - typeof(BlockListMessageComposer), - new BlockListMessageComposerSerializer(MessageComposer.BlockListMessageComposer) - }, - { - typeof(BlockUserUpdateMessageComposer), - new BlockUserUpdateMessageComposerSerializer( - MessageComposer.BlockUserUpdateMessageComposer - ) - }, - { - typeof(IgnoreResultMessageComposer), - new IgnoreResultMessageComposerSerializer( - MessageComposer.IgnoreResultMessageComposer - ) - }, - { - typeof(IgnoredUsersMessageComposer), - new IgnoredUsersMessageComposerSerializer( - MessageComposer.IgnoredUsersMessageComposer - ) - }, - { - typeof(RelationshipStatusInfoEventMessageComposer), - new RelationshipStatusInfoEventMessageComposerSerializer( - MessageComposer.RelationshipStatusInfoComposer - ) - }, - { - typeof(ScrSendKickbackInfoMessageComposer), - new ScrSendKickbackInfoMessageComposerSerializer( - MessageComposer.ScrSendKickbackInfoMessageComposer - ) - }, - { - typeof(ScrSendUserInfoMessageComposer), - new ScrSendUserInfoMessageSerializer(MessageComposer.ScrSendUserInfoComposer) - }, - #endregion - - #region Valut - { - typeof(IncomeRewardClaimResponseMessageComposer), - new IncomeRewardClaimResponseMessageComposerSerializer( - MessageComposer.IncomeRewardClaimResponseMessageComposer - ) - }, - { - typeof(IncomeRewardStatusMessageComposer), - new IncomeRewardStatusMessageComposerSerializer( - MessageComposer.IncomeRewardStatusMessageComposer - ) - }, - #endregion - }; - #endregion + new AdvertisementMap(), + new AvatarMap(), + new CameraMap(), + new CampaignMap(), + new CatalogMap(), + new CollectiblesMap(), + new CompetitionMap(), + new CraftingMap(), + new FriendFurniMap(), + new FriendListMap(protocolLimits.Value), + new GameMap(), + new GiftsMap(), + new GroupForumsMap(), + new HandshakeMap(), + new HelpMap(), + new HotlooksMap(), + new InventoryMap(protocolLimits.Value), + new LandingViewMap(), + new MarketplaceMap(), + new ModeratorMap(), + new MysteryBoxMap(), + new NavigatorMap(), + new NewNavigatorMap(), + new NftMap(), + new NotificationsMap(), + new NuxMap(), + new PollMap(), + new PreferencesMap(), + new QuestMap(), + new RegisterMap(), + new RoomMap(), + new RoomDirectoryMap(), + new RoomSettingsMap(protocolLimits.Value), + new SoundMap(), + new TalentMap(), + new TrackingMap(), + new UserClassificationMap(), + new UserDefinedRoomEventsMap(), + new UsersMap(), + new VaultMap(), + new AvailabilityMap(), + new CallForHelpMap(), + new PerkMap(), + } + ) +{ + public override string Revision => "WIN63-202607011411-782849652"; } diff --git a/Vortex.Revisions/RevisionBase.cs b/Vortex.Revisions/RevisionBase.cs new file mode 100644 index 00000000..4ee72588 --- /dev/null +++ b/Vortex.Revisions/RevisionBase.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Primitives.Packets; + +namespace Vortex.Revisions; + +/// +/// Base implementation of that assembles its parser/serializer tables from +/// a set of instances, one per message domain. A descendant revision that +/// only needs to change a handful of entries relative to this one can inherit from an existing +/// revision and override / to add or +/// replace specific entries on top of whatever the maps already registered - without duplicating +/// every other entry. +/// +public abstract class RevisionBase : IRevision +{ + protected RevisionBase(IEnumerable maps) + { + RevisionMapBuilder builder = new(); + + foreach (IRevisionMap map in maps) + { + map.RegisterInto(builder); + } + + ConfigureParsers(builder); + ConfigureSerializers(builder); + + Parsers = builder.Parsers; + Serializers = builder.Serializers; + } + + public abstract string Revision { get; } + + public IReadOnlyDictionary Parsers { get; } + + public IReadOnlyDictionary Serializers { get; } + + /// + /// Override to add or replace specific parser entries after all maps have registered theirs. + /// Runs once, during construction. + /// + protected virtual void ConfigureParsers(IRevisionMapBuilder builder) { } + + /// + /// Override to add or replace specific serializer entries after all maps have registered theirs. + /// Runs once, during construction. + /// + protected virtual void ConfigureSerializers(IRevisionMapBuilder builder) { } +} diff --git a/Vortex.Revisions/RevisionMapBuilder.cs b/Vortex.Revisions/RevisionMapBuilder.cs new file mode 100644 index 00000000..00f1338e --- /dev/null +++ b/Vortex.Revisions/RevisionMapBuilder.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using Vortex.Primitives.Networking.Revisions; +using Vortex.Primitives.Packets; + +namespace Vortex.Revisions; + +/// +/// Mutable collector used while a revision is being assembled from its +/// instances. Frozen (exposed as read-only) once is done building it. +/// +internal sealed class RevisionMapBuilder : IRevisionMapBuilder +{ + private readonly Dictionary _parsers = new(); + private readonly Dictionary _serializers = new(); + + public IReadOnlyDictionary Parsers => _parsers; + + public IReadOnlyDictionary Serializers => _serializers; + + public void MapParser(int header, IParser parser) + { + if (!_parsers.TryAdd(header, parser)) + { + throw new InvalidOperationException( + $"Header {header} is already mapped to a parser - two maps registered the same " + + "header instead of one being deleted or renamed." + ); + } + } + + public void MapSerializer(Type composerType, ISerializer serializer) + { + if (!_serializers.TryAdd(composerType, serializer)) + { + throw new InvalidOperationException( + $"Composer type {composerType} is already mapped to a serializer - two maps " + + "registered the same type instead of one being deleted or renamed." + ); + } + } +}