From 7df783302b226f831ba653b960cb9d204b1177e8 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Tue, 11 Aug 2026 11:36:51 -0400 Subject: [PATCH] [notify] add documentation on push shapes --- apps/notify/src/notification-payloads.ts | 581 +++++++++++++++++++++++ apps/notify/src/notification-types.ts | 99 +++- 2 files changed, 678 insertions(+), 2 deletions(-) create mode 100644 apps/notify/src/notification-payloads.ts diff --git a/apps/notify/src/notification-payloads.ts b/apps/notify/src/notification-payloads.ts new file mode 100644 index 0000000..f7bff86 --- /dev/null +++ b/apps/notify/src/notification-payloads.ts @@ -0,0 +1,581 @@ +/** + * The `Msg` bodies the client expects behind each {@link NotificationType}. + * + * Recovered from the client's Utf8Json generated formatters (see `recnet-patcher`, + * `il2cpp-tools/dtoshape.py`), not from a spec. Three properties of that decoder decide how + * much these matter: + * + * - **Every key is accepted in three casings** — Original, camelCase and all-lowercase. So + * `RoomId`, `roomId` and `roomid` are the same field. The spelling used below is the + * client's own canonical one. + * - **Unknown members are dropped in silence.** A typo'd key behaves exactly like an omitted + * one, which is why a wrong payload shows up as a blank UI rather than an error. + * - **A missing nested object is worse than a missing scalar.** Several handlers dereference + * one level down with no null guard, so omitting a nested field surfaces as a bare + * `NullReferenceException` in the client. Send a stub rather than nothing. + * + * Field *names* below are verified. Where a payload's type mapping could not be pinned down + * as confidently as its key list, the interface says so on the member. + */ + +/** How a balance came to change. Log/telemetry only on the purchase frame — see below. */ +export enum BalanceAddType { + Invalid = 0, + DirectBalanceWithMultiplier = 1, + FromGiftBox = 2, + NUXChallenge = 10, + AllNUXChallenges = 11, + DailyChallenge = 100, + AllDailyChallenges = 101, + FinishActivity = 200, + RecRoyaleMatchFinished = 250, + ChecklistCredit = 303, + WonGame = 1000, + LostGame = 1001, + WonGameRateLimited = 1002, + WonGamePartial = 1003, + LevelUp = 1100, + Registered = 1200, + CreatorReward = 1300, + CommercePurchase = 1400, + CommercePurchaseRevoked = 1401, + ManualRefund = 2000, + ManualThanks = 2010, + ManualApology = 2020, +} + +/** + * Which store a balance belongs to. Note the **wire key is `Platform`, not `BalanceType`** — + * the client's property is called `BalanceType` but carries a `[DataMember]` rename, so + * `balanceType` on the wire is dropped and the balance silently reads as `SteamPurchased`. + * + * Balances are held per `(CurrencyType, Platform)` pair, so this also selects which bucket a + * balance frame updates. `RecNetPurchased` is the one to use for a self-hosted store. + */ +export enum BalancePlatform { + NonPurchasedNotUsableInP2P = -2, + NonPurchasedDefault = -1, + SteamPurchased = 0, + OculusPurchased = 1, + PlayStationPurchased = 2, + MicrosoftPurchased = 3, + RecNetPurchased = 4, + IOSPurchased = 5, + GooglePlayPurchased = 6, + PicoPurchased = 8, + PlayStationNonPurchasedP2P = 100, + NonPlayStationNonPurchasedP2P = 101, + NonPurchasedEarnedByP2P = 1000, +} + +export enum CurrencyType { + Invalid = 0, + LaserTagTickets = 1, + RecCenterTokens = 2, + LostSkullsGold = 100, + DraculaSilver = 101, + RecRoyaleSeason1 = 200, + RoomCurrency = 300, + ProgressionEvent = 400, +} + +/** Why a player was kicked/banned/warned. Shared by ModerationKick and ModerationUnkick. */ +export enum KickReportCategory { + Moderator = -1, + Unknown = 0, + DeprecatedMicrophoneAbuse = 1, + Harassment = 2, + Cheating = 3, + DeprecatedImmatureBehavior = 4, + AFK = 5, + Misc = 6, + Underage = 7, + VoteKick = 10, + MisleadingPurchases = 11, + CoCUnderage = 100, + CoCSexual = 101, + CoCDiscrimination = 102, + CoCTrolling = 103, + CoCNameOrProfile = 104, + InappropriateClothing = 200, + IssuingInaccurateReports = 1000, +} + +export enum LogoutReason { + Unknown = 0, + UserInitiated = 1, + SessionTakeover = 2, + ForciblyLoggedOut = 3, + Banned = 4, +} + +/** The error the client renders when a `GoTo` fails. */ +export enum GoToFailureError { + UnknownError = -1, + Success = 0, + NoSuchGame = 1, + PlayerNotOnline = 2, + InsufficientSpace = 3, + EventNotStarted = 4, + EventAlreadyFinished = 5, + BlockedFromRoom = 7, + JuniorNotAllowed = 11, + Banned = 12, + AlreadyInBestInstance = 13, + InsufficientRelationship = 14, + UpdateRequired = 16, + AlreadyInTargetInstance = 17, + UGCNotAllowed = 19, + NoSuchRoom = 20, + RoomIsNotActive = 22, + RoomBlockedByCreator = 23, + RoomIsPrivate = 25, + RoomInstanceIsPrivate = 26, + DeviceClassNotSupported = 30, + DeviceClassNotSupportedByRoomOwner = 31, + MovementModeNotSupportedByRoomOwner = 32, + EventIsPrivate = 35, + EventIsFull = 36, + RoomInviteExpired = 40, + NoAvailableRegion = 45, +} + +// ---- Payloads ------------------------------------------------------------------ + +/** `StorefrontBalancePurchase` (62). */ +export interface PurchaseBalanceModificationPayload { + BalanceAddType: BalanceAddType + /** + * The change, **for display only** — the client does not apply it. Its handler logs a + * line and then stores {@link Balance} outright, so a correct `Delta` with a stale + * `Balance` leaves the player's balance wrong. + */ + Delta: number + /** The post-transaction total. Absolute, and the only field that changes client state. */ + Balance: number + Platform: BalancePlatform + CurrencyType: CurrencyType +} + +/** `StorefrontBalanceUpdate` (61) — a bare set of one bucket to an absolute value. */ +export interface BalanceResponsePayload { + Balance: number + CurrencyType: CurrencyType + Platform: BalancePlatform +} + +/** One element of the `StorefrontBalanceAdd` (60) batch. */ +export interface RewardBalanceModificationPayload { + BalanceAddType: BalanceAddType + BaseAward: number + BonusAward: number + RateLimit: number + CurrentCount: number + Total: number + Platform: BalancePlatform + BalanceInGiftBox: boolean +} + +/** `ConsumableMappingAdded` (70) / `ConsumableMappingRemoved` (71). */ +export interface ConsumableMappingPayload { + Id: number + ConsumableItemDesc: string + Count: number + InitialCount: number + /** ISO-8601. */ + CreatedAt: string + ActiveDurationMinutes: number | null + IsActive: boolean + IsTransferable: boolean +} + +/** `ModerationKick` (22) and `ModerationUnkick`. Room bans go out as this with `IsBan`. */ +export interface ModerationKickPayload { + ReportCategory: KickReportCategory + /** Seconds. */ + Duration: number + GameSessionId: number + IsHostKick: boolean + Message: string + PlayerIdReporter: number | null + IsBan: boolean + IsVoiceModAutoban: boolean + IsWarning: boolean + VoteKickReason: string + /** ISO-8601. */ + TimeoutStartedAt: string | null +} + +/** `ModerationKickAttemptFailed` (23) — a vote-kick that didn't carry. */ +export interface ModerationKickFailedPayload { + ReportCategory: KickReportCategory + YesVotes: number + NoVotes: number + PlayerIdReported: number +} + +/** `ServerMaintenance` (25). */ +export interface ServerMaintenancePayload { + StartsInMinutes: number +} + +/** `Logout` (6). */ +export interface LogoutPayload { + Reason: LogoutReason +} + +/** `MessageDeleted` (3) — the id of the message to drop. */ +export interface MessageDeletedPayload { + Id: number +} + +/** `MessageReceived` (2). */ +export interface MessageReceivedPayload { + Id: number + FromPlayerId: number + /** ISO-8601. */ + SentTime: string + Type: number + Data: string + RoomId: number | null + PlayerEventId: number | null +} + +/** `RelationshipChanged` (1). */ +export interface RelationshipChangedPayload { + /** Note the spelling — capital `ID`, unlike every other id key on the wire. */ + PlayerID: number + RelationshipType: number + Muted: boolean + Ignored: boolean + Favorited: boolean +} + +/** `PlayerEventDeleted` (82) / `PlayerEventResponseDeleted` (84). */ +export interface PlayerEventIdPayload { + PlayerEventId: number +} + +/** `PlayerEventResponseChanged` (83). */ +export interface PlayerEventResponsePayload { + PlayerEvent: Record + PlayerEventResponse: Record +} + +/** `PlayerEventCreated` (80) / `PlayerEventUpdated` (81). */ +export interface PlayerEventPayload { + Tags: unknown[] + PlayerEventId: number + CreatorPlayerId: number + RoomId: number + SubRoomId: number | null + ClubId: number | null + Name: string + Description: string + ImageName: string + /** ISO-8601. */ + StartTime: string + /** ISO-8601. */ + EndTime: string + AttendeeCount: number + Accessibility: number + IsMultiInstance: boolean + SupportMultiInstanceRoomChat: boolean + DefaultBroadcastPermissions: number + CanRequestBroadcastPermissions: number + BroadcastingRoomInstanceId: number | null +} + +/** `PlayerProgressionLevelUpdate`. `XP` is progress into the level, not a lifetime total. */ +export interface PlayerProgressionLevelPayload { + PlayerId: number + Level: number + XP: number +} + +/** `ProgressionEventsRecordUpdate`. */ +export interface ProgressionEventRecordPayload { + AccountId: number + Xp: number + GameMinutesToday: number + RewardsCollected: number + BonusRewardsCollected: number + /** ISO-8601. */ + XpBoostLastPurchasedAt: string | null +} + +/** + * `SubscriptionUpdateProfile` (`"AccountUpdate"`) — the public projection of an account. + * The `Obscured*` CodeStage wrappers on the client side serialise as their plain underlying + * value, so nothing special is needed on the wire. + */ +export interface AccountUpdatePayload { + AccountId: number + UserName: string + DisplayName: string + DisplayEmoji: string + ProfileImage: string + BannerImage: string + TreatAsJunior: boolean + HasBirthday: boolean + PersonalPronouns: number + IdentityFlags: number + /** Lowercase-camel on the client's canonical spelling, unlike its neighbours. */ + createdAt: string + IsJunior: boolean | null +} + +/** + * `SubscriptionUpdateSelfProfile` (`"SelfAccountUpdate"`) — the owner-only projection: the + * six private fields **first**, then every field of {@link AccountUpdatePayload}. That order + * is not cosmetic; the client's decoder emits a derived DTO's own members before its base's. + */ +export interface SelfAccountUpdatePayload extends AccountUpdatePayload { + Email: string + Phone: string + /** ISO-8601. Its absence is what caused the under-13 junior crash. */ + Birthday: string | null + JuniorState: number + ParentAccountId: number | null + AvailableUsernameChanges: number +} + +/** `ChatMessageReceived` and `PlayerLeftChat` — same shape. */ +export interface ChatMessagePayload { + ChatMessageId: number + ChatThreadId: number + SenderPlayerId: number + /** ISO-8601. */ + TimeSent: string + Contents: string + ModerationState: number +} + +/** `ClubMembershipUpdate`. */ +export interface ClubMembershipPayload { + ClubId: number + MembershipType: number +} + +/** `CreatorClubSubscriptionUpdate`. */ +export interface CreatorClubSubscriptionPayload { + CreatorAccountId: number + ClubId: number + MembershipType: number +} + +/** `RoomCurrencyCreated` / `RoomCurrencyModified`. */ +export interface RoomCurrencyPayload { + CurrencyId: string + RoomId: number | null + Name: string + Description: string + CurrencyType: CurrencyType + Limit: number + ImageName: string + /** ISO-8601. */ + CreatedAt: string + /** ISO-8601. */ + ModifiedAt: string +} + +/** `RoomCurrencyDeleted`. */ +export interface RoomCurrencyDeletedPayload { + CurrencyId: string +} + +/** `LocalRoomKeyCreated` (120). */ +export interface LocalRoomKeyPayload { + RoomKeyId: number + ReplicationId: string + RoomId: number + Name: string + Description: string + Price: number + PurchaseCurrencyId: string | null + /** ISO-8601. */ + CreatedAt: string + ImageName: string +} + +/** `LocalRoomKeyDeleted` (121). */ +export interface LocalRoomKeyDeletedPayload { + RoomKeyId: number +} + +/** `AnnouncementUpdate`. */ +export interface AnnouncementPayload { + AnnouncementId: number + AnnouncementType: number + Title: string + Body: string + ImageName: string + LinkType: number + LinkName: string + LinkButtonLabel: string + LinkUri: string + Platform: number + /** ISO-8601. */ + CreatedAt: string +} + +/** `AnnouncementDelete`. */ +export interface AnnouncementDeletePayload { + AnnouncementId: number +} + +/** `CommunityBoardAnnouncementUpdate` (96) — the board's single current announcement. */ +export interface CommunityBoardAnnouncementPayload { + Message: string + MoreInfoUrl: string +} + +/** `ReputationUpdate`. */ +export interface ReputationPayload { + AccountId: number + IsCheerful: boolean + SelectedCheer: number | null + CheerCredit: number + CheerGeneral: number + CheerHelpful: number + CheerCreative: number + CheerGreatHost: number + CheerSportsman: number +} + +/** `PhotonAccessToken`. */ +export interface PhotonAccessTokenPayload { + RoomInstanceId: number + PhotonAccessToken: string + Permissions: unknown[] +} + +/** `KeepsakeInstanceAdded` / `KeepsakeInstanceRemoved`. */ +export interface KeepsakeInstancePayload { + KeepsakeInstanceId: string + KeepsakeCategoryConfigId: number + PlacedByAccountId: number + RoomId: number + SubRoomId: number | null +} + +/** `PlayerCustomAvatarItemModerated`. */ +export interface CustomAvatarItemPayload { + CustomAvatarItemId: string + CreatorAccountId: number + Name: string + Description: string + Price: number + Accessibility: number + IsFeatured: boolean + BaseAvatarItemId: number | null + BaseAvatarItemColor: string + DesignFilename: string + ThumbnailImageFilename: string + /** ISO-8601. */ + CreatedAt: string + /** ISO-8601. */ + ModifiedAt: string +} + +/** `GoToFailure`. */ +export interface GoToFailurePayload { + Error: GoToFailureError +} + +/** `AppVersionUpdate` — the live replacement for the dead `ModerationUpdateRequired` (21). */ +export interface AppVersionUpdatePayload { + ActivePlatforms: unknown +} + +/** `IncentivizedReferralUpdate`. */ +export interface IncentivizedReferralPayload { + InviteeAccountId: number + /** ISO-8601. */ + CreatedAt: string + /** ISO-8601. */ + VerifiedAt: string | null +} + +/** `InfluencerSupportedUpdate`. */ +export interface InfluencerSupportedPayload { + SupportedInfluencerId: number | null +} + +/** `StringAutoLocalizationJob`. */ +export interface StringAutoLocalizationJobPayload { + Scope: string + Status: number +} + +/** `SubscriptionUpdateGameSession` (`"RoomInstanceUpdate"`). */ +export interface RoomInstanceUpdatePayload { + RoomInstanceId: number + RoomId: number + SubRoomId: number + Location: string + EventId: number + ClubId: number + RoomCode: string + /** Canonical spelling is lower-camel here, unlike its neighbours. */ + photonRegionId: string + PhotonRoomId: string + Name: string + MaxCapacity: number + IsFull: boolean + IsPrivate: boolean + IsInProgress: boolean + EncryptVoiceChat: boolean + RoomInstanceType: number + MatchmakingPolicy: number +} + +/** + * `GiftPackageReceived` (30), `GiftPackageReceivedImmediate` (31) and + * `GiftPackageRewardSelectionReceived` (32) all carry this. Note `BalanceType` here is NOT + * the renamed-to-`Platform` field seen on the balance payloads — it is its own member and + * keeps its name; `Platform` is a separate key on the same object. + */ +export interface GiftPackagePayload { + Id: number | null + FromPlayerId: number | null + ConsumableItemDesc: string + AvatarItemType: number | null + AvatarItemDesc: string + EquipmentPrefabName: string + EquipmentModificationGuid: string + CurrencyType: CurrencyType + Currency: number + Xp: number + GiftContext: number + GiftRarity: number + Message: string + Platform: number + PlatformsToSpawnOn: unknown + BalanceType: BalancePlatform | null +} + +/** `gift.manualconsumed`. */ +export interface GiftManualConsumedPayload { + GiftPackageId: number +} + +/** `RewardSelectionReceived` — distinct from the gift-package frames above. */ +export interface RewardSelectionPayload { + RewardSelectionId: number + RewardType: number + Message: string + GiftContext: number + GiftDrop1: unknown + GiftDrop2: unknown + GiftDrop3: unknown + Subscriber_GiftDrop3: unknown + /** ISO-8601. */ + CreatedAt: string +} + +/** + * Channels whose handler takes no argument at all — the client refetches rather than reading + * the frame. Send `{}`; anything else is ignored. + */ +export type NoPayload = Record diff --git a/apps/notify/src/notification-types.ts b/apps/notify/src/notification-types.ts index 610cecf..b5132ab 100644 --- a/apps/notify/src/notification-types.ts +++ b/apps/notify/src/notification-types.ts @@ -13,12 +13,32 @@ * Lives in the `notify` worker (the hub owner); other workers import it to send a * typed notification instead of a magic number. No runtime dependencies, so it's safe * to import as a value from another worker's bundle. + * + * ## How this was verified + * + * Every member below was checked against the client's own subscription table, recovered by + * static analysis of `GameAssembly.dll` (see `recnet-patcher`, `il2cpp-tools/regall.py`). + * The client registers each channel by key — a decimal string for numeric ids, a name for + * the rest — and **a key with no subscriber is dropped in silence**: the frame parses, no + * handler runs, nothing is logged. So "the client ignores this" is indistinguishable from + * "the server never sent it" at runtime, which is why the status is recorded here instead. + * + * Members marked `@deadOnClient` have no subscriber in the build this was taken from + * (20230414-era, `GameAssembly.dll` 156,069,888 bytes). They are kept because they name a + * real client enum member and may come back in another build — but sending one today is a + * no-op. Everything not so marked was confirmed to have a live handler. */ export enum NotificationType { RelationshipChanged = 1, MessageReceived = 2, MessageDeleted = 3, + /** + * @deadOnClient No subscriber, and unlike its neighbours it has no wire-name twin either — + * the string `PresenceHeartbeat…` does not appear anywhere in the client's metadata. The + * heartbeat response has nowhere to land in this build. + */ PresenceHeartbeatResponse = 4, + /** Also reachable as the wire name `"PlayerPrivileges.Refresh"` — same handler. */ RefreshLogin = 5, Logout = 6, SubscriptionUpdateProfile = 'AccountUpdate', @@ -28,6 +48,9 @@ export enum NotificationType { * sends both on connect and after a profile mutation — everyone gets the public frame, * the owner additionally gets this one. Named after its twin rather than after a client * enum member, since the client's enum doesn't list it; the WIRE name is what matters. + * + * Confirmed live: the client subscribes `"SelfAccountUpdate"` and the payload is the + * public account DTO plus six owner-only fields. See `SelfAccountUpdatePayload`. */ SubscriptionUpdateSelfProfile = 'SelfAccountUpdate', SubscriptionUpdatePresence = 'PresenceUpdate', @@ -39,16 +62,29 @@ export enum NotificationType { * stringifies whatever it is given, so `15` would go out as the unrelated `"15"`. */ SubscriptionUpdateRoom = 'RoomUpdate', - /** Unverified: nothing sends it here, and the reference's hub never puts it on the wire. */ + /** @deadOnClient No subscriber under `16` and no wire-name twin. */ SubscriptionUpdateRoomPlaylist = 16, ModerationQuitGame = 20, + /** + * @deadOnClient Nothing listens on `21`. {@link AppVersionUpdate} is the live channel + * that does this job — same handler class, addressed by name. + */ ModerationUpdateRequired = 21, ModerationKick = 22, ModerationKickAttemptFailed = 23, + /** + * @deadOnClient Not present in this build at all: no subscriber on `24`, and the string + * `"ModerationRoomBan"` does not exist in the client's metadata, so neither spelling can + * be dispatched. To ban from a room, use {@link ModerationKick} with `IsBan: true`. + */ ModerationRoomBan = 'ModerationRoomBan', ServerMaintenance = 25, GiftPackageReceived = 30, GiftPackageReceivedImmediate = 31, + /** + * Carries a gift package like its two neighbours. Distinct from + * {@link RewardSelectionReceived}, which is a different channel with a different payload. + */ GiftPackageRewardSelectionReceived = 32, /** * A player's level/XP changed — `{ PlayerId, Level, XP }`, where XP is the progress into @@ -56,9 +92,13 @@ export enum NotificationType { * wire name and its enum has no number for this one at all. It pushes the frame both when * progression changes and when the player reads it back, which is how a client that just * connected gets its bar right. + * + * Confirmed live, and the three-field payload confirmed against the client's formatter. */ PlayerProgressionLevelUpdate = 'PlayerProgressionLevelUpdate', + /** @deadOnClient No subscriber under `40` and no wire-name twin. */ ProfileJuniorStatusUpdate = 40, + /** Takes no payload — the client refetches. */ RelationshipsInvalid = 50, StorefrontBalanceAdd = 60, StorefrontBalanceUpdate = 61, @@ -70,12 +110,67 @@ export enum NotificationType { PlayerEventDeleted = 82, PlayerEventResponseChanged = 83, PlayerEventResponseDeleted = 84, + /** @deadOnClient No subscriber under `85` and no wire-name twin. */ PlayerEventStateChanged = 85, ChatMessageReceived = 'ChatMessageReceived', - CommunityBoardUpdate = 95, + /** + * STRING-valued, and this one is easy to get wrong: the client's enum *does* have a + * member numbered `95`, but nothing subscribes to `"95"` — the live subscription is on + * the name. Sending the number is a silent no-op. + */ + CommunityBoardUpdate = 'CommunityBoardUpdate', + /** + * Numeric, unlike its `CommunityBoard` sibling above — `96` genuinely has a subscriber. + * Do not "unify" these two; they were checked separately. Note the *announcement list* + * has its own name-addressed channels ({@link AnnouncementUpdate} / + * {@link AnnouncementDelete}); this one is the board's single current announcement. + */ CommunityBoardAnnouncementUpdate = 96, + /** Takes no payload — the client refetches. */ InventionModerationStateChanged = 100, + /** Takes no payload — the client refetches. */ FreeGiftButtonItemsAdded = 110, LocalRoomKeyCreated = 120, LocalRoomKeyDeleted = 121, + + // ---- Name-addressed channels with no client enum member ------------------ + // These have no number at all: the client subscribes them purely by name. Grouped + // separately so the numeric block above stays a faithful mirror of the client enum. + + /** Client version gate. The live replacement for {@link ModerationUpdateRequired}. */ + AppVersionUpdate = 'AppVersionUpdate', + AnnouncementUpdate = 'AnnouncementUpdate', + AnnouncementDelete = 'AnnouncementDelete', + ClubMembershipUpdate = 'ClubMembershipUpdate', + CreatorClubSubscriptionUpdate = 'CreatorClubSubscriptionUpdate', + CommerceSubscriptionUpdate = 'CommerceSubscriptionUpdate', + /** Takes no payload — the client refetches `api/config/v2`. */ + GameConfigRefresh = 'GameConfig.Refresh', + /** Takes no payload — the client refetches. */ + PlayerSettingsRefresh = 'PlayerSettings.Refresh', + /** Matchmaking told the client its `GoTo` failed; payload is a single error code. */ + GoToFailure = 'GoToFailure', + IncentivizedReferralUpdate = 'IncentivizedReferralUpdate', + InfluencerSupportedUpdate = 'InfluencerSupportedUpdate', + KeepsakeInstanceAdded = 'KeepsakeInstanceAdded', + KeepsakeInstanceRemoved = 'KeepsakeInstanceRemoved', + /** The un-kick; same payload type as {@link ModerationKick}. */ + ModerationUnkick = 'ModerationUnkick', + /** Hands the client a Photon token for a room instance. */ + PhotonAccessToken = 'PhotonAccessToken', + PlayerCustomAvatarItemModerated = 'PlayerCustomAvatarItemModerated', + /** Same payload type as {@link ChatMessageReceived}. */ + PlayerLeftChat = 'PlayerLeftChat', + ProgressionEventsRecordUpdate = 'ProgressionEventsRecordUpdate', + ReputationUpdate = 'ReputationUpdate', + /** Distinct from {@link GiftPackageRewardSelectionReceived} — different payload. */ + RewardSelectionReceived = 'RewardSelectionReceived', + RoomCommentDeleted = 'RoomCommentDeleted', + RoomCurrencyCreated = 'RoomCurrencyCreated', + RoomCurrencyModified = 'RoomCurrencyModified', + RoomCurrencyDeleted = 'RoomCurrencyDeleted', + StringAutoLocalizationJob = 'StringAutoLocalizationJob', + WebsiteInventionPurchase = 'WebsiteInventionPurchase', + GiftManualConsumed = 'gift.manualconsumed', + PushToDevice = 'rrs.pushtodevice', }