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 + + + + + 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.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/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/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!; } 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.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.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.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.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 a574913c..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,11 +96,39 @@ public static HostApplicationBuilder AddOrleans(this HostApplicationBuilder buil listenOnAnyHostAddress: true ); - silo.UseLocalhostClustering() - .AddMemoryGrainStorage(OrleansStorageNames.PUB_SUB_STORE) - .AddMemoryGrainStorage(OrleansStorageNames.PLAYER_STORE) - .AddMemoryGrainStorage(OrleansStorageNames.ROOM_STORE) - .AddMemoryStreams(OrleansStreamProviders.DEFAULT_STREAM_PROVIDER) + 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. + 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/Program.cs b/Vortex.Main/Program.cs index 5d8b7cf4..b3e3865a 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); } } } @@ -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.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 @@ + + + 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/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.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/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..345a71fe 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); @@ -30,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.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.PacketHandlers/UserDefinedRoomEvents/OpenMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/OpenMessageHandler.cs index 55af7b56..81754600 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/OpenMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/OpenMessageHandler.cs @@ -66,6 +66,6 @@ CancellationToken ct return; } - _ = ctx.SendComposerAsync(composer, ct).ConfigureAwait(false); + await ctx.SendComposerAsync(composer, ct).ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateActionMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateActionMessageHandler.cs index 7b16a6c4..99148369 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateActionMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateActionMessageHandler.cs @@ -34,7 +34,7 @@ CancellationToken ct return; } - _ = ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) + await ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) .ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateAddonMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateAddonMessageHandler.cs index 580ee9f3..72d402b6 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateAddonMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateAddonMessageHandler.cs @@ -34,7 +34,7 @@ CancellationToken ct return; } - _ = ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) + await ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) .ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateConditionMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateConditionMessageHandler.cs index 75124ec4..3b89715b 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateConditionMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateConditionMessageHandler.cs @@ -34,7 +34,7 @@ CancellationToken ct return; } - _ = ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) + await ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) .ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateSelectorMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateSelectorMessageHandler.cs index 6b6cd5a5..00db7186 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateSelectorMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateSelectorMessageHandler.cs @@ -34,7 +34,7 @@ CancellationToken ct return; } - _ = ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) + await ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) .ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateTriggerMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateTriggerMessageHandler.cs index 2734bcb6..dff2f865 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateTriggerMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateTriggerMessageHandler.cs @@ -31,7 +31,7 @@ CancellationToken ct .ConfigureAwait(false) ) { - _ = ctx.SendComposerAsync( + await ctx.SendComposerAsync( new WiredValidationErrorEventMessageComposer { LocalizationKey = "wired.validation.error", @@ -44,7 +44,7 @@ CancellationToken ct return; } - _ = ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) + await ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) .ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateVariableMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateVariableMessageHandler.cs index d9f83c87..ab4ede9a 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateVariableMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/UpdateVariableMessageHandler.cs @@ -34,7 +34,7 @@ CancellationToken ct return; } - _ = ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) + await ctx.SendComposerAsync(new WiredSaveSuccessEventMessageComposer(), ct) .ConfigureAwait(false); } } diff --git a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredClearErrorLogsMessageHandler.cs b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredClearErrorLogsMessageHandler.cs index 3832ba5e..574e83a1 100644 --- a/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredClearErrorLogsMessageHandler.cs +++ b/Vortex.PacketHandlers/UserDefinedRoomEvents/Wiredmenu/WiredClearErrorLogsMessageHandler.cs @@ -33,7 +33,7 @@ CancellationToken ct List 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.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 + + + + + + + + + + + + + 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.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/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/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.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.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/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.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.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/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." + ); + } + } +} 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/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/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/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.Trading.cs b/Vortex.Rooms/Grains/RoomGrain.Trading.cs index abe47888..5677502d 100644 --- a/Vortex.Rooms/Grains/RoomGrain.Trading.cs +++ b/Vortex.Rooms/Grains/RoomGrain.Trading.cs @@ -1,714 +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; - - 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); - - 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; - } - finally - { - await dbCtx.DisposeAsync().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 445dac32..75cb1664 100644 --- a/Vortex.Rooms/Grains/RoomGrain.cs +++ b/Vortex.Rooms/Grains/RoomGrain.cs @@ -81,6 +81,9 @@ 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; + public readonly RoomTradingSystem TradingSystem; internal IAsyncStream _roomOutbound = default!; @@ -138,6 +141,9 @@ RoomWiredLogChannel wiredLogChannel GameSystem = new RoomGameSystem(this); FreezeSystem = new RoomFreezeSystem(this); 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/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; + } } 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); +} 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) + ); +} 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); 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(); 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); } } 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,