diff --git a/packages/api/src/octane/room/IRoomEngine.ts b/packages/api/src/octane/room/IRoomEngine.ts index 750b8ff3..ea8a7298 100644 --- a/packages/api/src/octane/room/IRoomEngine.ts +++ b/packages/api/src/octane/room/IRoomEngine.ts @@ -1,4 +1,4 @@ -import { Container, Point, Rectangle, RenderTexture, Texture } from 'pixi.js'; +import { Container, Point, PointData, Rectangle, RenderTexture, Texture } from 'pixi.js'; import { IRoomGeometry, IRoomManager, IRoomObject, IRoomObjectController, IRoomRenderingCanvas } from '../../room'; import { IVector3D } from '../../utils'; import { IPetCustomPart } from '../session'; @@ -14,12 +14,15 @@ export interface IRoomEngine setActiveRoomId(roomId: number): void; createRoomInstance(roomId: number, roomMap: IRoomMapData): void; getRoomInstanceDisplay(roomId: number, id: number, width: number, height: number, scale: number): Container; - setRoomInstanceRenderingCanvasScale(roomId: number, canvasId: number, level: number, point?: Point, offsetPoint?: Point, isFlipForced?: boolean): void; + // `isFlipForced` toggles the 180 degree room flip instead of zooming; + // `isAnimated` passes a fractional scale through without snapping it. + setRoomInstanceRenderingCanvasScale(roomId: number, canvasId: number, level: number, point?: PointData, offsetPoint?: PointData, isFlipForced?: boolean, isAnimated?: boolean): void; setRoomInstanceRenderingCanvasMask(roomId: number, canvasId: number, flag: boolean): void; getRoomInstanceRenderingCanvas(roomId: number, canvasId?: number): IRoomRenderingCanvas; getRoomInstanceRenderingCanvasOffset(roomId: number, canvasId?: number): Point; setRoomInstanceRenderingCanvasOffset(roomId: number, canvasId: number, point: Point): boolean; getRoomInstanceRenderingCanvasScale(roomId?: number, canvasId?: number): number; + getRoomInstanceRenderingCanvasIsFlipped(roomId?: number, canvasId?: number): boolean; initializeRoomInstanceRenderingCanvas(roomId: number, canvasId: number, width: number, height: number): void; updateRoomInstancePlaneVisibility(roomId: number, wallVisible: boolean, floorVisible?: boolean): boolean; updateRoomInstancePlaneThickness(roomId: number, wallThickness: number, floorThickness: number): boolean; @@ -75,6 +78,7 @@ export interface IRoomEngine changeObjectModelData(roomId: number, objectId: number, category: number, numberKey: string, numberValue: number): boolean; changeObjectState(roomId: number, objectId: number, category: number): void; processRoomObjectOperation(objectId: number, category: number, operation: string): boolean; + rotateActiveObjectPreview(positive: boolean): boolean; modifyRoomObjectDataWithMap(objectId: number, category: number, operation: string, data: Map): boolean modifyRoomObjectData(objectId: number, category: number, colorHex: string, data: string): boolean processRoomObjectPlacement(placementSource: string, id: number, category: number, typeId: number, legacyString?: string, stuffData?: IObjectData, state?: number, frameNumber?: number, posture?: string): boolean; diff --git a/packages/api/src/room/renderer/IRoomRenderingCanvas.ts b/packages/api/src/room/renderer/IRoomRenderingCanvas.ts index 2617757e..380efe2c 100644 --- a/packages/api/src/room/renderer/IRoomRenderingCanvas.ts +++ b/packages/api/src/room/renderer/IRoomRenderingCanvas.ts @@ -1,4 +1,4 @@ -import { Container, Point, Texture } from 'pixi.js'; +import { Container, PointData, Texture } from 'pixi.js'; import { IRoomGeometry } from '../IRoomGeometry'; import { RoomObjectSpriteData } from '../RoomObjectSpriteData'; import { ISortableSprite } from '../object'; @@ -9,7 +9,10 @@ export interface IRoomRenderingCanvas dispose(): void; initialize(width: number, height: number): void; setMask(flag: boolean): void; - setScale(scale: number, point?: Point, offsetPoint?: Point, isFlipForced?: boolean): void; + // `point` is the screen position that must stay under the cursor across the + // transform change; `offsetPoint` is where it should land (defaults to `point`). + setScale(scale: number, point?: PointData, offsetPoint?: PointData, isFlipForced?: boolean): void; + setFlip(flag: boolean, point?: PointData, offsetPoint?: PointData): void; render(time: number, update?: boolean): void; update(): void; setMouseListener(listener: IRoomCanvasMouseListener): void; @@ -30,6 +33,7 @@ export interface IRoomRenderingCanvas screenOffsetX: number; screenOffsetY: number; scale: number; + isFlipped: boolean; width: number; height: number; canvasUpdated: boolean; diff --git a/packages/communication/src/OctaneMessages.ts b/packages/communication/src/OctaneMessages.ts index 86cf3d4e..e4a721d1 100644 --- a/packages/communication/src/OctaneMessages.ts +++ b/packages/communication/src/OctaneMessages.ts @@ -1,9 +1,13 @@ import { IMessageConfiguration } from '@octane/api'; import { UnsupportedOutgoingHeader } from './messages/outgoing/UnsupportedOutgoingHeader'; import { UserSettingsPrivacyComposer } from './messages/outgoing/user/settings/UserSettingsPrivacyComposer'; +import { UserSettingsChatPreferencesComposer } from './messages/outgoing/user/settings/UserSettingsChatPreferencesComposer'; +import { UserSettingsOnlineIndicatorComposer } from './messages/outgoing/user/settings/UserSettingsOnlineIndicatorComposer'; +import { WiredMenuSettingsComposer } from './messages/outgoing/roomevents/WiredMenuSettingsComposer'; import { RequestOfflineMessagesComposer } from './messages/outgoing/friendlist/RequestOfflineMessagesComposer'; import { HotelViewLandingEvent, HotelViewLandingRequestComposer, HotelViewLandingSaveComposer, HotelViewLandingSaveSceneComposer, HotelViewLandingVoteComposer, HotelViewLandingResetVotesComposer } from './messages'; import { RoomUnitHabbiconEvent, UseHabbiconComposer } from './messages'; +import { AddCustomFilterWordMessageComposer, CustomFilterResultEvent, GetCustomFilterMessageComposer, ModifyCustomFilterResultEvent, RemoveCustomFilterWordMessageComposer } from './messages'; import { MarkMessengerReadComposer, MessengerConversationsEvent, MessengerHistoryEvent, MessengerMessageAckEvent, MessengerMessageFailedEvent, MessengerMessageEvent, MessengerReadCursorEvent, RequestMessengerConversationsComposer, RequestMessengerHistoryComposer, SendMessengerMessageComposer } from './messages'; import { GetTraxEditorSongsComposer, TraxEditorBuySongComposer, TraxEditorDeleteSongComposer, TraxEditorErrorEvent, TraxEditorSaveSongComposer, TraxEditorSongsEvent } from './messages'; import { SnowWarGetAllTimeFriendsLeaderboardComposer, SnowWarGetAllTimeLeaderboardComposer, SnowWarGetWeeklyFriendsLeaderboardComposer, SnowWarGetWeeklyLeaderboardComposer, SnowWarSelectArenaComposer } from './messages'; @@ -24,6 +28,7 @@ import { CatalogProductMetadataComposer } from './messages/outgoing/catalog/meta import { CatalogRuntimeConfigurationEvent } from './messages/incoming/catalog/configuration'; import { CatalogRuntimeConfigurationComposer } from './messages/outgoing/catalog/configuration'; import { RareValuesEvent, RequestRareValuesComposer } from './messages'; +import { GetHotLooksComposer, HotLooksEvent } from './messages'; import { WheelBuySpinComposer, WheelDataEvent, WheelOpenComposer, WheelRecentWinsEvent, WheelResultEvent, WheelSpinComposer } from './messages'; import { WheelAdminGetPrizesComposer, WheelAdminPrizesEvent, WheelAdminSavePrizesComposer } from './messages'; import { ChestDataEvent, ChestDepositComposer, ChestDepositFurniComposer, ChestDepositInventoryItemComposer, ChestFurniChunkEvent, ChestFurniDeltaEvent, ChestOpenComposer, ChestOpenEvent, ChestStartDepositComposer, ChestWithdrawAllFurniComposer, ChestWithdrawComposer, ChestWithdrawFurniComposer, ChestLogEvent, ChestCloseComposer, ChestEnableWiredComposer, ChestSaveOptionsComposer, ChestSaveSettingsComposer, ChestSaveNotificationsComposer, ChestUpgradeCapacityComposer, ChestNotificationEvent, ChestUpgradeResultEvent, ChestRequestLogComposer, WiredChestRoomLogsComposer, WiredChestLockComposer, WiredChestTransactionDetailsComposer, WiredChestRoomLogsEvent, WiredChestLockStateEvent, WiredChestTransactionDetailsEvent, WiredTradeOpenEvent, WiredTradeItemsEvent, WiredTradeCancelledEvent, WiredTradeCompletedEvent, WiredTradeOfferItemsComposer, WiredTradeAcceptComposer, WiredTradeCancelComposer } from './messages'; @@ -31,6 +36,7 @@ import { SoundboardCatalogEvent, SoundboardCatalogReorderComposer, SoundboardCat import { PressKeybindComposer } from './messages'; import { EarningsCenterEvent, EarningsClaimResultEvent, RequestEarningsCenterComposer, ClaimEarningsRewardComposer, ClaimAllEarningsRewardsComposer } from './messages'; import { DeleteMentionComposer, MarkMentionsReadComposer, MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from './messages'; +import { ActiveDailyTasksMessageEvent, ClaimDailyTaskMessageComposer, ClaimRewardTrackPrizeMessageComposer, DailyTaskUpdatedMessageEvent, DailyTasksAddedMessageEvent, GetDailyTasksMessageComposer, GetRewardTracksMessageComposer, PurchaseRewardTrackPremiumMessageComposer, RewardTrackClaimResultMessageEvent, RewardTrackPremiumPurchaseResultMessageEvent, RewardTrackProgressMessageEvent, RewardTracksMessageEvent } from './messages'; export class OctaneMessages implements IMessageConfiguration { private _events: Map; @@ -451,6 +457,13 @@ export class OctaneMessages implements IMessageConfiguration this._events.set(IncomingHeader.COMMUNITY_GOAL_PROGRESS, CommunityGoalProgressMessageEvent); this._events.set(IncomingHeader.CONCURRENT_USERS_GOAL_PROGRESS, ConcurrentUsersGoalProgressMessageEvent); this._events.set(IncomingHeader.QUEST_DAILY, QuestDailyMessageEvent); + this._events.set(IncomingHeader.ACTIVE_DAILY_TASKS, ActiveDailyTasksMessageEvent); + this._events.set(IncomingHeader.DAILY_TASKS_ADDED, DailyTasksAddedMessageEvent); + this._events.set(IncomingHeader.DAILY_TASK_UPDATED, DailyTaskUpdatedMessageEvent); + this._events.set(IncomingHeader.REWARD_TRACKS, RewardTracksMessageEvent); + this._events.set(IncomingHeader.REWARD_TRACK_CLAIM_RESULT, RewardTrackClaimResultMessageEvent); + this._events.set(IncomingHeader.REWARD_TRACK_PROGRESS, RewardTrackProgressMessageEvent); + this._events.set(IncomingHeader.REWARD_TRACK_PREMIUM_PURCHASE_RESULT, RewardTrackPremiumPurchaseResultMessageEvent); this._events.set(IncomingHeader.QUEST_CANCELLED, QuestCancelledMessageEvent); this._events.set(IncomingHeader.QUEST_COMPLETED, QuestCompletedMessageEvent); this._events.set(IncomingHeader.COMMUNITY_GOAL_HALL_OF_FAME, CommunityGoalHallOfFameMessageEvent); @@ -545,6 +558,8 @@ export class OctaneMessages implements IMessageConfiguration this._events.set(IncomingHeader.ROOM_PAINT, RoomPaintEvent); this._events.set(IncomingHeader.ROOM_THICKNESS, RoomVisualizationSettingsEvent); this._events.set(IncomingHeader.ROOM_GET_FILTER_WORDS, RoomFilterSettingsMessageEvent); + this._events.set(IncomingHeader.CUSTOM_FILTER_RESULT, CustomFilterResultEvent); + this._events.set(IncomingHeader.MODIFY_CUSTOM_FILTER_RESULT, ModifyCustomFilterResultEvent); this._events.set(IncomingHeader.ROOM_MODEL_BLOCKED_TILES, RoomOccupiedTilesMessageEvent); this._events.set(IncomingHeader.PET_FIGURE_UPDATE, PetFigureUpdateEvent); this._events.set(IncomingHeader.PET_INFO, PetInfoEvent); @@ -598,6 +613,7 @@ export class OctaneMessages implements IMessageConfiguration // Custom features this._events.set(IncomingHeader.RARE_VALUES, RareValuesEvent); + this._events.set(IncomingHeader.HOT_LOOKS, HotLooksEvent); this._events.set(IncomingHeader.WHEEL_DATA, WheelDataEvent); this._events.set(IncomingHeader.WHEEL_RESULT, WheelResultEvent); this._events.set(IncomingHeader.WHEEL_RECENT_WINS, WheelRecentWinsEvent); @@ -917,6 +933,9 @@ export class OctaneMessages implements IMessageConfiguration this._composers.set(OutgoingHeader.SNOWWAR_GET_WEEKLY_FRIENDS_LEADERBOARD, SnowWarGetWeeklyFriendsLeaderboardComposer); this._composers.set(OutgoingHeader.USE_HABBICON, UseHabbiconComposer); this._composers.set(OutgoingHeader.USER_SETTINGS_PRIVACY, UserSettingsPrivacyComposer); + this._composers.set(OutgoingHeader.USER_SETTINGS_CHAT_PREFERENCES, UserSettingsChatPreferencesComposer); + this._composers.set(OutgoingHeader.USER_SETTINGS_ONLINE_INDICATOR, UserSettingsOnlineIndicatorComposer); + this._composers.set(OutgoingHeader.WIRED_MENU_SETTINGS_SAVE, WiredMenuSettingsComposer); this._composers.set(OutgoingHeader.REQUEST_OFFLINE_MESSAGES, RequestOfflineMessagesComposer); this._composers.set(OutgoingHeader.HOTEL_VIEW_LANDING_REQUEST, HotelViewLandingRequestComposer); this._composers.set(OutgoingHeader.HOTEL_VIEW_LANDING_SAVE, HotelViewLandingSaveComposer); @@ -1055,6 +1074,9 @@ export class OctaneMessages implements IMessageConfiguration this._composers.set(OutgoingHeader.ROOM_STAFF_PICK, ToggleStaffPickMessageComposer); this._composers.set(OutgoingHeader.ROOM_FILTER_WORDS, GetCustomRoomFilterMessageComposer); this._composers.set(OutgoingHeader.ROOM_FILTER_WORDS_MODIFY, UpdateRoomFilterMessageComposer); + this._composers.set(OutgoingHeader.GET_CUSTOM_FILTER, GetCustomFilterMessageComposer); + this._composers.set(OutgoingHeader.ADD_CUSTOM_FILTER_WORD, AddCustomFilterWordMessageComposer); + this._composers.set(OutgoingHeader.REMOVE_CUSTOM_FILTER_WORD, RemoveCustomFilterWordMessageComposer); this._composers.set(OutgoingHeader.USER_HOME_ROOM, UpdateHomeRoomMessageComposer); this._composers.set(OutgoingHeader.UPDATE_ROOM_THUMBNAIL, UpdateRoomThumbnailMessageComposer); // NEW NAVIGATOR @@ -1392,6 +1414,11 @@ export class OctaneMessages implements IMessageConfiguration this._composers.set(OutgoingHeader.GET_CONCURRENT_USERS_GOAL_PROGRESS, GetConcurrentUsersGoalProgressMessageComposer); this._composers.set(OutgoingHeader.GET_CONCURRENT_USERS_REWARD, GetConcurrentUsersRewardMessageComposer); this._composers.set(OutgoingHeader.GET_DAILY_QUEST, GetDailyQuestMessageComposer); + this._composers.set(OutgoingHeader.GET_DAILY_TASKS, GetDailyTasksMessageComposer); + this._composers.set(OutgoingHeader.CLAIM_DAILY_TASK, ClaimDailyTaskMessageComposer); + this._composers.set(OutgoingHeader.GET_REWARD_TRACKS, GetRewardTracksMessageComposer); + this._composers.set(OutgoingHeader.CLAIM_REWARD_TRACK_PRIZE, ClaimRewardTrackPrizeMessageComposer); + this._composers.set(OutgoingHeader.PURCHASE_REWARD_TRACK_PREMIUM, PurchaseRewardTrackPremiumMessageComposer); this._composers.set(OutgoingHeader.GET_QUESTS, GetQuestsMessageComposer); this._composers.set(OutgoingHeader.GET_SEASONAL_QUESTS_ONLY, GetSeasonalQuestsOnlyMessageComposer); this._composers.set(OutgoingHeader.OPEN_QUEST_TRACKER, OpenQuestTrackerMessageComposer); @@ -1488,6 +1515,7 @@ export class OctaneMessages implements IMessageConfiguration // Custom features this._composers.set(OutgoingHeader.REQUEST_RARE_VALUES, RequestRareValuesComposer); + this._composers.set(OutgoingHeader.GET_HOT_LOOKS, GetHotLooksComposer); this._composers.set(OutgoingHeader.WHEEL_OPEN, WheelOpenComposer); this._composers.set(OutgoingHeader.WHEEL_SPIN, WheelSpinComposer); this._composers.set(OutgoingHeader.WHEEL_BUY_SPIN, WheelBuySpinComposer); diff --git a/packages/communication/src/messages/incoming/IncomingHeader.ts b/packages/communication/src/messages/incoming/IncomingHeader.ts index 6db27a19..4bf5cb42 100644 --- a/packages/communication/src/messages/incoming/IncomingHeader.ts +++ b/packages/communication/src/messages/incoming/IncomingHeader.ts @@ -211,6 +211,8 @@ export class IncomingHeader public static ROOM_SPECTATOR = 1033; public static ROOM_THICKNESS = 3547; public static ROOM_GET_FILTER_WORDS = 2937; + public static CUSTOM_FILTER_RESULT = 3883; + public static MODIFY_CUSTOM_FILTER_RESULT = 3333; public static ROOM_MESSAGE_NOTIFICATION = 1634; public static ROOM_POPULAR_TAGS_RESULT = 2012; public static INFO_FEED_ENABLE = 3284; @@ -364,6 +366,14 @@ export class IncomingHeader public static SEASONAL_QUESTS = 1122; public static QUESTS = 3625; public static QUEST = 230; + // AIR 13 daily tasks and reward track (server -> client); 9450-9452 replace the colliding official 2392/596/2142 + public static ACTIVE_DAILY_TASKS = 2900; + public static DAILY_TASKS_ADDED = 670; + public static DAILY_TASK_UPDATED = 9450; + public static REWARD_TRACKS = 2327; + public static REWARD_TRACK_CLAIM_RESULT = 9451; + public static REWARD_TRACK_PROGRESS = 9452; + public static REWARD_TRACK_PREMIUM_PURCHASE_RESULT = 2248; public static BONUS_RARE_INFO = 1533; public static CRAFTABLE_PRODUCTS = 1000; public static CRAFTING_RECIPE = 2774; @@ -600,4 +610,6 @@ export class IncomingHeader // Room unit habbicon broadcast (server -> client). 9410 to match emulator // RoomUserHabbiconComposer; 6011 is reserved by SnowWar SAVE_EDITOR. public static UNIT_HABBICON = 9410; + // AIR 13 avatar editor hot looks tab (server -> client). 9360 to match emulator HotLooksComposer. + public static HOT_LOOKS = 9360; } diff --git a/packages/communication/src/messages/incoming/hotlooks/HotLooksEvent.ts b/packages/communication/src/messages/incoming/hotlooks/HotLooksEvent.ts new file mode 100644 index 00000000..75ab7c20 --- /dev/null +++ b/packages/communication/src/messages/incoming/hotlooks/HotLooksEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { HotLooksParser } from '../../parser'; + +export class HotLooksEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, HotLooksParser); + } + + public getParser(): HotLooksParser + { + return this.parser as HotLooksParser; + } +} diff --git a/packages/communication/src/messages/incoming/hotlooks/index.ts b/packages/communication/src/messages/incoming/hotlooks/index.ts new file mode 100644 index 00000000..0352b685 --- /dev/null +++ b/packages/communication/src/messages/incoming/hotlooks/index.ts @@ -0,0 +1 @@ +export * from './HotLooksEvent'; diff --git a/packages/communication/src/messages/incoming/index.ts b/packages/communication/src/messages/incoming/index.ts index 2b7d6854..021174a5 100644 --- a/packages/communication/src/messages/incoming/index.ts +++ b/packages/communication/src/messages/incoming/index.ts @@ -29,6 +29,7 @@ export * from './handshake'; export * from './help'; export * from './housekeeping'; export * from './rarevalues'; +export * from './hotlooks'; export * from './soundboard'; export * from './traxeditor'; export * from './wheel'; diff --git a/packages/communication/src/messages/incoming/quest/ActiveDailyTasksMessageEvent.ts b/packages/communication/src/messages/incoming/quest/ActiveDailyTasksMessageEvent.ts new file mode 100644 index 00000000..7d7cc4d4 --- /dev/null +++ b/packages/communication/src/messages/incoming/quest/ActiveDailyTasksMessageEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { ActiveDailyTasksMessageParser } from '../../parser'; + +export class ActiveDailyTasksMessageEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, ActiveDailyTasksMessageParser); + } + + public getParser(): ActiveDailyTasksMessageParser + { + return this.parser as ActiveDailyTasksMessageParser; + } +} diff --git a/packages/communication/src/messages/incoming/quest/DailyTaskUpdatedMessageEvent.ts b/packages/communication/src/messages/incoming/quest/DailyTaskUpdatedMessageEvent.ts new file mode 100644 index 00000000..36b9af99 --- /dev/null +++ b/packages/communication/src/messages/incoming/quest/DailyTaskUpdatedMessageEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { DailyTaskUpdatedMessageParser } from '../../parser'; + +export class DailyTaskUpdatedMessageEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, DailyTaskUpdatedMessageParser); + } + + public getParser(): DailyTaskUpdatedMessageParser + { + return this.parser as DailyTaskUpdatedMessageParser; + } +} diff --git a/packages/communication/src/messages/incoming/quest/DailyTasksAddedMessageEvent.ts b/packages/communication/src/messages/incoming/quest/DailyTasksAddedMessageEvent.ts new file mode 100644 index 00000000..2cc712fb --- /dev/null +++ b/packages/communication/src/messages/incoming/quest/DailyTasksAddedMessageEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { ActiveDailyTasksMessageParser } from '../../parser'; + +export class DailyTasksAddedMessageEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, ActiveDailyTasksMessageParser); + } + + public getParser(): ActiveDailyTasksMessageParser + { + return this.parser as ActiveDailyTasksMessageParser; + } +} diff --git a/packages/communication/src/messages/incoming/quest/RewardTrackClaimResultMessageEvent.ts b/packages/communication/src/messages/incoming/quest/RewardTrackClaimResultMessageEvent.ts new file mode 100644 index 00000000..3a23edb2 --- /dev/null +++ b/packages/communication/src/messages/incoming/quest/RewardTrackClaimResultMessageEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { RewardTrackClaimResultMessageParser } from '../../parser'; + +export class RewardTrackClaimResultMessageEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, RewardTrackClaimResultMessageParser); + } + + public getParser(): RewardTrackClaimResultMessageParser + { + return this.parser as RewardTrackClaimResultMessageParser; + } +} diff --git a/packages/communication/src/messages/incoming/quest/RewardTrackPremiumPurchaseResultMessageEvent.ts b/packages/communication/src/messages/incoming/quest/RewardTrackPremiumPurchaseResultMessageEvent.ts new file mode 100644 index 00000000..af3b62c0 --- /dev/null +++ b/packages/communication/src/messages/incoming/quest/RewardTrackPremiumPurchaseResultMessageEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { RewardTrackPremiumPurchaseResultMessageParser } from '../../parser'; + +export class RewardTrackPremiumPurchaseResultMessageEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, RewardTrackPremiumPurchaseResultMessageParser); + } + + public getParser(): RewardTrackPremiumPurchaseResultMessageParser + { + return this.parser as RewardTrackPremiumPurchaseResultMessageParser; + } +} diff --git a/packages/communication/src/messages/incoming/quest/RewardTrackProgressMessageEvent.ts b/packages/communication/src/messages/incoming/quest/RewardTrackProgressMessageEvent.ts new file mode 100644 index 00000000..32683ba1 --- /dev/null +++ b/packages/communication/src/messages/incoming/quest/RewardTrackProgressMessageEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { RewardTrackProgressMessageParser } from '../../parser'; + +export class RewardTrackProgressMessageEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, RewardTrackProgressMessageParser); + } + + public getParser(): RewardTrackProgressMessageParser + { + return this.parser as RewardTrackProgressMessageParser; + } +} diff --git a/packages/communication/src/messages/incoming/quest/RewardTracksMessageEvent.ts b/packages/communication/src/messages/incoming/quest/RewardTracksMessageEvent.ts new file mode 100644 index 00000000..f8ea3d6c --- /dev/null +++ b/packages/communication/src/messages/incoming/quest/RewardTracksMessageEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { RewardTracksMessageParser } from '../../parser'; + +export class RewardTracksMessageEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, RewardTracksMessageParser); + } + + public getParser(): RewardTracksMessageParser + { + return this.parser as RewardTracksMessageParser; + } +} diff --git a/packages/communication/src/messages/incoming/quest/index.ts b/packages/communication/src/messages/incoming/quest/index.ts index f2b87077..b6c1c6b5 100644 --- a/packages/communication/src/messages/incoming/quest/index.ts +++ b/packages/communication/src/messages/incoming/quest/index.ts @@ -9,3 +9,10 @@ export * from './QuestDailyMessageEvent'; export * from './QuestMessageEvent'; export * from './QuestsMessageEvent'; export * from './SeasonalQuestsMessageEvent'; +export * from './ActiveDailyTasksMessageEvent'; +export * from './DailyTasksAddedMessageEvent'; +export * from './DailyTaskUpdatedMessageEvent'; +export * from './RewardTracksMessageEvent'; +export * from './RewardTrackClaimResultMessageEvent'; +export * from './RewardTrackProgressMessageEvent'; +export * from './RewardTrackPremiumPurchaseResultMessageEvent'; diff --git a/packages/communication/src/messages/incoming/user/index.ts b/packages/communication/src/messages/incoming/user/index.ts index f6f86421..44c63f6c 100644 --- a/packages/communication/src/messages/incoming/user/index.ts +++ b/packages/communication/src/messages/incoming/user/index.ts @@ -25,3 +25,4 @@ export * from './RespectReceivedEvent'; export * from './ScrSendKickbackInfoMessageEvent'; export * from './wardrobe'; export * from './WelcomeGiftChangeEmailResultEvent'; +export * from './wordfilter'; diff --git a/packages/communication/src/messages/incoming/user/wordfilter/CustomFilterResultEvent.ts b/packages/communication/src/messages/incoming/user/wordfilter/CustomFilterResultEvent.ts new file mode 100644 index 00000000..50ffc181 --- /dev/null +++ b/packages/communication/src/messages/incoming/user/wordfilter/CustomFilterResultEvent.ts @@ -0,0 +1,16 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { CustomFilterResultParser } from '../../../parser'; + +export class CustomFilterResultEvent extends MessageEvent implements IMessageEvent +{ + constructor(callBack: Function) + { + super(callBack, CustomFilterResultParser); + } + + public getParser(): CustomFilterResultParser + { + return this.parser as CustomFilterResultParser; + } +} diff --git a/packages/communication/src/messages/incoming/user/wordfilter/ModifyCustomFilterResultEvent.ts b/packages/communication/src/messages/incoming/user/wordfilter/ModifyCustomFilterResultEvent.ts new file mode 100644 index 00000000..aaf5695f --- /dev/null +++ b/packages/communication/src/messages/incoming/user/wordfilter/ModifyCustomFilterResultEvent.ts @@ -0,0 +1,23 @@ +import { IMessageEvent } from '@octane/api'; +import { MessageEvent } from '@octane/events'; +import { ModifyCustomFilterResultParser } from '../../../parser'; + +export class ModifyCustomFilterResultEvent extends MessageEvent implements IMessageEvent +{ + /** The word could not be added or removed (already listed, empty, ...). */ + public static FAILED: number = 0; + /** The word was added to the list. */ + public static ADDED: number = 1; + /** The word was removed from the list. */ + public static REMOVED: number = 3; + + constructor(callBack: Function) + { + super(callBack, ModifyCustomFilterResultParser); + } + + public getParser(): ModifyCustomFilterResultParser + { + return this.parser as ModifyCustomFilterResultParser; + } +} diff --git a/packages/communication/src/messages/incoming/user/wordfilter/index.ts b/packages/communication/src/messages/incoming/user/wordfilter/index.ts new file mode 100644 index 00000000..aa38ff28 --- /dev/null +++ b/packages/communication/src/messages/incoming/user/wordfilter/index.ts @@ -0,0 +1,2 @@ +export * from './CustomFilterResultEvent'; +export * from './ModifyCustomFilterResultEvent'; diff --git a/packages/communication/src/messages/outgoing/OutgoingHeader.ts b/packages/communication/src/messages/outgoing/OutgoingHeader.ts index cb2f9950..5a447ae7 100644 --- a/packages/communication/src/messages/outgoing/OutgoingHeader.ts +++ b/packages/communication/src/messages/outgoing/OutgoingHeader.ts @@ -243,6 +243,9 @@ export class OutgoingHeader public static ROOM_STAFF_PICK = 1918; public static ROOM_FILTER_WORDS = 1911; public static ROOM_FILTER_WORDS_MODIFY = 3001; + public static GET_CUSTOM_FILTER = 145; + public static ADD_CUSTOM_FILTER_WORD = 68; + public static REMOVE_CUSTOM_FILTER_WORD = 1996; public static MYSTERYBOXWAITINGCANCELEDMESSAGE = 2012; public static MYSTERYBOX_OPEN_TROPHY = 3074; public static SECURITY_MACHINE = 2490; @@ -294,6 +297,11 @@ export class OutgoingHeader public static USER_SETTINGS_INVITES = 1086; public static USER_SETTINGS_OLD_CHAT = 1262; public static USER_SETTINGS_VOLUME = 1367; + // Official AIR 13 ids (client -> server): chat mode/bubble width/scroll speed, friend-online + // notification preference and the wired menu preferences (wired whisper switch). + public static USER_SETTINGS_CHAT_PREFERENCES = 2506; + public static USER_SETTINGS_ONLINE_INDICATOR = 818; + public static WIRED_MENU_SETTINGS_SAVE = 1226; public static USER_SUBSCRIPTION = 3166; public static GET_WARDROBE = 2742; public static SAVE_WARDROBE_OUTFIT = 800; @@ -392,6 +400,12 @@ export class OutgoingHeader public static REDEEM_COMMUNITY_GOAL_PRIZE = 90; public static REJECT_QUEST = 2397; public static START_CAMPAIGN = 1697; + // AIR 13 daily tasks and reward track (client -> server); GET_REWARD_TRACKS is custom, the official client is pushed + public static GET_DAILY_TASKS = 4100; + public static CLAIM_DAILY_TASK = 4101; + public static GET_REWARD_TRACKS = 9450; + public static CLAIM_REWARD_TRACK_PRIZE = 1111; + public static PURCHASE_REWARD_TRACK_PREMIUM = 3022; public static GET_BONUS_RARE_INFO = 957; public static CRAFT = 3591; public static CRAFT_SECRET = 1251; @@ -667,4 +681,6 @@ export class OutgoingHeader // Use habbicon (client -> server). 9417 to match emulator RoomUserHabbiconEvent; // 6010 is reserved by SnowWar EDIT_ROOM. public static USE_HABBICON = 9417; + // AIR 13 avatar editor hot looks tab (client -> server). 9360 to match emulator GetHotLooksEvent. + public static GET_HOT_LOOKS = 9360; } diff --git a/packages/communication/src/messages/outgoing/group/GroupRemoveMemberComposer.ts b/packages/communication/src/messages/outgoing/group/GroupRemoveMemberComposer.ts index 001423c8..492719a7 100644 --- a/packages/communication/src/messages/outgoing/group/GroupRemoveMemberComposer.ts +++ b/packages/communication/src/messages/outgoing/group/GroupRemoveMemberComposer.ts @@ -4,9 +4,10 @@ export class GroupRemoveMemberComposer implements IMessageComposer; - constructor(groupId: number, memberId: number) + // AIR 13 KickMember: `block` true kicks and blocks the member in one message. + constructor(groupId: number, memberId: number, block: boolean = false) { - this._data = [groupId, memberId]; + this._data = [groupId, memberId, block]; } public getMessageArray() diff --git a/packages/communication/src/messages/outgoing/hotlooks/GetHotLooksComposer.ts b/packages/communication/src/messages/outgoing/hotlooks/GetHotLooksComposer.ts new file mode 100644 index 00000000..d6e45fd3 --- /dev/null +++ b/packages/communication/src/messages/outgoing/hotlooks/GetHotLooksComposer.ts @@ -0,0 +1,25 @@ +import { IMessageComposer } from '@octane/api'; + +/** + * AIR 13 GetHotLooks: the avatar editor asks for up to `count` ready-made looks of the user's gender + * (the official client always sends 20). + */ +export class GetHotLooksComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor(count: number = 20) + { + this._data = [count]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/hotlooks/index.ts b/packages/communication/src/messages/outgoing/hotlooks/index.ts new file mode 100644 index 00000000..0c87da8b --- /dev/null +++ b/packages/communication/src/messages/outgoing/hotlooks/index.ts @@ -0,0 +1 @@ +export * from './GetHotLooksComposer'; diff --git a/packages/communication/src/messages/outgoing/index.ts b/packages/communication/src/messages/outgoing/index.ts index 66e70a11..addbd54b 100644 --- a/packages/communication/src/messages/outgoing/index.ts +++ b/packages/communication/src/messages/outgoing/index.ts @@ -25,6 +25,7 @@ export * from './handshake'; export * from './help'; export * from './housekeeping'; export * from './rarevalues'; +export * from './hotlooks'; export * from './soundboard'; export * from './traxeditor'; export * from './wheel'; diff --git a/packages/communication/src/messages/outgoing/quest/ClaimDailyTaskMessageComposer.ts b/packages/communication/src/messages/outgoing/quest/ClaimDailyTaskMessageComposer.ts new file mode 100644 index 00000000..24724224 --- /dev/null +++ b/packages/communication/src/messages/outgoing/quest/ClaimDailyTaskMessageComposer.ts @@ -0,0 +1,21 @@ +import { IMessageComposer } from '@octane/api'; + +export class ClaimDailyTaskMessageComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor(taskId: number) + { + this._data = [taskId]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/quest/ClaimRewardTrackPrizeMessageComposer.ts b/packages/communication/src/messages/outgoing/quest/ClaimRewardTrackPrizeMessageComposer.ts new file mode 100644 index 00000000..ef32153a --- /dev/null +++ b/packages/communication/src/messages/outgoing/quest/ClaimRewardTrackPrizeMessageComposer.ts @@ -0,0 +1,21 @@ +import { IMessageComposer } from '@octane/api'; + +export class ClaimRewardTrackPrizeMessageComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor(trackId: string, rewardId: string) + { + this._data = [trackId, rewardId]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/quest/GetDailyTasksMessageComposer.ts b/packages/communication/src/messages/outgoing/quest/GetDailyTasksMessageComposer.ts new file mode 100644 index 00000000..77a9cd01 --- /dev/null +++ b/packages/communication/src/messages/outgoing/quest/GetDailyTasksMessageComposer.ts @@ -0,0 +1,21 @@ +import { IMessageComposer } from '@octane/api'; + +export class GetDailyTasksMessageComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor() + { + this._data = []; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/quest/GetRewardTracksMessageComposer.ts b/packages/communication/src/messages/outgoing/quest/GetRewardTracksMessageComposer.ts new file mode 100644 index 00000000..c10610e8 --- /dev/null +++ b/packages/communication/src/messages/outgoing/quest/GetRewardTracksMessageComposer.ts @@ -0,0 +1,21 @@ +import { IMessageComposer } from '@octane/api'; + +export class GetRewardTracksMessageComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor() + { + this._data = []; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/quest/PurchaseRewardTrackPremiumMessageComposer.ts b/packages/communication/src/messages/outgoing/quest/PurchaseRewardTrackPremiumMessageComposer.ts new file mode 100644 index 00000000..ea2125a9 --- /dev/null +++ b/packages/communication/src/messages/outgoing/quest/PurchaseRewardTrackPremiumMessageComposer.ts @@ -0,0 +1,21 @@ +import { IMessageComposer } from '@octane/api'; + +export class PurchaseRewardTrackPremiumMessageComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor(trackId: string) + { + this._data = [trackId]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/quest/RejectQuestMessageComposer.ts b/packages/communication/src/messages/outgoing/quest/RejectQuestMessageComposer.ts index a498991f..e6cef6d4 100644 --- a/packages/communication/src/messages/outgoing/quest/RejectQuestMessageComposer.ts +++ b/packages/communication/src/messages/outgoing/quest/RejectQuestMessageComposer.ts @@ -4,9 +4,9 @@ export class RejectQuestMessageComposer implements IMessageComposer; - constructor() + constructor(questId: number) { - this._data = []; + this._data = [questId]; } public getMessageArray() diff --git a/packages/communication/src/messages/outgoing/quest/index.ts b/packages/communication/src/messages/outgoing/quest/index.ts index e9f5c992..6b0ecd87 100644 --- a/packages/communication/src/messages/outgoing/quest/index.ts +++ b/packages/communication/src/messages/outgoing/quest/index.ts @@ -14,3 +14,8 @@ export * from './OpenQuestTrackerMessageComposer'; export * from './RedeemCommunityGoalPrizeMessageComposer'; export * from './RejectQuestMessageComposer'; export * from './StartCampaignMessageComposer'; +export * from './GetDailyTasksMessageComposer'; +export * from './ClaimDailyTaskMessageComposer'; +export * from './GetRewardTracksMessageComposer'; +export * from './ClaimRewardTrackPrizeMessageComposer'; +export * from './PurchaseRewardTrackPremiumMessageComposer'; diff --git a/packages/communication/src/messages/outgoing/roomevents/WiredMenuSettingsComposer.ts b/packages/communication/src/messages/outgoing/roomevents/WiredMenuSettingsComposer.ts new file mode 100644 index 00000000..ddaa5e93 --- /dev/null +++ b/packages/communication/src/messages/outgoing/roomevents/WiredMenuSettingsComposer.ts @@ -0,0 +1,26 @@ +import { IMessageComposer } from '@octane/api'; + +/** + * Official AIR 13 wired menu preferences composer (header 1226, WiredMenuController.sendPreferences): + * menu button, inspect button, play-test mode, a reserved int (always 0), wired whisper disabled, + * show all notifications and the ui style name. + */ +export class WiredMenuSettingsComposer implements IMessageComposer<[boolean, boolean, boolean, number, boolean, boolean, string]> +{ + private _data: [boolean, boolean, boolean, number, boolean, boolean, string]; + + constructor(menuButton: boolean, inspectButton: boolean, playTestMode: boolean, wiredWhisperDisabled: boolean, showAllNotifications: boolean, uiStyle: string) + { + this._data = [ menuButton, inspectButton, playTestMode, 0, wiredWhisperDisabled, showAllNotifications, uiStyle ]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/roomevents/index.ts b/packages/communication/src/messages/outgoing/roomevents/index.ts index 72bc2ba6..e6678569 100644 --- a/packages/communication/src/messages/outgoing/roomevents/index.ts +++ b/packages/communication/src/messages/outgoing/roomevents/index.ts @@ -13,3 +13,4 @@ export * from './WiredUserInspectMoveComposer'; export * from './WiredUserVariableManageComposer'; export * from './WiredUserVariablesRequestComposer'; export * from './WiredUserVariableUpdateComposer'; +export * from './WiredMenuSettingsComposer'; diff --git a/packages/communication/src/messages/outgoing/user/index.ts b/packages/communication/src/messages/outgoing/user/index.ts index 9a10b1fe..dd76e389 100644 --- a/packages/communication/src/messages/outgoing/user/index.ts +++ b/packages/communication/src/messages/outgoing/user/index.ts @@ -12,3 +12,4 @@ export * from './settings'; export * from './UnblockGroupMemberMessageComposer'; export * from './UserRespectComposer'; export * from './WelcomeGiftChangeEmailComposer'; +export * from './wordfilter'; diff --git a/packages/communication/src/messages/outgoing/user/settings/UserSettingsChatPreferencesComposer.ts b/packages/communication/src/messages/outgoing/user/settings/UserSettingsChatPreferencesComposer.ts new file mode 100644 index 00000000..cce273f5 --- /dev/null +++ b/packages/communication/src/messages/outgoing/user/settings/UserSettingsChatPreferencesComposer.ts @@ -0,0 +1,26 @@ +import { IMessageComposer } from '@octane/api'; + +/** + * Official AIR 13 chat preferences composer (header 2506): a leading boolean the official client + * always sends as false, then chat mode (0 free flow / 1 line by line), bubble width + * (0 wide / 1 normal / 2 thin) and scroll speed (0 fast / 1 normal / 2 slow). + */ +export class UserSettingsChatPreferencesComposer implements IMessageComposer<[boolean, number, number, number]> +{ + private _data: [boolean, number, number, number]; + + constructor(chatMode: number, chatBubbleWidth: number, chatScrollSpeed: number) + { + this._data = [ false, chatMode, chatBubbleWidth, chatScrollSpeed ]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/user/settings/UserSettingsOnlineIndicatorComposer.ts b/packages/communication/src/messages/outgoing/user/settings/UserSettingsOnlineIndicatorComposer.ts new file mode 100644 index 00000000..36cbfccc --- /dev/null +++ b/packages/communication/src/messages/outgoing/user/settings/UserSettingsOnlineIndicatorComposer.ts @@ -0,0 +1,25 @@ +import { IMessageComposer } from '@octane/api'; + +/** + * Official AIR 13 friend-online notification preference composer (header 818): + * 0 everyone, 1 users in my relationship status, 2 nobody. + */ +export class UserSettingsOnlineIndicatorComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor(preference: number) + { + this._data = [ preference ]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/user/settings/index.ts b/packages/communication/src/messages/outgoing/user/settings/index.ts index 312e9d48..361904e5 100644 --- a/packages/communication/src/messages/outgoing/user/settings/index.ts +++ b/packages/communication/src/messages/outgoing/user/settings/index.ts @@ -3,3 +3,5 @@ export * from './UserSettingsOldChatComposer'; export * from './UserSettingsPrivacyComposer'; export * from './UserSettingsRoomInvitesComposer'; export * from './UserSettingsSoundComposer'; +export * from './UserSettingsChatPreferencesComposer'; +export * from './UserSettingsOnlineIndicatorComposer'; diff --git a/packages/communication/src/messages/outgoing/user/wordfilter/AddCustomFilterWordMessageComposer.ts b/packages/communication/src/messages/outgoing/user/wordfilter/AddCustomFilterWordMessageComposer.ts new file mode 100644 index 00000000..7cd592a8 --- /dev/null +++ b/packages/communication/src/messages/outgoing/user/wordfilter/AddCustomFilterWordMessageComposer.ts @@ -0,0 +1,22 @@ +import { IMessageComposer } from '@octane/api'; + +/** AddCustomFilterWord (68). */ +export class AddCustomFilterWordMessageComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor(word: string) + { + this._data = [word]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/user/wordfilter/GetCustomFilterMessageComposer.ts b/packages/communication/src/messages/outgoing/user/wordfilter/GetCustomFilterMessageComposer.ts new file mode 100644 index 00000000..b2244755 --- /dev/null +++ b/packages/communication/src/messages/outgoing/user/wordfilter/GetCustomFilterMessageComposer.ts @@ -0,0 +1,22 @@ +import { IMessageComposer } from '@octane/api'; + +/** GetCustomFilter (145): asks for the personal word filter list. */ +export class GetCustomFilterMessageComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor() + { + this._data = []; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/user/wordfilter/RemoveCustomFilterWordMessageComposer.ts b/packages/communication/src/messages/outgoing/user/wordfilter/RemoveCustomFilterWordMessageComposer.ts new file mode 100644 index 00000000..447e6ae4 --- /dev/null +++ b/packages/communication/src/messages/outgoing/user/wordfilter/RemoveCustomFilterWordMessageComposer.ts @@ -0,0 +1,22 @@ +import { IMessageComposer } from '@octane/api'; + +/** RemoveCustomFilterWord (1996). */ +export class RemoveCustomFilterWordMessageComposer implements IMessageComposer> +{ + private _data: ConstructorParameters; + + constructor(word: string) + { + this._data = [word]; + } + + public getMessageArray() + { + return this._data; + } + + public dispose(): void + { + return; + } +} diff --git a/packages/communication/src/messages/outgoing/user/wordfilter/index.ts b/packages/communication/src/messages/outgoing/user/wordfilter/index.ts new file mode 100644 index 00000000..6b383fe0 --- /dev/null +++ b/packages/communication/src/messages/outgoing/user/wordfilter/index.ts @@ -0,0 +1,3 @@ +export * from './AddCustomFilterWordMessageComposer'; +export * from './GetCustomFilterMessageComposer'; +export * from './RemoveCustomFilterWordMessageComposer'; diff --git a/packages/communication/src/messages/parser/game/snowwar/SnowWarGameResultPlayerData.ts b/packages/communication/src/messages/parser/game/snowwar/SnowWarGameResultPlayerData.ts index c167842a..55e9c114 100644 --- a/packages/communication/src/messages/parser/game/snowwar/SnowWarGameResultPlayerData.ts +++ b/packages/communication/src/messages/parser/game/snowwar/SnowWarGameResultPlayerData.ts @@ -1,10 +1,22 @@ import { IMessageDataWrapper } from '@octane/api'; +/** + * One player row of the results screen. The base record is (userId, name, + * score); the official Game2TeamPlayerData extras (figure, gender and the + * Game2PlayerStatsData hits / kills plus the lobby skill level) arrive in the + * optional stats tail of SnowWarOnGameEndingParser and are merged in by + * applyStats, so they stay undefined against older servers. + */ export class SnowWarGameResultPlayerData { private _userId: number; private _name: string; private _score: number; + private _figure: string = undefined; + private _gender: string = undefined; + private _snowballHits: number = undefined; + private _kills: number = undefined; + private _skillLevel: number = undefined; constructor(wrapper: IMessageDataWrapper) { @@ -13,6 +25,15 @@ export class SnowWarGameResultPlayerData this._score = wrapper.readInt(); } + public applyStats(figure: string, gender: string, snowballHits: number, kills: number, skillLevel: number): void + { + this._figure = figure; + this._gender = gender; + this._snowballHits = snowballHits; + this._kills = kills; + this._skillLevel = skillLevel; + } + public get userId(): number { return this._userId; @@ -27,4 +48,30 @@ export class SnowWarGameResultPlayerData { return this._score; } + + public get figure(): string + { + return this._figure; + } + + public get gender(): string + { + return this._gender; + } + + public get snowballHits(): number + { + return this._snowballHits; + } + + public get kills(): number + { + return this._kills; + } + + /** AIR GameLobbyPlayerData.skillLevel (1-30). */ + public get skillLevel(): number + { + return this._skillLevel; + } } diff --git a/packages/communication/src/messages/parser/game/snowwar/SnowWarLobbyTeamsParser.ts b/packages/communication/src/messages/parser/game/snowwar/SnowWarLobbyTeamsParser.ts index e7a3167c..3ef2f296 100644 --- a/packages/communication/src/messages/parser/game/snowwar/SnowWarLobbyTeamsParser.ts +++ b/packages/communication/src/messages/parser/game/snowwar/SnowWarLobbyTeamsParser.ts @@ -13,7 +13,8 @@ export interface ISnowWarArenaData * can show the waiting players split into their teams (Red / Blue) before the * arena splash. Player records use the SnowWarPlayerData wire shape (objectId * is 0 here - the real match, and its object ids, only exists once the - * countdown ends). + * countdown ends). The AIR GameLobbyPlayerData skill level follows the arena + * list as an optional tail (count, [userId, skillLevel]) merged into players. */ export class SnowWarLobbyTeamsParser implements IMessageParser { @@ -67,6 +68,23 @@ export class SnowWarLobbyTeamsParser implements IMessageParser totalArenas--; } + if(!wrapper.bytesAvailable) return true; + + let totalSkillLevels = wrapper.readInt(); + + while(totalSkillLevels > 0) + { + const userId = wrapper.readInt(); + const skillLevel = wrapper.readInt(); + + for(const player of this._players) + { + if(player.userId === userId) player.skillLevel = skillLevel; + } + + totalSkillLevels--; + } + return true; } diff --git a/packages/communication/src/messages/parser/game/snowwar/SnowWarOnGameEndingParser.ts b/packages/communication/src/messages/parser/game/snowwar/SnowWarOnGameEndingParser.ts index 01455eed..6c580a15 100644 --- a/packages/communication/src/messages/parser/game/snowwar/SnowWarOnGameEndingParser.ts +++ b/packages/communication/src/messages/parser/game/snowwar/SnowWarOnGameEndingParser.ts @@ -1,15 +1,28 @@ import { IMessageDataWrapper, IMessageParser } from '@octane/api'; import { SnowWarGameResultTeamData } from './SnowWarGameResultTeamData'; +/** + * Final results (5022): seconds until the results screen closes and the + * teams with their players. The official Game2GameEnding extras follow as an + * optional tail so older servers still parse: playerWithMostHits, + * playerWithMostKills and per player (userId, figure, gender, snowballHits, + * kills, skillLevel) merged into the matching team player rows. + */ export class SnowWarOnGameEndingParser implements IMessageParser { private _secondsToResults: number; private _teams: SnowWarGameResultTeamData[]; + private _playerWithMostHits: number; + private _playerWithMostKills: number; + private _hasPlayerStats: boolean; public flush(): boolean { this._secondsToResults = 0; this._teams = []; + this._playerWithMostHits = 0; + this._playerWithMostKills = 0; + this._hasPlayerStats = false; return true; } @@ -29,6 +42,35 @@ export class SnowWarOnGameEndingParser implements IMessageParser totalTeams--; } + // Optional Game2GameEnding stats tail (AIR Game2SnowWarGameStats + Game2PlayerStatsData subset). + if(!wrapper.bytesAvailable) return true; + + this._playerWithMostHits = wrapper.readInt(); + this._playerWithMostKills = wrapper.readInt(); + this._hasPlayerStats = true; + + let totalStats = wrapper.readInt(); + + while(totalStats > 0) + { + const userId = wrapper.readInt(); + const figure = wrapper.readString(); + const gender = wrapper.readString(); + const snowballHits = wrapper.readInt(); + const kills = wrapper.readInt(); + const skillLevel = wrapper.readInt(); + + for(const team of this._teams) + { + for(const player of team.players) + { + if(player.userId === userId) player.applyStats(figure, gender, snowballHits, kills, skillLevel); + } + } + + totalStats--; + } + return true; } @@ -41,4 +83,21 @@ export class SnowWarOnGameEndingParser implements IMessageParser { return this._teams; } + + /** User id of the player with the most snowball hits (0 when the server sent no stats). */ + public get playerWithMostHits(): number + { + return this._playerWithMostHits; + } + + /** User id of the player with the most knock-outs (0 when the server sent no stats). */ + public get playerWithMostKills(): number + { + return this._playerWithMostKills; + } + + public get hasPlayerStats(): boolean + { + return this._hasPlayerStats; + } } diff --git a/packages/communication/src/messages/parser/game/snowwar/SnowWarPlayerData.ts b/packages/communication/src/messages/parser/game/snowwar/SnowWarPlayerData.ts index 63b0847f..84e1a4ef 100644 --- a/packages/communication/src/messages/parser/game/snowwar/SnowWarPlayerData.ts +++ b/packages/communication/src/messages/parser/game/snowwar/SnowWarPlayerData.ts @@ -8,6 +8,7 @@ export class SnowWarPlayerData private _name: string; private _figure: string; private _gender: string; + private _skillLevel: number = undefined; constructor(wrapper: IMessageDataWrapper) { @@ -48,4 +49,15 @@ export class SnowWarPlayerData { return this._gender; } + + /** AIR GameLobbyPlayerData.skillLevel (1-30); undefined when the server did not send it. */ + public get skillLevel(): number + { + return this._skillLevel; + } + + public set skillLevel(value: number) + { + this._skillLevel = value; + } } diff --git a/packages/communication/src/messages/parser/game/snowwar/SnowWarStatsTail.test.ts b/packages/communication/src/messages/parser/game/snowwar/SnowWarStatsTail.test.ts new file mode 100644 index 00000000..ef96e558 --- /dev/null +++ b/packages/communication/src/messages/parser/game/snowwar/SnowWarStatsTail.test.ts @@ -0,0 +1,116 @@ +import { IMessageDataWrapper } from '@octane/api'; +import { describe, expect, it } from 'vitest'; +import { UserCurrentBadgesParser } from '../../user/data/UserCurrentBadgesParser'; +import { SnowWarLobbyTeamsParser } from './SnowWarLobbyTeamsParser'; +import { SnowWarOnGameEndingParser } from './SnowWarOnGameEndingParser'; + +type Value = number | string | boolean; + +const wrapperOf = (values: Value[]): IMessageDataWrapper => +{ + let index = 0; + const next = () => values[index++]; + + return { + readByte: () => next() as number, + readBytes: () => null, + readBoolean: () => next() as boolean, + readShort: () => next() as number, + readInt: () => next() as number, + readFloat: () => next() as number, + readDouble: () => next() as number, + readString: () => next() as string, + header: 0, + get bytesAvailable() + { + return index < values.length; + } + }; +}; + +describe('SnowWarOnGameEndingParser', () => +{ + const teams: Value[] = [ 5, 2, 0, 12, 1, 7, 'Red', 12, 1, 3, 1, 9, 'Blue', 3 ]; + + it('parses the legacy team block without the stats tail', () => + { + const parser = new SnowWarOnGameEndingParser(); + parser.flush(); + + expect(parser.parse(wrapperOf(teams))).toBe(true); + expect(parser.secondsToResults).toBe(5); + expect(parser.teams.map(team => team.score)).toEqual([ 12, 3 ]); + expect(parser.hasPlayerStats).toBe(false); + expect(parser.playerWithMostHits).toBe(0); + expect(parser.teams[0].players[0].figure).toBeUndefined(); + expect(parser.teams[0].players[0].skillLevel).toBeUndefined(); + }); + + it('merges the official Game2GameEnding stats tail into the player rows', () => + { + const parser = new SnowWarOnGameEndingParser(); + parser.flush(); + + const tail: Value[] = [ 7, 9, 2, 9, 'hd-180-1', 'M', 1, 0, 4, 7, 'hr-100-61', 'F', 6, 2, 12 ]; + + expect(parser.parse(wrapperOf([ ...teams, ...tail ]))).toBe(true); + expect(parser.hasPlayerStats).toBe(true); + expect(parser.playerWithMostHits).toBe(7); + expect(parser.playerWithMostKills).toBe(9); + + const red = parser.teams[0].players[0]; + expect(red.userId).toBe(7); + expect(red.figure).toBe('hr-100-61'); + expect(red.gender).toBe('F'); + expect(red.snowballHits).toBe(6); + expect(red.kills).toBe(2); + expect(red.skillLevel).toBe(12); + + const blue = parser.teams[1].players[0]; + expect(blue.figure).toBe('hd-180-1'); + expect(blue.snowballHits).toBe(1); + expect(blue.kills).toBe(0); + expect(blue.skillLevel).toBe(4); + }); +}); + +describe('SnowWarLobbyTeamsParser', () => +{ + const base: Value[] = [ 2, 2, 0, 7, 0, 'Red', 'hr-1', 'F', 0, 9, 1, 'Blue', 'hd-2', 'M', 7, 1, 1, 1, 'Arctic', true ]; + + it('keeps the skill level undefined without the tail', () => + { + const parser = new SnowWarLobbyTeamsParser(); + parser.flush(); + + expect(parser.parse(wrapperOf(base))).toBe(true); + expect(parser.players.map(player => player.skillLevel)).toEqual([ undefined, undefined ]); + }); + + it('reads the skill level tail into the matching players', () => + { + const parser = new SnowWarLobbyTeamsParser(); + parser.flush(); + + expect(parser.parse(wrapperOf([ ...base, 2, 9, 3, 7, 15 ]))).toBe(true); + expect(parser.players.find(player => player.userId === 7).skillLevel).toBe(15); + expect(parser.players.find(player => player.userId === 9).skillLevel).toBe(3); + }); +}); + +describe('UserCurrentBadgesParser', () => +{ + it('reads the official (slot, code, ownerCount, rarity) layout', () => + { + const parser = new UserCurrentBadgesParser(); + parser.flush(); + + expect(parser.parse(wrapperOf([ 42, 2, 1, 'ACH_Login1', 120, 0, 2, 'HWAY1', 3, 4 ]))).toBe(true); + expect(parser.userId).toBe(42); + expect(parser.badges).toEqual([ 'ACH_Login1', 'HWAY1' ]); + expect(parser.badgeDetails).toEqual([ + { slotId: 1, badgeCode: 'ACH_Login1', ownerCount: 120, badgeRarityId: 0 }, + { slotId: 2, badgeCode: 'HWAY1', ownerCount: 3, badgeRarityId: 4 } + ]); + }); +}); diff --git a/packages/communication/src/messages/parser/group/utils/GroupMemberParser.ts b/packages/communication/src/messages/parser/group/utils/GroupMemberParser.ts index cf8317ba..920f7f2c 100644 --- a/packages/communication/src/messages/parser/group/utils/GroupMemberParser.ts +++ b/packages/communication/src/messages/parser/group/utils/GroupMemberParser.ts @@ -7,6 +7,8 @@ export class GroupRank public static readonly MEMBER: number = 2; public static readonly REQUESTED: number = 3; public static readonly DELETED: number = 4; + /** AIR 13: a member blocked by a group admin (same rank value the official client calls `blocked`). */ + public static readonly BLOCKED: number = 4; } export class GroupMemberParser diff --git a/packages/communication/src/messages/parser/hotlooks/HotLooksParser.ts b/packages/communication/src/messages/parser/hotlooks/HotLooksParser.ts new file mode 100644 index 00000000..28db3400 --- /dev/null +++ b/packages/communication/src/messages/parser/hotlooks/HotLooksParser.ts @@ -0,0 +1,42 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; + +/** One ready-made outfit of the avatar editor "hot looks" tab (AIR 13 HotLookInfo). */ +export interface IHotLookInfo +{ + gender: string; + figureString: string; +} + +export class HotLooksParser implements IMessageParser +{ + private _hotLooks: IHotLookInfo[] = []; + + public flush(): boolean + { + this._hotLooks = []; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + const count = wrapper.readInt(); + + for(let i = 0; i < count; i++) + { + const gender = wrapper.readString(); + const figureString = wrapper.readString(); + + this._hotLooks.push({ gender, figureString }); + } + + return true; + } + + public get hotLooks(): IHotLookInfo[] + { + return this._hotLooks; + } +} diff --git a/packages/communication/src/messages/parser/hotlooks/index.ts b/packages/communication/src/messages/parser/hotlooks/index.ts new file mode 100644 index 00000000..cf78d0cd --- /dev/null +++ b/packages/communication/src/messages/parser/hotlooks/index.ts @@ -0,0 +1 @@ +export * from './HotLooksParser'; diff --git a/packages/communication/src/messages/parser/index.ts b/packages/communication/src/messages/parser/index.ts index fd21a2fa..dbd19a79 100644 --- a/packages/communication/src/messages/parser/index.ts +++ b/packages/communication/src/messages/parser/index.ts @@ -28,6 +28,7 @@ export * from './handshake'; export * from './help'; export * from './housekeeping'; export * from './rarevalues'; +export * from './hotlooks'; export * from './soundboard'; export * from './traxeditor'; export * from './wheel'; diff --git a/packages/communication/src/messages/parser/quest/ActiveDailyTasksMessageParser.ts b/packages/communication/src/messages/parser/quest/ActiveDailyTasksMessageParser.ts new file mode 100644 index 00000000..a9651986 --- /dev/null +++ b/packages/communication/src/messages/parser/quest/ActiveDailyTasksMessageParser.ts @@ -0,0 +1,31 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; +import { DailyTaskData } from './DailyTaskData'; + +/** ActiveDailyTasks (2900) and DailyTasksAdded (670): a list of daily tasks. */ +export class ActiveDailyTasksMessageParser implements IMessageParser +{ + private _tasks: DailyTaskData[]; + + public flush(): boolean + { + this._tasks = []; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + const count = wrapper.readInt(); + + for(let i = 0; i < count; i++) this._tasks.push(new DailyTaskData(wrapper)); + + return true; + } + + public get tasks(): DailyTaskData[] + { + return this._tasks; + } +} diff --git a/packages/communication/src/messages/parser/quest/DailyTaskData.ts b/packages/communication/src/messages/parser/quest/DailyTaskData.ts new file mode 100644 index 00000000..111666b5 --- /dev/null +++ b/packages/communication/src/messages/parser/quest/DailyTaskData.ts @@ -0,0 +1,189 @@ +import { IMessageDataWrapper } from '@octane/api'; + +/** One reward of a daily task (AIR 13 class_4155). */ +export class DailyTaskRewardData +{ + private _productItemTypeId: number; + private _rewardTypeId: string; + private _extraParams: string; + private _amount: number; + + constructor(wrapper: IMessageDataWrapper) + { + this._productItemTypeId = wrapper.readShort(); + this._rewardTypeId = wrapper.readString(); + this._extraParams = wrapper.readString(); + this._amount = wrapper.readInt(); + } + + public get productItemTypeId(): number + { + return this._productItemTypeId; + } + + public get rewardTypeId(): string + { + return this._rewardTypeId; + } + + public get extraParams(): string + { + return this._extraParams; + } + + public get amount(): number + { + return this._amount; + } +} + +/** A daily task of the "daily reward" window (AIR 13 class_2786). */ +export class DailyTaskData +{ + public static readonly STATUS_IN_PROGRESS = 0; + public static readonly STATUS_COMPLETED = 1; + public static readonly STATUS_CLAIMED = 2; + + private _taskId: number; + private _taskCode: string; + private _questTypeCode: string; + private _isBonus: boolean; + private _imageVersion: string; + private _catalogName: string; + private _requiredRepeats: number; + private _repeats: number; + private _status: number; + private _secondsLeft: number; + private _receiveTime: Date; + private _rewards: DailyTaskRewardData[]; + + constructor(wrapper: IMessageDataWrapper) + { + this._receiveTime = new Date(); + this._taskId = DailyTaskData.readLong(wrapper); + this._taskCode = wrapper.readString(); + this._questTypeCode = wrapper.readString(); + this._isBonus = wrapper.readBoolean(); + this._imageVersion = wrapper.readString(); + this._catalogName = wrapper.readString(); + this._requiredRepeats = wrapper.readInt(); + this._repeats = wrapper.readInt(); + this._status = wrapper.readByte(); + this._secondsLeft = wrapper.readInt(); + this._rewards = []; + + const rewardCount = wrapper.readInt(); + + for(let i = 0; i < rewardCount; i++) this._rewards.push(new DailyTaskRewardData(wrapper)); + } + + /** The official task id travels as a 64-bit integer, two big-endian ints on the wire. */ + public static readLong(wrapper: IMessageDataWrapper): number + { + const high = wrapper.readInt(); + const low = wrapper.readInt(); + + return (high * 0x100000000) + (low >>> 0); + } + + public get taskId(): number + { + return this._taskId; + } + + public get taskCode(): string + { + return this._taskCode; + } + + public get questTypeCode(): string + { + return this._questTypeCode; + } + + public get isBonus(): boolean + { + return this._isBonus; + } + + public get imageVersion(): string + { + return this._imageVersion; + } + + public get catalogName(): string + { + return this._catalogName; + } + + public get requiredRepeats(): number + { + return this._requiredRepeats; + } + + public get repeats(): number + { + return this._repeats; + } + + public set repeats(value: number) + { + this._repeats = value; + } + + public get status(): number + { + return this._status; + } + + public set status(value: number) + { + this._status = value; + } + + public get rawSecondsLeft(): number + { + return this._secondsLeft; + } + + public set rawSecondsLeft(value: number) + { + this._secondsLeft = value; + this._receiveTime = new Date(); + } + + /** Seconds left, counted down from the moment the packet arrived; 0 once the raw value is gone. */ + public get secondsLeft(): number + { + if(this._secondsLeft <= 0) return 0; + + const elapsed = Math.floor((Date.now() - this._receiveTime.getTime()) / 1000); + + return Math.max(0, this._secondsLeft - elapsed); + } + + public get isExpired(): boolean + { + return (this._secondsLeft < 0) && (this._status !== DailyTaskData.STATUS_IN_PROGRESS); + } + + public get rewards(): DailyTaskRewardData[] + { + return this._rewards; + } + + public get nameLocalizationKey(): string + { + return `dailytask.${ this._taskCode }.name`; + } + + public get descriptionLocalizationKey(): string + { + return `dailytask.${ this._taskCode }.desc`; + } + + public get hintLocalizationKey(): string + { + return `dailytask.${ this._taskCode }.hint`; + } +} diff --git a/packages/communication/src/messages/parser/quest/DailyTaskUpdatedMessageParser.ts b/packages/communication/src/messages/parser/quest/DailyTaskUpdatedMessageParser.ts new file mode 100644 index 00000000..9d9d8591 --- /dev/null +++ b/packages/communication/src/messages/parser/quest/DailyTaskUpdatedMessageParser.ts @@ -0,0 +1,53 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; +import { DailyTaskData } from './DailyTaskData'; + +/** DailyTaskUpdated (9450): the repeats and status of one task changed. */ +export class DailyTaskUpdatedMessageParser implements IMessageParser +{ + private _taskId: number; + private _repeats: number; + private _status: number; + private _secondsLeft: number; + + public flush(): boolean + { + this._taskId = 0; + this._repeats = 0; + this._status = 0; + this._secondsLeft = 0; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + this._taskId = DailyTaskData.readLong(wrapper); + this._repeats = wrapper.readInt(); + this._status = wrapper.readByte(); + this._secondsLeft = wrapper.readInt(); + + return true; + } + + public get taskId(): number + { + return this._taskId; + } + + public get repeats(): number + { + return this._repeats; + } + + public get status(): number + { + return this._status; + } + + public get secondsLeft(): number + { + return this._secondsLeft; + } +} diff --git a/packages/communication/src/messages/parser/quest/QuestCancelledMessageParser.ts b/packages/communication/src/messages/parser/quest/QuestCancelledMessageParser.ts index 2e6e7475..9c593936 100644 --- a/packages/communication/src/messages/parser/quest/QuestCancelledMessageParser.ts +++ b/packages/communication/src/messages/parser/quest/QuestCancelledMessageParser.ts @@ -1,11 +1,16 @@ import { IMessageDataWrapper, IMessageParser } from '@octane/api'; +import { QuestMessageData } from './QuestMessageData'; +/** QuestCancelled (3027): the tracked quest stops; AIR 13 sends the expiry flag and the quest. */ export class QuestCancelledMessageParser implements IMessageParser { private _expired: boolean; + private _quest: QuestMessageData; public flush(): boolean { + this._quest = null; + return true; } @@ -14,6 +19,8 @@ export class QuestCancelledMessageParser implements IMessageParser if(!wrapper) return false; this._expired = wrapper.readBoolean(); + this._quest = new QuestMessageData(wrapper); + return true; } @@ -21,4 +28,9 @@ export class QuestCancelledMessageParser implements IMessageParser { return this._expired; } + + public get quest(): QuestMessageData + { + return this._quest; + } } diff --git a/packages/communication/src/messages/parser/quest/QuestMessageData.ts b/packages/communication/src/messages/parser/quest/QuestMessageData.ts index 3f768099..d35164ec 100644 --- a/packages/communication/src/messages/parser/quest/QuestMessageData.ts +++ b/packages/communication/src/messages/parser/quest/QuestMessageData.ts @@ -19,6 +19,8 @@ export class QuestMessageData private _catalogPageName: string; private _chainCode: string; private _easy: boolean; + private _isSeasonal: boolean; + private _secondsLeft: number; private _receiveTime: Date; constructor(wrapper: IMessageDataWrapper) @@ -40,6 +42,31 @@ export class QuestMessageData this._catalogPageName = wrapper.readString(); this._chainCode = wrapper.readString(); this._easy = wrapper.readBoolean(); + this._isSeasonal = wrapper.readBoolean(); + this._secondsLeft = 0; + + if(this._isSeasonal) this._secondsLeft = wrapper.readInt(); + } + + /** Seasonal quests carry a countdown; it is counted down from the moment the packet arrived. */ + public get isSeasonal(): boolean + { + return this._isSeasonal; + } + + public get secondsLeft(): number + { + if(this._secondsLeft <= 0) return 0; + + const elapsed = Math.floor((Date.now() - this._receiveTime.getTime()) / 1000); + + return this._secondsLeft - elapsed; + } + + /** The key of the tracker instance: seasonal quests are tracked per chain. */ + public get campaignChainCode(): string + { + return this._isSeasonal ? `${ this._campaignCode }.${ this._chainCode }` : this._campaignCode; } public static getCampaignLocalizationKeyForCode(code: string): string diff --git a/packages/communication/src/messages/parser/quest/RewardTrackClaimResultMessageParser.ts b/packages/communication/src/messages/parser/quest/RewardTrackClaimResultMessageParser.ts new file mode 100644 index 00000000..f92dac49 --- /dev/null +++ b/packages/communication/src/messages/parser/quest/RewardTrackClaimResultMessageParser.ts @@ -0,0 +1,44 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; + +/** RewardTrackClaimResult (9451): 0 is success, any other code is localized as reward_track.claim.notification.fail.N. */ +export class RewardTrackClaimResultMessageParser implements IMessageParser +{ + private _trackId: string; + private _rewardId: string; + private _resultCode: number; + + public flush(): boolean + { + this._trackId = ''; + this._rewardId = ''; + this._resultCode = 0; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + this._trackId = wrapper.readString(); + this._rewardId = wrapper.readString(); + this._resultCode = wrapper.readInt(); + + return true; + } + + public get trackId(): string + { + return this._trackId; + } + + public get rewardId(): string + { + return this._rewardId; + } + + public get resultCode(): number + { + return this._resultCode; + } +} diff --git a/packages/communication/src/messages/parser/quest/RewardTrackData.ts b/packages/communication/src/messages/parser/quest/RewardTrackData.ts new file mode 100644 index 00000000..9c1d95e0 --- /dev/null +++ b/packages/communication/src/messages/parser/quest/RewardTrackData.ts @@ -0,0 +1,421 @@ +import { IMessageDataWrapper } from '@octane/api'; + +/** One level of a reward-track task (AIR 13 class_4130). */ +export class RewardTrackTaskLevelData +{ + private _requiredCount: number; + private _pointsReward: number; + private _premium: boolean; + + constructor(wrapper: IMessageDataWrapper) + { + this._requiredCount = wrapper.readInt(); + this._pointsReward = wrapper.readInt(); + this._premium = wrapper.readBoolean(); + } + + public get requiredCount(): number + { + return this._requiredCount; + } + + public get pointsReward(): number + { + return this._pointsReward; + } + + public get premium(): boolean + { + return this._premium; + } +} + +/** A reward-track task (AIR 13 class_4037). */ +export class RewardTrackTaskData +{ + private _id: string; + private _actionType: string; + private _parameter: string; + private _progressCount: number; + private _premium: boolean; + private _levels: RewardTrackTaskLevelData[]; + + constructor(wrapper: IMessageDataWrapper) + { + this._id = wrapper.readString(); + this._actionType = wrapper.readString(); + this._parameter = wrapper.readString(); + this._progressCount = wrapper.readInt(); + this._premium = wrapper.readBoolean(); + this._levels = []; + + const count = wrapper.readInt(); + + for(let i = 0; i < count; i++) this._levels.push(new RewardTrackTaskLevelData(wrapper)); + } + + public get id(): string + { + return this._id; + } + + public get actionType(): string + { + return this._actionType; + } + + public get parameter(): string + { + return this._parameter; + } + + public get progressCount(): number + { + return this._progressCount; + } + + public set progressCount(value: number) + { + this._progressCount = value; + } + + public get premium(): boolean + { + return this._premium; + } + + public get levels(): RewardTrackTaskLevelData[] + { + return this._levels; + } + + public get isComplete(): boolean + { + return this._levels.every(level => this._progressCount >= level.requiredCount); + } + + public get hasProgress(): boolean + { + return this._progressCount > 0; + } + + /** The first level not reached yet, or the last one when the task is complete. */ + public get activeLevelIndex(): number + { + const index = this._levels.findIndex(level => this._progressCount < level.requiredCount); + + return (index === -1) ? Math.max(0, this._levels.length - 1) : index; + } + + public get activeLevel(): RewardTrackTaskLevelData + { + return this._levels[this.activeLevelIndex] ?? null; + } + + public progressRatioFor(level: RewardTrackTaskLevelData): number + { + if(!level || (level.requiredCount <= 0)) return 1; + + return Math.min(1, Math.max(0, this._progressCount / level.requiredCount)); + } +} + +/** A prize on the track (AIR 13 class_3945). */ +export class RewardTrackPrizeData +{ + private _id: string; + private _requiredPoints: number; + private _productItemTypeId: number; + private _rewardTypeId: string; + private _extraParams: string; + private _rewardAmount: number; + private _premium: boolean; + private _available: boolean; + private _claimed: boolean; + + constructor(wrapper: IMessageDataWrapper) + { + this._id = wrapper.readString(); + this._requiredPoints = wrapper.readInt(); + this._productItemTypeId = wrapper.readShort(); + this._rewardTypeId = wrapper.readString(); + this._extraParams = wrapper.readString(); + this._rewardAmount = wrapper.readInt(); + this._premium = wrapper.readBoolean(); + this._available = wrapper.readBoolean(); + this._claimed = wrapper.readBoolean(); + } + + public get id(): string + { + return this._id; + } + + public get requiredPoints(): number + { + return this._requiredPoints; + } + + public get productItemTypeId(): number + { + return this._productItemTypeId; + } + + public get rewardTypeId(): string + { + return this._rewardTypeId; + } + + public get extraParams(): string + { + return this._extraParams; + } + + public get rewardAmount(): number + { + return this._rewardAmount; + } + + public get premium(): boolean + { + return this._premium; + } + + public get available(): boolean + { + return this._available; + } + + public set available(value: boolean) + { + this._available = value; + } + + public get claimed(): boolean + { + return this._claimed; + } + + public set claimed(value: boolean) + { + this._claimed = value; + } + + public isPremiumLocked(track: RewardTrackData): boolean + { + return this._premium && !track.premium; + } + + public hasEnoughPoints(track: RewardTrackData): boolean + { + return track.points >= this._requiredPoints; + } + + public isAvailable(track: RewardTrackData): boolean + { + return !this.isPremiumLocked(track) && this.hasEnoughPoints(track); + } + + public isClaimable(track: RewardTrackData): boolean + { + return this.isAvailable(track) && !this._claimed; + } +} + +/** A reward track (AIR 13 class_2464). */ +export class RewardTrackData +{ + private _id: string; + private _theme: string; + private _points: number; + private _hasPremiumConfig: boolean; + private _taskPointsBoost: number; + private _instantPoints: number; + private _costDiamonds: number; + private _costCredits: number; + private _premium: boolean; + private _complete: boolean; + private _premiumComplete: boolean; + private _tasks: RewardTrackTaskData[]; + private _prizes: RewardTrackPrizeData[]; + + constructor(wrapper: IMessageDataWrapper) + { + this._id = wrapper.readString(); + this._theme = wrapper.readString(); + this._points = wrapper.readInt(); + this._hasPremiumConfig = wrapper.readBoolean(); + this._taskPointsBoost = 0; + this._instantPoints = 0; + this._costDiamonds = 0; + this._costCredits = 0; + + if(this._hasPremiumConfig) + { + this._taskPointsBoost = wrapper.readDouble(); + this._instantPoints = wrapper.readInt(); + this._costDiamonds = wrapper.readInt(); + this._costCredits = wrapper.readInt(); + } + + this._premium = wrapper.readBoolean(); + this._complete = wrapper.readBoolean(); + this._premiumComplete = wrapper.readBoolean(); + this._tasks = []; + this._prizes = []; + + const taskCount = wrapper.readInt(); + + for(let i = 0; i < taskCount; i++) this._tasks.push(new RewardTrackTaskData(wrapper)); + + const prizeCount = wrapper.readInt(); + + for(let i = 0; i < prizeCount; i++) this._prizes.push(new RewardTrackPrizeData(wrapper)); + } + + public get id(): string + { + return this._id; + } + + public get theme(): string + { + return this._theme; + } + + public get points(): number + { + return this._points; + } + + public set points(value: number) + { + this._points = value; + this.refreshDerivedState(); + } + + public get hasPremiumConfig(): boolean + { + return this._hasPremiumConfig; + } + + public get taskPointsBoost(): number + { + return this._taskPointsBoost; + } + + public get instantPoints(): number + { + return this._instantPoints; + } + + public get costDiamonds(): number + { + return this._costDiamonds; + } + + public get costCredits(): number + { + return this._costCredits; + } + + public get premium(): boolean + { + return this._premium; + } + + public get complete(): boolean + { + return this._complete; + } + + public get premiumComplete(): boolean + { + return this._premiumComplete; + } + + public get tasks(): RewardTrackTaskData[] + { + return this._tasks; + } + + public get prizes(): RewardTrackPrizeData[] + { + return this._prizes; + } + + public getTask(taskId: string): RewardTrackTaskData + { + return this._tasks.find(task => task.id === taskId) ?? null; + } + + public getPrize(prizeId: string): RewardTrackPrizeData + { + return this._prizes.find(prize => prize.id === prizeId) ?? null; + } + + public get completedTaskCount(): number + { + return this._tasks.filter(task => task.isComplete).length; + } + + public get totalTaskCount(): number + { + return this._tasks.length; + } + + public get claimedPrizeCount(): number + { + return this._prizes.filter(prize => prize.claimed).length; + } + + public get totalPrizeCount(): number + { + return this._prizes.length; + } + + public get claimablePrizeCount(): number + { + return this._prizes.filter(prize => prize.isClaimable(this)).length; + } + + public get hasPremiumPrizes(): boolean + { + return this._prizes.some(prize => prize.premium); + } + + public get hasPremiumTasks(): boolean + { + return this._tasks.some(task => task.premium); + } + + public get hasPremiumLevels(): boolean + { + return this._tasks.some(task => task.levels.some(level => level.premium)); + } + + public markPrizeClaimed(prizeId: string): void + { + const prize = this.getPrize(prizeId); + + if(prize) prize.claimed = true; + + this.refreshDerivedState(); + } + + public markPremiumPurchased(points: number): void + { + this._premium = true; + this._points = points; + this.refreshDerivedState(); + } + + /** The official rule: complete = every free prize claimed; premiumComplete adds the premium ones. */ + public refreshDerivedState(): void + { + for(const prize of this._prizes) prize.available = prize.isAvailable(this); + + this._complete = this._prizes.filter(prize => !prize.premium).every(prize => prize.claimed); + this._premiumComplete = !this._hasPremiumConfig || (this._complete && this._prizes.filter(prize => prize.premium).every(prize => prize.claimed)); + } +} diff --git a/packages/communication/src/messages/parser/quest/RewardTrackPremiumPurchaseResultMessageParser.ts b/packages/communication/src/messages/parser/quest/RewardTrackPremiumPurchaseResultMessageParser.ts new file mode 100644 index 00000000..cde6a2c6 --- /dev/null +++ b/packages/communication/src/messages/parser/quest/RewardTrackPremiumPurchaseResultMessageParser.ts @@ -0,0 +1,44 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; + +/** RewardTrackPremiumPurchaseResult (2248): 0 is success, else reward_track.premium.notification.fail.N. */ +export class RewardTrackPremiumPurchaseResultMessageParser implements IMessageParser +{ + private _trackId: string; + private _resultCode: number; + private _points: number; + + public flush(): boolean + { + this._trackId = ''; + this._resultCode = 0; + this._points = 0; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + this._trackId = wrapper.readString(); + this._resultCode = wrapper.readInt(); + this._points = wrapper.readInt(); + + return true; + } + + public get trackId(): string + { + return this._trackId; + } + + public get resultCode(): number + { + return this._resultCode; + } + + public get points(): number + { + return this._points; + } +} diff --git a/packages/communication/src/messages/parser/quest/RewardTrackProgressMessageParser.ts b/packages/communication/src/messages/parser/quest/RewardTrackProgressMessageParser.ts new file mode 100644 index 00000000..c3f6dcb0 --- /dev/null +++ b/packages/communication/src/messages/parser/quest/RewardTrackProgressMessageParser.ts @@ -0,0 +1,52 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; + +/** RewardTrackProgress (9452): a task moved and the track points it yielded. */ +export class RewardTrackProgressMessageParser implements IMessageParser +{ + private _trackId: string; + private _taskId: string; + private _progressCount: number; + private _points: number; + + public flush(): boolean + { + this._trackId = ''; + this._taskId = ''; + this._progressCount = 0; + this._points = 0; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + this._trackId = wrapper.readString(); + this._taskId = wrapper.readString(); + this._progressCount = wrapper.readInt(); + this._points = wrapper.readInt(); + + return true; + } + + public get trackId(): string + { + return this._trackId; + } + + public get taskId(): string + { + return this._taskId; + } + + public get progressCount(): number + { + return this._progressCount; + } + + public get points(): number + { + return this._points; + } +} diff --git a/packages/communication/src/messages/parser/quest/RewardTracksMessageParser.ts b/packages/communication/src/messages/parser/quest/RewardTracksMessageParser.ts new file mode 100644 index 00000000..809ea348 --- /dev/null +++ b/packages/communication/src/messages/parser/quest/RewardTracksMessageParser.ts @@ -0,0 +1,49 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; +import { RewardTrackData } from './RewardTrackData'; + +/** RewardTracks (2327): every active track; `disabled` wipes them, `reload` rebuilds the windows. */ +export class RewardTracksMessageParser implements IMessageParser +{ + private _disabled: boolean; + private _tracks: RewardTrackData[]; + private _reload: boolean; + + public flush(): boolean + { + this._disabled = false; + this._tracks = []; + this._reload = false; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + this._disabled = wrapper.readBoolean(); + + const count = wrapper.readInt(); + + for(let i = 0; i < count; i++) this._tracks.push(new RewardTrackData(wrapper)); + + this._reload = wrapper.readBoolean(); + + return true; + } + + public get disabled(): boolean + { + return this._disabled; + } + + public get tracks(): RewardTrackData[] + { + return this._tracks; + } + + public get reload(): boolean + { + return this._reload; + } +} diff --git a/packages/communication/src/messages/parser/quest/index.ts b/packages/communication/src/messages/parser/quest/index.ts index 1185da4f..2446b776 100644 --- a/packages/communication/src/messages/parser/quest/index.ts +++ b/packages/communication/src/messages/parser/quest/index.ts @@ -15,3 +15,11 @@ export * from './QuestMessageData'; export * from './QuestMessageParser'; export * from './QuestsMessageParser'; export * from './SeasonalQuestsParser'; +export * from './ActiveDailyTasksMessageParser'; +export * from './DailyTaskData'; +export * from './DailyTaskUpdatedMessageParser'; +export * from './RewardTrackClaimResultMessageParser'; +export * from './RewardTrackData'; +export * from './RewardTrackPremiumPurchaseResultMessageParser'; +export * from './RewardTrackProgressMessageParser'; +export * from './RewardTracksMessageParser'; diff --git a/packages/communication/src/messages/parser/user/data/UserCurrentBadgesParser.ts b/packages/communication/src/messages/parser/user/data/UserCurrentBadgesParser.ts index d93e03ec..2cfb6708 100644 --- a/packages/communication/src/messages/parser/user/data/UserCurrentBadgesParser.ts +++ b/packages/communication/src/messages/parser/user/data/UserCurrentBadgesParser.ts @@ -1,14 +1,30 @@ import { IMessageDataWrapper, IMessageParser } from '@octane/api'; +export interface IUserCurrentBadgeData +{ + slotId: number; + badgeCode: string; + /** Users holding the badge. */ + ownerCount: number; + /** Official BadgeRarity tier id 0 (common) .. 6 (unique). */ + badgeRarityId: number; +} + +/** + * UserCurrentBadges (1087) in the official layout: per worn slot + * (badgeIndex, badgeCode, ownerCount, badgeRarityId). + */ export class UserCurrentBadgesParser implements IMessageParser { private _userId: number; private _badges: string[]; + private _badgeDetails: IUserCurrentBadgeData[]; public flush(): boolean { this._userId = null; this._badges = []; + this._badgeDetails = []; return true; } @@ -25,8 +41,11 @@ export class UserCurrentBadgesParser implements IMessageParser { const slotId = wrapper.readInt(); const badgeCode = wrapper.readString(); + const ownerCount = wrapper.readInt(); + const badgeRarityId = wrapper.readInt(); this._badges.push(badgeCode); + this._badgeDetails.push({ slotId, badgeCode, ownerCount, badgeRarityId }); totalBadges--; } @@ -43,4 +62,9 @@ export class UserCurrentBadgesParser implements IMessageParser { return this._badges; } + + public get badgeDetails(): IUserCurrentBadgeData[] + { + return this._badgeDetails; + } } diff --git a/packages/communication/src/messages/parser/user/data/UserSettingsParser.ts b/packages/communication/src/messages/parser/user/data/UserSettingsParser.ts index 0666f908..aac6a674 100644 --- a/packages/communication/src/messages/parser/user/data/UserSettingsParser.ts +++ b/packages/communication/src/messages/parser/user/data/UserSettingsParser.ts @@ -14,6 +14,11 @@ export class UserSettingsParser implements IMessageParser private _onlineStatusVisible: boolean; private _friendsCanFollow: boolean; private _friendRequestsAllowed: boolean; + private _wiredWhisperDisabled: boolean; + private _chatMode: number; + private _chatBubbleWidth: number; + private _chatScrollSpeed: number; + private _onlineIndicatorPreference: number; public flush(): boolean { @@ -29,6 +34,11 @@ export class UserSettingsParser implements IMessageParser this._onlineStatusVisible = true; this._friendsCanFollow = true; this._friendRequestsAllowed = true; + this._wiredWhisperDisabled = false; + this._chatMode = 0; + this._chatBubbleWidth = 1; + this._chatScrollSpeed = 1; + this._onlineIndicatorPreference = 0; return true; } @@ -49,6 +59,13 @@ export class UserSettingsParser implements IMessageParser this._friendsCanFollow = wrapper.readBoolean(); this._friendRequestsAllowed = wrapper.readBoolean(); this._volumeSoundboard = wrapper.bytesAvailable ? wrapper.readInt() : 80; + // Trailing per-user preferences (official UserSettings 3574 layout, each optional so an older + // emulator that stops at the soundboard volume keeps the official defaults). + this._wiredWhisperDisabled = wrapper.bytesAvailable ? wrapper.readBoolean() : false; + this._chatMode = wrapper.bytesAvailable ? wrapper.readInt() : 0; + this._chatBubbleWidth = wrapper.bytesAvailable ? wrapper.readInt() : 1; + this._chatScrollSpeed = wrapper.bytesAvailable ? wrapper.readInt() : 1; + this._onlineIndicatorPreference = wrapper.bytesAvailable ? wrapper.readInt() : 0; return true; } @@ -112,4 +129,29 @@ export class UserSettingsParser implements IMessageParser { return this._friendRequestsAllowed; } + + public get wiredWhisperDisabled(): boolean + { + return this._wiredWhisperDisabled; + } + + public get chatMode(): number + { + return this._chatMode; + } + + public get chatBubbleWidth(): number + { + return this._chatBubbleWidth; + } + + public get chatScrollSpeed(): number + { + return this._chatScrollSpeed; + } + + public get onlineIndicatorPreference(): number + { + return this._onlineIndicatorPreference; + } } diff --git a/packages/communication/src/messages/parser/user/data/__tests__/UserSettingsParser.test.ts b/packages/communication/src/messages/parser/user/data/__tests__/UserSettingsParser.test.ts index 80e6bb69..7dc219f7 100644 --- a/packages/communication/src/messages/parser/user/data/__tests__/UserSettingsParser.test.ts +++ b/packages/communication/src/messages/parser/user/data/__tests__/UserSettingsParser.test.ts @@ -86,3 +86,63 @@ describe('UserSettingsParser Soundboard volume', () => expect(parser.volumeSoundboard).toBe(80); }); }); + +describe('UserSettingsParser per-user preferences', () => +{ + const parse = (writer: BinaryWriter) => + { + const parser = new UserSettingsParser(); + parser.flush(); + expect(parser.parse(new TestWrapper(new BinaryReader(writer.getBuffer())) as any)).toBe(true); + return parser; + }; + + it('reads the trailing wired whisper, chat and friend-online preferences', () => + { + const writer = new BinaryWriter(); + writeLegacySettings(writer); + writer.writeInt(40); + writer.writeByte(1); + writer.writeInt(1); + writer.writeInt(2); + writer.writeInt(0); + writer.writeInt(2); + + const parser = parse(writer); + + expect(parser.volumeSoundboard).toBe(40); + expect(parser.wiredWhisperDisabled).toBe(true); + expect(parser.chatMode).toBe(1); + expect(parser.chatBubbleWidth).toBe(2); + expect(parser.chatScrollSpeed).toBe(0); + expect(parser.onlineIndicatorPreference).toBe(2); + }); + + it('keeps the official defaults when the emulator stops at the soundboard volume', () => + { + const writer = new BinaryWriter(); + writeLegacySettings(writer); + writer.writeInt(40); + + const parser = parse(writer); + + expect(parser.chatType).toBe(4); + expect(parser.wiredWhisperDisabled).toBe(false); + expect(parser.chatMode).toBe(0); + expect(parser.chatBubbleWidth).toBe(1); + expect(parser.chatScrollSpeed).toBe(1); + expect(parser.onlineIndicatorPreference).toBe(0); + }); + + it('keeps the official defaults for the legacy packet without the soundboard volume', () => + { + const writer = new BinaryWriter(); + writeLegacySettings(writer); + + const parser = parse(writer); + + expect(parser.volumeSoundboard).toBe(80); + expect(parser.chatBubbleWidth).toBe(1); + expect(parser.onlineIndicatorPreference).toBe(0); + }); +}); diff --git a/packages/communication/src/messages/parser/user/index.ts b/packages/communication/src/messages/parser/user/index.ts index 82dfb339..361d910d 100644 --- a/packages/communication/src/messages/parser/user/index.ts +++ b/packages/communication/src/messages/parser/user/index.ts @@ -28,3 +28,4 @@ export * from './ScrKickbackData'; export * from './ScrSendKickbackInfoMessageParser'; export * from './wardrobe'; export * from './WelcomeGiftChangeEmailResultParser'; +export * from './wordfilter'; diff --git a/packages/communication/src/messages/parser/user/wordfilter/CustomFilterResultParser.ts b/packages/communication/src/messages/parser/user/wordfilter/CustomFilterResultParser.ts new file mode 100644 index 00000000..759a28a2 --- /dev/null +++ b/packages/communication/src/messages/parser/user/wordfilter/CustomFilterResultParser.ts @@ -0,0 +1,35 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; + +/** CustomFilterResult (3883): the user's personal word filter list. */ +export class CustomFilterResultParser implements IMessageParser +{ + private _words: string[]; + + public flush(): boolean + { + this._words = []; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + let totalWords = wrapper.readInt(); + + while(totalWords > 0) + { + this._words.push(wrapper.readString()); + + totalWords--; + } + + return true; + } + + public get words(): string[] + { + return this._words; + } +} diff --git a/packages/communication/src/messages/parser/user/wordfilter/CustomWordFilterPacketContract.test.ts b/packages/communication/src/messages/parser/user/wordfilter/CustomWordFilterPacketContract.test.ts new file mode 100644 index 00000000..3455d44f --- /dev/null +++ b/packages/communication/src/messages/parser/user/wordfilter/CustomWordFilterPacketContract.test.ts @@ -0,0 +1,79 @@ +import { BinaryReader, BinaryWriter } from '@octane/utils'; +import { describe, expect, it } from 'vitest'; +import { OctaneMessages } from '../../../../OctaneMessages'; +import { IncomingHeader } from '../../../incoming/IncomingHeader'; +import { CustomFilterResultEvent, ModifyCustomFilterResultEvent } from '../../../incoming/user/wordfilter'; +import { OutgoingHeader } from '../../../outgoing/OutgoingHeader'; +import { AddCustomFilterWordMessageComposer, GetCustomFilterMessageComposer, RemoveCustomFilterWordMessageComposer } from '../../../outgoing/user/wordfilter'; +import { CustomFilterResultParser } from './CustomFilterResultParser'; +import { ModifyCustomFilterResultParser } from './ModifyCustomFilterResultParser'; + +class TestWrapper +{ + constructor(private reader: BinaryReader) + {} + readInt() + { + return this.reader.readInt(); + } + readString() + { + const length = this.reader.readShort(); return this.reader.readBytes(length).toString(); + } + get bytesAvailable() + { + return this.reader.remaining() > 0; + } +} + +/** AIR 13 class_1956: composers 145 / 68 / 1996, events 3883 / 3333. */ +describe('personal word filter packet contract', () => +{ + it('uses the official headers and registers every request and response', () => + { + const messages = new OctaneMessages(); + + expect(OutgoingHeader.GET_CUSTOM_FILTER).toBe(145); + expect(OutgoingHeader.ADD_CUSTOM_FILTER_WORD).toBe(68); + expect(OutgoingHeader.REMOVE_CUSTOM_FILTER_WORD).toBe(1996); + expect(IncomingHeader.CUSTOM_FILTER_RESULT).toBe(3883); + expect(IncomingHeader.MODIFY_CUSTOM_FILTER_RESULT).toBe(3333); + + expect(messages.composers.get(145)).toBe(GetCustomFilterMessageComposer); + expect(messages.composers.get(68)).toBe(AddCustomFilterWordMessageComposer); + expect(messages.composers.get(1996)).toBe(RemoveCustomFilterWordMessageComposer); + expect(messages.events.get(3883)).toBe(CustomFilterResultEvent); + expect(messages.events.get(3333)).toBe(ModifyCustomFilterResultEvent); + }); + + it('serializes the request bodies as the official composers do', () => + { + expect(new GetCustomFilterMessageComposer().getMessageArray()).toEqual([]); + expect(new AddCustomFilterWordMessageComposer('pippo').getMessageArray()).toEqual([ 'pippo' ]); + expect(new RemoveCustomFilterWordMessageComposer('pippo').getMessageArray()).toEqual([ 'pippo' ]); + }); + + it('parses the word list as a counted string block', () => + { + const writer = new BinaryWriter(); + writer.writeInt(2); writer.writeString('pippo'); writer.writeString('pluto'); + + const parser = new CustomFilterResultParser(); + expect(parser.flush()).toBe(true); + expect(parser.parse(new TestWrapper(new BinaryReader(writer.getBuffer())) as any)).toBe(true); + expect(parser.words).toEqual([ 'pippo', 'pluto' ]); + }); + + it('parses the modify result as result code then word', () => + { + const writer = new BinaryWriter(); + writer.writeInt(ModifyCustomFilterResultEvent.REMOVED); writer.writeString('pippo'); + + const parser = new ModifyCustomFilterResultParser(); + expect(parser.flush()).toBe(true); + expect(parser.parse(new TestWrapper(new BinaryReader(writer.getBuffer())) as any)).toBe(true); + expect(parser.result).toBe(3); + expect(parser.word).toBe('pippo'); + expect(ModifyCustomFilterResultEvent.ADDED).toBe(1); + }); +}); diff --git a/packages/communication/src/messages/parser/user/wordfilter/ModifyCustomFilterResultParser.ts b/packages/communication/src/messages/parser/user/wordfilter/ModifyCustomFilterResultParser.ts new file mode 100644 index 00000000..ae5fc2ca --- /dev/null +++ b/packages/communication/src/messages/parser/user/wordfilter/ModifyCustomFilterResultParser.ts @@ -0,0 +1,36 @@ +import { IMessageDataWrapper, IMessageParser } from '@octane/api'; + +/** ModifyCustomFilterResult (3333): result code followed by the word it refers to. */ +export class ModifyCustomFilterResultParser implements IMessageParser +{ + private _result: number; + private _word: string; + + public flush(): boolean + { + this._result = -1; + this._word = ''; + + return true; + } + + public parse(wrapper: IMessageDataWrapper): boolean + { + if(!wrapper) return false; + + this._result = wrapper.readInt(); + this._word = wrapper.readString(); + + return true; + } + + public get result(): number + { + return this._result; + } + + public get word(): string + { + return this._word; + } +} diff --git a/packages/communication/src/messages/parser/user/wordfilter/index.ts b/packages/communication/src/messages/parser/user/wordfilter/index.ts new file mode 100644 index 00000000..4c34f858 --- /dev/null +++ b/packages/communication/src/messages/parser/user/wordfilter/index.ts @@ -0,0 +1,2 @@ +export * from './CustomFilterResultParser'; +export * from './ModifyCustomFilterResultParser'; diff --git a/packages/events/src/session/RoomSessionUserBadgesEvent.ts b/packages/events/src/session/RoomSessionUserBadgesEvent.ts index 51dcca25..2e9fb3dd 100644 --- a/packages/events/src/session/RoomSessionUserBadgesEvent.ts +++ b/packages/events/src/session/RoomSessionUserBadgesEvent.ts @@ -1,20 +1,31 @@ import { IRoomSession } from '@octane/api'; import { RoomSessionEvent } from './RoomSessionEvent'; +/** One worn badge of UserCurrentBadges (1087) with its official rarity data. */ +export interface IRoomSessionUserBadgeDetail +{ + slotId: number; + badgeCode: string; + ownerCount: number; + badgeRarityId: number; +} + export class RoomSessionUserBadgesEvent extends RoomSessionEvent { public static RSUBE_BADGES: string = 'RSUBE_BADGES'; private _userId: number = 0; private _badges: string[]; + private _badgeDetails: IRoomSessionUserBadgeDetail[]; - constructor(session: IRoomSession, userId: number, badges: string[]) + constructor(session: IRoomSession, userId: number, badges: string[], badgeDetails: IRoomSessionUserBadgeDetail[] = []) { super(RoomSessionUserBadgesEvent.RSUBE_BADGES, session); this._badges = []; this._userId = userId; this._badges = badges; + this._badgeDetails = badgeDetails; } public get userId(): number @@ -26,4 +37,9 @@ export class RoomSessionUserBadgesEvent extends RoomSessionEvent { return this._badges; } + + public get badgeDetails(): IRoomSessionUserBadgeDetail[] + { + return this._badgeDetails; + } } diff --git a/packages/room/src/RoomEngine.ts b/packages/room/src/RoomEngine.ts index 6fcf6bce..dac3713a 100644 --- a/packages/room/src/RoomEngine.ts +++ b/packages/room/src/RoomEngine.ts @@ -4,7 +4,7 @@ import { GetConfiguration } from '@octane/configuration'; import { BadgeImageReadyEvent, GetEventDispatcher, OctaneToolbarAnimateIconEvent, RoomBackgroundColorEvent, RoomDragEvent, RoomEngineAreaHideStateEvent, RoomEngineEvent, RoomEngineObjectEvent, RoomObjectEvent, RoomObjectFurnitureActionEvent, RoomObjectMouseEvent, RoomSessionEvent, RoomToObjectOwnAvatarMoveEvent } from '@octane/events'; import { GetRoomSessionManager, GetSessionDataManager } from '@octane/session'; import { FurniId, GetTickerTime, OctaneLogger, NumberBank, TextureUtils, Vector3d } from '@octane/utils'; -import { Container, Matrix, Point, Rectangle, RenderTexture, Sprite, Texture, Ticker } from 'pixi.js'; +import { Container, Matrix, Point, PointData, Rectangle, RenderTexture, Sprite, Texture, Ticker } from 'pixi.js'; import { GetRoomContentLoader } from './GetRoomContentLoader'; import { GetRoomManager } from './GetRoomManager'; import { GetRoomMessageHandler } from './GetRoomMessageHandler'; @@ -391,17 +391,22 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService if(roomCanvas) roomCanvas.setMask(flag); } - public setRoomInstanceRenderingCanvasScale(roomId: number, canvasId: number, level: number, point: Point = null, offsetPoint: Point = null, isFlipForced: boolean = false, flag: boolean = false): void + public setRoomInstanceRenderingCanvasScale(roomId: number, canvasId: number, level: number, point: PointData = null, offsetPoint: PointData = null, isFlipForced: boolean = false, isAnimated: boolean = false): void { if(!GetConfiguration().getValue('room.zoom.enabled', true)) return; - if(!flag) level = ((isFlipForced) ? -1 : ((level) < 1) ? 0.5 : Math.floor(level)); + // Discrete zoom levels are snapped; an animation frame passes its + // fractional scale through untouched. + if(!isAnimated && !isFlipForced) level = (((level) < 1) ? 0.5 : Math.floor(level)); const roomCanvas = this.getRoomInstanceRenderingCanvas(roomId, canvasId); if(!roomCanvas) return; - roomCanvas.setScale(level, point, offsetPoint, isFlipForced); + // A forced flip toggles the 180 degree flip and leaves the zoom scale alone. + if(isFlipForced) roomCanvas.setFlip(!roomCanvas.isFlipped, point, offsetPoint); + else roomCanvas.setScale(level, point, offsetPoint, false); + this.syncRoomCameraLocationToCanvasOffset(roomId, roomCanvas); GetEventDispatcher().dispatchEvent(new RoomEngineEvent(RoomEngineEvent.ROOM_ZOOMED, roomId)); @@ -419,7 +424,8 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService private syncRoomCameraLocationToCanvasOffset(roomId: number, canvas: IRoomRenderingCanvas): void { - if(!this.useOffsetScrolling || !canvas || (canvas.scale <= 0)) return; + // Offset scrolling is suspended while the room is flipped. + if(!this.useOffsetScrolling || !canvas || (canvas.scale <= 0) || canvas.isFlipped) return; const instanceData = this.getRoomInstanceData(roomId); @@ -497,6 +503,25 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService return canvas.scale; } + public getRoomInstanceRenderingCanvasIsFlipped(roomId: number = -1000, canvasId: number = -1): boolean + { + if(roomId === -1000) roomId = this._activeRoomId; + + if(canvasId === -1) canvasId = this._activeRoomActiveCanvas; + + const canvas = this.getRoomInstanceRenderingCanvas(roomId, canvasId); + + return !!(canvas && canvas.isFlipped); + } + + // Signed display scale of a canvas: negative on both axes while flipped. + private getCanvasTransformScale(canvas: IRoomRenderingCanvas): number + { + if(!canvas) return 1; + + return (canvas.isFlipped ? -canvas.scale : canvas.scale); + } + public initializeRoomInstanceRenderingCanvas(roomId: number, canvasId: number, width: number, height: number): void { const canvas = this.getRoomInstanceRenderingCanvas(roomId, canvasId); @@ -1097,7 +1122,8 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService const renderingCanvas = this.getRoomInstanceRenderingCanvas(roomId, canvasId); const instanceData = this.getRoomInstanceData(roomId); - if(!renderingCanvas || !instanceData || (renderingCanvas.scale !== 1)) return; + // The camera never follows while the room is flipped. + if(!renderingCanvas || !instanceData || (renderingCanvas.scale !== 1) || renderingCanvas.isFlipped) return; const roomGeometry = (renderingCanvas.geometry as RoomGeometry); const roomCamera = instanceData.roomCamera; @@ -1403,7 +1429,7 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService const rectangle = visualization.getBoundingRectangle(); const canvas = this.getRoomInstanceRenderingCanvas(roomId, canvasId); - const scale = ((canvas) ? canvas.scale : 1); + const scale = this.getCanvasTransformScale(canvas); const screenPoint = geometry.getScreenPoint(roomObject.getLocation()); if(!screenPoint) return null; @@ -1411,10 +1437,22 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService screenPoint.x = Math.round(screenPoint.x); screenPoint.y = Math.round(screenPoint.y); - rectangle.x = (rectangle.x * scale); - rectangle.y = (rectangle.y * scale); - rectangle.width = (rectangle.width * scale); - rectangle.height = (rectangle.height * scale); + if(scale < 0) + { + // A flipped room projects the far corner of the rectangle first, + // so the far edge becomes the near one and the size stays positive. + rectangle.x = ((rectangle.x + rectangle.width) * scale); + rectangle.y = ((rectangle.y + rectangle.height) * scale); + rectangle.width = (rectangle.width * -scale); + rectangle.height = (rectangle.height * -scale); + } + else + { + rectangle.x = (rectangle.x * scale); + rectangle.y = (rectangle.y * scale); + rectangle.width = (rectangle.width * scale); + rectangle.height = (rectangle.height * scale); + } screenPoint.x = (screenPoint.x * scale); screenPoint.y = (screenPoint.y * scale); @@ -1424,8 +1462,11 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService if(!canvas) return null; - rectangle.x += (Math.round(canvas.width / 2) + canvas.screenOffsetX); - rectangle.y += (Math.round(canvas.height / 2) + canvas.screenOffsetY); + const canvasWidth = (canvas.isFlipped ? -canvas.width : canvas.width); + const canvasHeight = (canvas.isFlipped ? -canvas.height : canvas.height); + + rectangle.x += (Math.round(canvasWidth / 2) + canvas.screenOffsetX); + rectangle.y += (Math.round(canvasHeight / 2) + canvas.screenOffsetY); return rectangle; } @@ -2506,7 +2547,9 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService if(camera) { - if(this.useOffsetScrolling) + // The camera never follows a flipped room, so its + // location is left alone until the flip is undone. + if(this.useOffsetScrolling && !canvas.isFlipped) { if(!camera.isMoving) { @@ -2616,6 +2659,13 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService this._roomObjectEventHandler.modifyRoomObject(this._activeRoomId, objectId, category, operation); } + public rotateActiveObjectPreview(positive: boolean): boolean + { + if(!this._roomObjectEventHandler) return false; + + return this._roomObjectEventHandler.rotateActiveObjectPreview(this._activeRoomId, positive); + } + public modifyRoomObjectDataWithMap(objectId: number, category: number, operation: string, data: Map): boolean { if(!this._roomObjectEventHandler) return false; @@ -2678,11 +2728,16 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService if(!renderingCanvas) return null; - screenPoint.x = (screenPoint.x * renderingCanvas.scale); - screenPoint.y = (screenPoint.y * renderingCanvas.scale); + // A flipped canvas projects through the negative scale and size. + const scale = this.getCanvasTransformScale(renderingCanvas); + const canvasWidth = (renderingCanvas.isFlipped ? -renderingCanvas.width : renderingCanvas.width); + const canvasHeight = (renderingCanvas.isFlipped ? -renderingCanvas.height : renderingCanvas.height); + + screenPoint.x = (screenPoint.x * scale); + screenPoint.y = (screenPoint.y * scale); - screenPoint.x += ((renderingCanvas.width / 2) + renderingCanvas.screenOffsetX); - screenPoint.y += ((renderingCanvas.height / 2) + renderingCanvas.screenOffsetY); + screenPoint.x += ((canvasWidth / 2) + renderingCanvas.screenOffsetX); + screenPoint.y += ((canvasHeight / 2) + renderingCanvas.screenOffsetY); screenPoint.x = Math.round(screenPoint.x); screenPoint.y = Math.round(screenPoint.y); diff --git a/packages/room/src/RoomEngineFlip.test.ts b/packages/room/src/RoomEngineFlip.test.ts new file mode 100644 index 00000000..56d7dd59 --- /dev/null +++ b/packages/room/src/RoomEngineFlip.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, it, vi } from 'vitest'; +import { Rectangle } from 'pixi.js'; +import { RoomEngine } from './RoomEngine'; +import { RoomInstanceData } from './utils'; + +vi.mock('./GetRoomMessageHandler', () => ({ + GetRoomMessageHandler: () => ({}) +})); + +vi.mock('./GetRoomEngine', () => ({ + GetRoomEngine: () => null +})); + +const createFakeCanvas = () => +{ + const canvas = { + width: 800, + height: 600, + scale: 1, + isFlipped: false, + screenOffsetX: 0, + screenOffsetY: 0, + setScale: vi.fn((scale: number) => + { + canvas.scale = scale; + }), + setFlip: vi.fn((flag: boolean) => + { + canvas.isFlipped = flag; + }) + }; + + return canvas; +}; + +const createEngine = (roomId: number, canvas: ReturnType) => +{ + const instanceData = new RoomInstanceData(roomId); + const engine = Object.create(RoomEngine.prototype) as RoomEngine; + + Object.assign(engine, { + _activeRoomId: roomId, + _activeRoomActiveCanvas: 1, + _roomInstanceDatas: new Map([[ roomId, instanceData ]]), + getRoomInstanceRenderingCanvas: () => canvas + }); + + return { engine, instanceData }; +}; + +// A room object standing at room-space screen point (100, 40) whose sprite +// bounding box spans (-20, -60) to (30, 0) around that point. +const stubRoomObject = (engine: RoomEngine) => +{ + Object.assign(engine, { + getRoomInstanceGeometry: () => ({ getScreenPoint: () => ({ x: 100, y: 40 }) }), + getRoomObject: () => ({ + getLocation: () => ({ x: 0, y: 0, z: 0 }), + visualization: { getBoundingRectangle: () => new Rectangle(-20, -60, 50, 60) } + }) + }); +}; + +describe('RoomEngine room flip', () => +{ + it('toggles the canvas flip on every forced flip and exposes it through the getter', () => + { + const roomId = 7; + const canvas = createFakeCanvas(); + const { engine } = createEngine(roomId, canvas); + + expect(engine.getRoomInstanceRenderingCanvasIsFlipped(roomId, 1)).toBe(false); + + engine.setRoomInstanceRenderingCanvasScale(roomId, 1, -1, null, null, true); + + expect(canvas.setFlip).toHaveBeenLastCalledWith(true, null, null); + expect(canvas.setScale).not.toHaveBeenCalled(); + expect(engine.getRoomInstanceRenderingCanvasIsFlipped(roomId, 1)).toBe(true); + expect(engine.getRoomInstanceRenderingCanvasIsFlipped()).toBe(true); + + engine.setRoomInstanceRenderingCanvasScale(roomId, 1, -1, null, null, true); + + expect(canvas.setFlip).toHaveBeenLastCalledWith(false, null, null); + expect(engine.getRoomInstanceRenderingCanvasIsFlipped(roomId, 1)).toBe(false); + expect(canvas.scale).toBe(1); + }); + + it('does not sync the camera to the canvas offset while flipped', () => + { + const roomId = 8; + const canvas = createFakeCanvas(); + const { engine, instanceData } = createEngine(roomId, canvas); + + canvas.isFlipped = true; + canvas.screenOffsetX = -600; + canvas.screenOffsetY = -300; + + engine.setRoomInstanceRenderingCanvasScale(roomId, 1, 2); + + expect(canvas.scale).toBe(2); + expect(instanceData.roomCamera.location).toBeNull(); + }); + + it('snaps discrete levels but passes animated scales through untouched', () => + { + const roomId = 9; + const canvas = createFakeCanvas(); + const { engine } = createEngine(roomId, canvas); + + engine.setRoomInstanceRenderingCanvasScale(roomId, 1, 2.7); + + expect(canvas.scale).toBe(2); + + engine.setRoomInstanceRenderingCanvasScale(roomId, 1, 2.7, null, null, false, true); + + expect(canvas.scale).toBe(2.7); + }); + + it('reports an unflipped canvas when there is no room', () => + { + const engine = Object.create(RoomEngine.prototype) as RoomEngine; + + Object.assign(engine, { _activeRoomId: 1, _activeRoomActiveCanvas: 1, getRoomInstanceRenderingCanvas: () => null }); + + expect(engine.getRoomInstanceRenderingCanvasIsFlipped(1, 1)).toBe(false); + }); + + it('projects object screen locations through the flipped transform', () => + { + const roomId = 10; + const canvas = createFakeCanvas(); + const { engine } = createEngine(roomId, canvas); + + stubRoomObject(engine); + + expect(engine.getRoomObjectScreenLocation(roomId, 1, 10, 1)).toMatchObject({ x: 500, y: 340 }); + + // Flipping mirrors the point through the canvas centre on both axes. + canvas.isFlipped = true; + + expect(engine.getRoomObjectScreenLocation(roomId, 1, 10, 1)).toMatchObject({ x: -500, y: -340 }); + + canvas.screenOffsetX = 800; + canvas.screenOffsetY = 600; + + expect(engine.getRoomObjectScreenLocation(roomId, 1, 10, 1)).toMatchObject({ x: 300, y: 260 }); + }); + + it('keeps flipped bounding rectangles positive and reflected', () => + { + const roomId = 11; + const canvas = createFakeCanvas(); + const { engine } = createEngine(roomId, canvas); + + stubRoomObject(engine); + + expect(engine.getRoomObjectBoundingRectangle(roomId, 1, 10, 1)).toMatchObject({ x: 480, y: 280, width: 50, height: 60 }); + + canvas.isFlipped = true; + canvas.screenOffsetX = 800; + canvas.screenOffsetY = 600; + + // The far corner of the box (30, 0) becomes its near corner once flipped. + expect(engine.getRoomObjectBoundingRectangle(roomId, 1, 10, 1)).toMatchObject({ x: 270, y: 260, width: 50, height: 60 }); + }); +}); diff --git a/packages/room/src/RoomObjectEventHandler.ts b/packages/room/src/RoomObjectEventHandler.ts index 8b8025b6..3e202a68 100644 --- a/packages/room/src/RoomObjectEventHandler.ts +++ b/packages/room/src/RoomObjectEventHandler.ts @@ -128,6 +128,37 @@ export class RoomObjectEventHandler implements IRoomCanvasMouseListener, IRoomOb return true; } + // Rotates the floor item currently being moved or placed without talking to + // the server: the preview direction only becomes real once it is dropped. + public rotateActiveObjectPreview(roomId: number, positive: boolean): boolean + { + if(!this._roomEngine) return false; + + const selectedData = this.getSelectedRoomObjectData(roomId); + + if(!selectedData || (selectedData.category !== RoomObjectCategory.FLOOR)) return false; + + if((selectedData.operation !== RoomObjectOperationType.OBJECT_MOVE) && (selectedData.operation !== RoomObjectOperationType.OBJECT_PLACE)) return false; + + const roomObject = this._roomEngine.getRoomObject(roomId, selectedData.id, selectedData.category); + + if(!roomObject) return false; + + const direction = this.getValidRoomObjectDirection(roomObject, positive); + + if(direction === roomObject.getDirection().x) return false; + + const newDirection = new Vector3d(direction); + + if(!this.isValidLocation(roomObject, newDirection, this._roomEngine.getFurnitureStackingHeightMap(roomId))) return false; + + roomObject.setDirection(newDirection); + + this.updateSelectedObjectData(roomId, selectedData.id, selectedData.category, roomObject.getLocation(), newDirection, selectedData.operation, selectedData.typeId, selectedData.instanceData, selectedData.stuffData, selectedData.state, selectedData.animFrame, selectedData.posture); + + return true; + } + private getMouseEventId(category: number, eventType: string): string { const existing = this._eventIds.get(category); diff --git a/packages/room/src/RoomObjectEventHandlerRotatePreview.test.ts b/packages/room/src/RoomObjectEventHandlerRotatePreview.test.ts new file mode 100644 index 00000000..8865146c --- /dev/null +++ b/packages/room/src/RoomObjectEventHandlerRotatePreview.test.ts @@ -0,0 +1,78 @@ +import { RoomObjectCategory, RoomObjectOperationType } from '@octane/api'; +import { describe, expect, it, vi } from 'vitest'; +import { RoomObjectEventHandler } from './RoomObjectEventHandler'; + +vi.mock('./GetRoomEngine', () => ({ + GetRoomEngine: () => null +})); + +const createHandler = (selectedData: Record, options: { currentDirection?: number; nextDirection?: number; validLocation?: boolean } = {}) => +{ + const { currentDirection = 0, nextDirection = 2, validLocation = true } = options; + const direction = { x: currentDirection, y: 0, z: 0 }; + const roomObject = { + getDirection: () => direction, + getLocation: () => ({ x: 3, y: 4, z: 0 }), + setDirection: vi.fn((vector: { x: number }) => + { + direction.x = vector.x; + }) + }; + const handler = Object.create(RoomObjectEventHandler.prototype) as RoomObjectEventHandler; + const updateSelectedObjectData = vi.fn(); + + Object.assign(handler, { + _roomEngine: { + getRoomObject: () => roomObject, + getFurnitureStackingHeightMap: () => ({}) + }, + getSelectedRoomObjectData: () => selectedData, + getValidRoomObjectDirection: () => nextDirection, + isValidLocation: () => validLocation, + updateSelectedObjectData + }); + + return { handler, roomObject, updateSelectedObjectData }; +}; + +const movingFloorItem = { id: 5, category: RoomObjectCategory.FLOOR, operation: RoomObjectOperationType.OBJECT_MOVE, typeId: 9, instanceData: null, stuffData: null, state: -1, animFrame: -1, posture: null }; + +describe('RoomObjectEventHandler rotateActiveObjectPreview', () => +{ + it('rotates the floor item being moved and refreshes the selection without a server round trip', () => + { + const { handler, roomObject, updateSelectedObjectData } = createHandler(movingFloorItem); + + expect(handler.rotateActiveObjectPreview(1, true)).toBe(true); + + expect(roomObject.setDirection).toHaveBeenCalledWith(expect.objectContaining({ x: 2 })); + expect(updateSelectedObjectData).toHaveBeenCalledWith(1, 5, RoomObjectCategory.FLOOR, { x: 3, y: 4, z: 0 }, expect.objectContaining({ x: 2 }), RoomObjectOperationType.OBJECT_MOVE, 9, null, null, -1, -1, null); + }); + + it('also rotates an item being placed from the inventory', () => + { + const { handler } = createHandler({ ...movingFloorItem, operation: RoomObjectOperationType.OBJECT_PLACE }); + + expect(handler.rotateActiveObjectPreview(1, false)).toBe(true); + }); + + it('ignores wall items and objects that are not being moved or placed', () => + { + expect(createHandler({ ...movingFloorItem, category: RoomObjectCategory.WALL }).handler.rotateActiveObjectPreview(1, true)).toBe(false); + expect(createHandler({ ...movingFloorItem, operation: RoomObjectOperationType.OBJECT_ROTATE_POSITIVE }).handler.rotateActiveObjectPreview(1, true)).toBe(false); + expect(createHandler(null).handler.rotateActiveObjectPreview(1, true)).toBe(false); + }); + + it('leaves the preview alone when the next direction is the current one or does not fit', () => + { + const same = createHandler(movingFloorItem, { currentDirection: 2, nextDirection: 2 }); + + expect(same.handler.rotateActiveObjectPreview(1, true)).toBe(false); + expect(same.roomObject.setDirection).not.toHaveBeenCalled(); + + const blocked = createHandler(movingFloorItem, { validLocation: false }); + + expect(blocked.handler.rotateActiveObjectPreview(1, true)).toBe(false); + expect(blocked.roomObject.setDirection).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/room/src/renderer/RoomSpriteCanvas.ts b/packages/room/src/renderer/RoomSpriteCanvas.ts index 0ebf33ad..3802736b 100644 --- a/packages/room/src/renderer/RoomSpriteCanvas.ts +++ b/packages/room/src/renderer/RoomSpriteCanvas.ts @@ -2,7 +2,7 @@ import { IPlaneVisualization, IRoomCanvasMouseListener, IRoomGeometry, IRoomObje import { GetConfiguration } from '@octane/configuration'; import { RoomSpriteMouseEvent } from '@octane/events'; import { GetTicker, TextureUtils, Vector3d } from '@octane/utils'; -import { Container, Graphics, Matrix, Point, Rectangle, Sprite, Texture } from 'pixi.js'; +import { Container, Graphics, Matrix, Point, PointData, Rectangle, Sprite, Texture } from 'pixi.js'; import { RoomEnterEffect, RoomGeometry, RoomRotatingEffect, RoomShakingEffect } from '../utils'; import { RoomObjectCache, RoomObjectCacheItem } from './cache'; import { getObjectAltitudeDepth } from './ObjectAltitudeDepth'; @@ -24,6 +24,7 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas private _lastBoundaryOffsetY: number = NaN; private _lastBoundaryGeometryId: number = -1; private _lastBoundaryScale: number = NaN; + private _lastBoundaryFlipped: boolean = false; private _sortableSprites: SortableSprite[] = []; private _spriteCount: number = 0; @@ -47,6 +48,7 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas private _eventCache: Map = new Map(); private _eventId: number = 0; private _scale: number = 1; + private _isFlipped: boolean = false; private _SafeStr_4507: boolean = false; private _rotation: number = 0; @@ -245,20 +247,43 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas } } - public setScale(scale: number, point: Point = null, offsetPoint: Point = null, isFlipForced: boolean = false): void + public setScale(scale: number, point: PointData = null, offsetPoint: PointData = null, isFlipForced: boolean = false): void + { + // The zoom scale is always positive; the flip is a separate flag so it + // can be toggled back and zooming keeps working while flipped. + this.setTransform(Math.abs(scale), this._isFlipped, point, offsetPoint); + } + + public setFlip(flag: boolean, point: PointData = null, offsetPoint: PointData = null): void + { + this.setTransform(this._scale, flag, point, offsetPoint); + } + + private setTransform(scale: number, isFlipped: boolean, point: PointData, offsetPoint: PointData): void { if(!this._master || !this._display) return; - if(!point) point = new Point((this._width / 2), (this._height / 2)); + if(!point) point = { x: (this._width / 2), y: (this._height / 2) }; if(!offsetPoint) offsetPoint = point; - point = this._display.toLocal(point); + // Keep the room content under `point` fixed: convert it to display space + // with the old transform, then place it back at `offsetPoint` with the new one. + const localX = ((point.x - this._screenOffsetX) / this.displayScale); + const localY = ((point.y - this._screenOffsetY) / this.displayScale); this._scale = scale; + this._isFlipped = isFlipped; - this.screenOffsetX = (offsetPoint.x - (point.x * this._scale)); - this.screenOffsetY = (offsetPoint.y - (point.y * this._scale)); + this.screenOffsetX = (offsetPoint.x - (localX * this.displayScale)); + this.screenOffsetY = (offsetPoint.y - (localY * this.displayScale)); + } + + // Signed display scale on both axes: the official flip turns the room 180 + // degrees around the anchor point, it is not a horizontal mirror. + private get displayScale(): number + { + return (this._isFlipped ? -this._scale : this._scale); } private updateBoundaryMask(): void @@ -269,13 +294,16 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas const offsetX = this._screenOffsetX; const offsetY = this._screenOffsetY; const scale = this._scale; + const displayScale = this.displayScale; + const flipped = this._isFlipped; - if(geometryId === this._lastBoundaryGeometryId && offsetX === this._lastBoundaryOffsetX && offsetY === this._lastBoundaryOffsetY && scale === this._lastBoundaryScale) return; + if(geometryId === this._lastBoundaryGeometryId && offsetX === this._lastBoundaryOffsetX && offsetY === this._lastBoundaryOffsetY && scale === this._lastBoundaryScale && flipped === this._lastBoundaryFlipped) return; this._lastBoundaryGeometryId = geometryId; this._lastBoundaryOffsetX = offsetX; this._lastBoundaryOffsetY = offsetY; this._lastBoundaryScale = scale; + this._lastBoundaryFlipped = flipped; const pts: { x: number; y: number }[] = []; const w2 = this._width / 2; @@ -312,7 +340,7 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas if(!sp) continue; - pts.push({ x: (sp.x + w2) * scale + offsetX, y: (sp.y + h2) * scale + offsetY }); + pts.push({ x: (sp.x + w2) * displayScale + offsetX, y: (sp.y + h2) * displayScale + offsetY }); } } @@ -420,9 +448,9 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas update = true; } - if(this._display.scale.x !== this._scale) + if((this._display.scale.x !== this.displayScale) || (this._display.scale.y !== this.displayScale)) { - this._display.scale.set(this._scale); + this._display.scale.set(this.displayScale); update = true; } @@ -870,10 +898,26 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas { if(this._noSpriteVisibilityChecking) return true; - x = (((x - this._screenOffsetX) * this._scale) + this._screenOffsetX); - y = (((y - this._screenOffsetY) * this._scale) + this._screenOffsetY); - width = (width * this._scale); - height = (height * this._scale); + const displayScale = this.displayScale; + + x = (((x - this._screenOffsetX) * displayScale) + this._screenOffsetX); + y = (((y - this._screenOffsetY) * displayScale) + this._screenOffsetY); + width = (width * displayScale); + height = (height * displayScale); + + // A flipped room projects the sprite behind its origin, so the span is + // normalised back to a positive size before the bounds test. + if(width < 0) + { + x += width; + width = -width; + } + + if(height < 0) + { + y += height; + height = -height; + } if(((x < this._width) && ((x + width) >= 0)) && ((y < this._height) && ((y + height) >= 0))) { @@ -888,12 +932,16 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas x = (x - this._screenOffsetX); y = (y - this._screenOffsetY); - this._mouseLocation.x = (x / this._scale); - this._mouseLocation.y = (y / this._scale); + // Screen coordinates map back through the signed scale, so a flipped + // room still hit-tests the sprite that is actually under the cursor. + const displayScale = this.displayScale; + + this._mouseLocation.x = (x / displayScale); + this._mouseLocation.y = (y / displayScale); if((this._mouseCheckCount > 0) && (type == MouseEventType.MOUSE_MOVE)) return this._mouseSpriteWasHit; - this._mouseSpriteWasHit = this.checkMouseHits(Math.trunc(x / this._scale), Math.trunc(y / this._scale), type, altKey, ctrlKey, shiftKey, buttonDown); + this._mouseSpriteWasHit = this.checkMouseHits(Math.trunc(x / displayScale), Math.trunc(y / displayScale), type, altKey, ctrlKey, shiftKey, buttonDown); this._mouseCheckCount++; @@ -1074,10 +1122,12 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas this._noSpriteVisibilityChecking = true; const currentScale = this._scale; + const currentFlipped = this._isFlipped; const currentOffsetX = this._screenOffsetX; const currentOffsetY = this._screenOffsetY; - this.setScale(1); + // Captures are always taken unflipped at scale 1. + this.setTransform(1, false, null, null); this._screenOffsetX = 0; this._screenOffsetY = 0; @@ -1093,7 +1143,7 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas this._noSpriteVisibilityChecking = false; - this.setScale(currentScale); + this.setTransform(currentScale, currentFlipped, null, null); this._screenOffsetX = currentOffsetX; this._screenOffsetY = currentOffsetY; @@ -1323,6 +1373,11 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas return this._scale; } + public get isFlipped(): boolean + { + return this._isFlipped; + } + public get width(): number { return (this._width * this._scale); diff --git a/packages/room/src/renderer/RoomSpriteCanvasFlip.test.ts b/packages/room/src/renderer/RoomSpriteCanvasFlip.test.ts new file mode 100644 index 00000000..59dab5cf --- /dev/null +++ b/packages/room/src/renderer/RoomSpriteCanvasFlip.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it, vi } from 'vitest'; +import { RoomSpriteCanvas } from './RoomSpriteCanvas'; + +vi.mock('@octane/utils', () => ({ + GetTicker: () => ({ deltaTime: 1 }), + TextureUtils: {}, + Vector3d: class + { + constructor(public x: number = 0, public y: number = 0, public z: number = 0) + {} + } +})); + +vi.mock('@octane/configuration', () => ({ + GetConfiguration: () => ({ getValue: (_key: string, fallback: unknown) => fallback }) +})); + +vi.mock('../utils', () => ({ + RoomEnterEffect: { isVisualizationOn: () => false }, + RoomGeometry: class + {}, + RoomRotatingEffect: {}, + RoomShakingEffect: {} +})); + +const createCanvas = () => +{ + const canvas = Object.create(RoomSpriteCanvas.prototype) as RoomSpriteCanvas & Record; + + Object.assign(canvas, { + _master: {}, + _display: { scale: { x: 1, y: 1, set(x: number, y: number = x) { this.x = x; this.y = y; } }, x: 0, y: 0 }, + _width: 800, + _height: 600, + _screenOffsetX: 0, + _screenOffsetY: 0, + _scale: 1, + _isFlipped: false, + _mouseLocation: { x: 0, y: 0 }, + _mouseCheckCount: 0, + _mouseSpriteWasHit: false, + _noSpriteVisibilityChecking: false, + _usesExclusionRectangles: false + }); + + return canvas; +}; + +describe('RoomSpriteCanvas flip', () => +{ + it('starts unflipped and reports the flip state through the getter', () => + { + const canvas = createCanvas(); + + expect(canvas.isFlipped).toBe(false); + + canvas.setFlip(true); + + expect(canvas.isFlipped).toBe(true); + expect(canvas.scale).toBe(1); + }); + + it('turns the room 180 degrees around the canvas centre and toggling back restores the offset', () => + { + const canvas = createCanvas(); + + canvas.setFlip(true); + + // The centre pixel stays put: the display origin moves to the far corner. + expect(canvas.screenOffsetX).toBe(800); + expect(canvas.screenOffsetY).toBe(600); + + canvas.setFlip(false); + + expect(canvas.isFlipped).toBe(false); + expect(canvas.screenOffsetX).toBe(0); + expect(canvas.screenOffsetY).toBe(0); + }); + + it('keeps the content under the anchor point fixed when flipping', () => + { + const canvas = createCanvas(); + + canvas.setFlip(true, { x: 200, y: 100 }); + + // Local (200, 100) must still project to screen (200, 100) through the negative scale. + expect((200 * -1) + canvas.screenOffsetX).toBe(200); + expect((100 * -1) + canvas.screenOffsetY).toBe(100); + }); + + it('keeps the zoom scale positive and the flip state when zooming while flipped', () => + { + const canvas = createCanvas(); + + canvas.setFlip(true); + canvas.setScale(2); + + expect(canvas.scale).toBe(2); + expect(canvas.isFlipped).toBe(true); + expect(canvas.width).toBe(1600); + + // A legacy negative scale request is treated as its magnitude. + canvas.setScale(-1); + + expect(canvas.scale).toBe(1); + expect(canvas.isFlipped).toBe(true); + }); + + it('applies the negative display scale to both axes only while flipped', () => + { + const canvas = createCanvas(); + + canvas.setFlip(true); + + expect((canvas as any).displayScale).toBe(-1); + + canvas.setScale(2); + + expect((canvas as any).displayScale).toBe(-2); + + canvas.setFlip(false); + + expect((canvas as any).displayScale).toBe(2); + }); + + it('maps mouse coordinates back through the flipped transform', () => + { + const canvas = createCanvas(); + const seen: number[][] = []; + + Object.assign(canvas, { checkMouseHits: (x: number, y: number) => { seen.push([ x, y ]); return false; } }); + + canvas.setFlip(true); + canvas.handleMouseEvent(100, 50, 'click', false, false, false, false); + + // Screen (100, 50) sits 700 px left and 550 px above the flipped origin at (800, 600). + expect(seen[0]).toEqual([ 700, 550 ]); + expect((canvas as any)._mouseLocation).toEqual({ x: 700, y: 550 }); + }); + + it('tests sprite visibility against the flipped screen span', () => + { + const canvas = createCanvas(); + const isVisible = (x: number, y: number) => (canvas as any).isSpriteVisible(x + canvas.screenOffsetX, y + canvas.screenOffsetY, 50, 50); + + canvas.setFlip(true); + + // Local (100, 100) lands on screen at (650..700, 450..500): inside the canvas. + expect(isVisible(100, 100)).toBe(true); + + // Local (900, 100) lands left of the screen once flipped. + expect(isVisible(900, 100)).toBe(false); + + canvas.setFlip(false); + + expect(isVisible(100, 100)).toBe(true); + expect(isVisible(900, 100)).toBe(false); + }); +}); diff --git a/packages/session/src/handler/RoomUsersHandler.ts b/packages/session/src/handler/RoomUsersHandler.ts index b4a7b423..3e9b2674 100644 --- a/packages/session/src/handler/RoomUsersHandler.ts +++ b/packages/session/src/handler/RoomUsersHandler.ts @@ -162,7 +162,7 @@ export class RoomUsersHandler extends BaseHandler session.userDataManager.setUserBadges(parser.userId, parser.badges); - GetEventDispatcher().dispatchEvent(new RoomSessionUserBadgesEvent(session, parser.userId, parser.badges)); + GetEventDispatcher().dispatchEvent(new RoomSessionUserBadgesEvent(session, parser.userId, parser.badges, parser.badgeDetails)); } private onRoomDoorbellEvent(event: DoorbellMessageEvent): void diff --git a/protocol/packet-field-contracts.json b/protocol/packet-field-contracts.json index daefb620..fb81e606 100644 --- a/protocol/packet-field-contracts.json +++ b/protocol/packet-field-contracts.json @@ -508,6 +508,28 @@ }, "fields": [] }, + { + "name": "ADD_CUSTOM_FILTER_WORD", + "direction": "client_to_server", + "header": 68, + "java": { + "symbol": "AddCustomWordFilterWordEvent", + "className": "AddCustomWordFilterWordEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/users/AddCustomWordFilterWordEvent.java" + }, + "typescript": { + "symbol": "ADD_CUSTOM_FILTER_WORD", + "className": "AddCustomFilterWordMessageComposer", + "path": "packages/communication/src/messages/outgoing/user/wordfilter/AddCustomFilterWordMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "word" + } + ] + }, { "name": "FURNITURE_MULTISTATE", "direction": "client_to_server", @@ -573,6 +595,22 @@ } ] }, + { + "name": "GET_CUSTOM_FILTER", + "direction": "client_to_server", + "header": 145, + "java": { + "symbol": "RequestCustomWordFilterEvent", + "className": "RequestCustomWordFilterEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestCustomWordFilterEvent.java" + }, + "typescript": { + "symbol": "GET_CUSTOM_FILTER", + "className": "GetCustomFilterMessageComposer", + "path": "packages/communication/src/messages/outgoing/user/wordfilter/GetCustomFilterMessageComposer.ts" + }, + "fields": [] + }, { "name": "FURNITURE_WALL_UPDATE", "direction": "client_to_server", @@ -1397,6 +1435,11 @@ "kind": "scalar", "type": "int", "name": "userId" + }, + { + "kind": "scalar", + "type": "boolean", + "name": "block" } ] }, @@ -2256,6 +2299,60 @@ } ] }, + { + "name": "ROOM_SETTINGS_UPDATE_ROOM_CATEGORY_AND_TRADE", + "direction": "client_to_server", + "header": 1265, + "java": { + "symbol": "UpdateRoomCategoryAndTradeSettingsEvent", + "className": "UpdateRoomCategoryAndTradeSettingsEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/UpdateRoomCategoryAndTradeSettingsEvent.java" + }, + "typescript": { + "symbol": "ROOM_SETTINGS_UPDATE_ROOM_CATEGORY_AND_TRADE", + "className": "UpdateRoomCategoryAndTradeSettingsComposer", + "path": "packages/communication/src/messages/outgoing/roomsettings/UpdateRoomCategoryAndTradeSettingsComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "" + }, + { + "kind": "scalar", + "type": "int", + "name": "" + }, + { + "kind": "scalar", + "type": "int", + "name": "" + } + ] + }, + { + "name": "GET_QUIZ_QUESTIONS", + "direction": "client_to_server", + "header": 1296, + "java": { + "symbol": "GetQuizQuestionsEvent", + "className": "GetQuizQuestionsEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/habboway/GetQuizQuestionsEvent.java" + }, + "typescript": { + "symbol": "GET_QUIZ_QUESTIONS", + "className": "GetQuizQuestionsComposer", + "path": "packages/communication/src/messages/outgoing/help/GetQuizQuestionsComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "" + } + ] + }, { "name": "ROOM_KICK", "direction": "client_to_server", @@ -3165,6 +3262,28 @@ } ] }, + { + "name": "REMOVE_CUSTOM_FILTER_WORD", + "direction": "client_to_server", + "header": 1996, + "java": { + "symbol": "RemoveCustomWordFilterWordEvent", + "className": "RemoveCustomWordFilterWordEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RemoveCustomWordFilterWordEvent.java" + }, + "typescript": { + "symbol": "REMOVE_CUSTOM_FILTER_WORD", + "className": "RemoveCustomFilterWordMessageComposer", + "path": "packages/communication/src/messages/outgoing/user/wordfilter/RemoveCustomFilterWordMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "word" + } + ] + }, { "name": "SET_TARGETTED_OFFER_STATE", "direction": "client_to_server", @@ -4644,6 +4763,119 @@ } ] }, + { + "name": "RENTABLE_SPACE_STATUS", + "direction": "client_to_server", + "header": 872, + "java": { + "symbol": "GetRentableSpaceStatusEvent", + "className": "GetRentableSpaceStatusEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/GetRentableSpaceStatusEvent.java" + }, + "typescript": { + "symbol": "RENTABLE_SPACE_STATUS", + "className": "RentableSpaceStatusMessageComposer", + "path": "packages/communication/src/messages/outgoing/room/furniture/RentableSpaceStatusMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "itemId" + } + ] + }, + { + "name": "RENTABLE_GET_RENT_OR_BUYOUT_OFFER", + "direction": "client_to_server", + "header": 2518, + "java": { + "symbol": "GetRentOrBuyoutOfferEvent", + "className": "GetRentOrBuyoutOfferEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentable/GetRentOrBuyoutOfferEvent.java" + }, + "typescript": { + "symbol": "RENTABLE_GET_RENT_OR_BUYOUT_OFFER", + "className": "GetRentOrBuyoutOfferMessageComposer", + "path": "packages/communication/src/messages/outgoing/room/furniture/GetRentOrBuyoutOfferMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "boolean", + "name": "isWallItem" + }, + { + "kind": "scalar", + "type": "string", + "name": "furniTypeName" + }, + { + "kind": "scalar", + "type": "boolean", + "name": "buyout" + } + ] + }, + { + "name": "RENTABLE_EXTEND_RENT_OR_BUYOUT_FURNI", + "direction": "client_to_server", + "header": 1071, + "java": { + "symbol": "ExtendRentOrBuyoutFurniEvent", + "className": "ExtendRentOrBuyoutFurniEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentable/ExtendRentOrBuyoutFurniEvent.java" + }, + "typescript": { + "symbol": "RENTABLE_EXTEND_RENT_OR_BUYOUT_FURNI", + "className": "ExtendRentOrBuyoutFurniMessageComposer", + "path": "packages/communication/src/messages/outgoing/room/furniture/ExtendRentOrBuyoutFurniMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "boolean", + "name": "isWallItem" + }, + { + "kind": "scalar", + "type": "int", + "name": "itemId" + }, + { + "kind": "scalar", + "type": "boolean", + "name": "buyout" + } + ] + }, + { + "name": "RENTABLE_EXTEND_RENT_OR_BUYOUT_STRIP_ITEM", + "direction": "client_to_server", + "header": 2115, + "java": { + "symbol": "ExtendRentOrBuyoutStripItemEvent", + "className": "ExtendRentOrBuyoutStripItemEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentable/ExtendRentOrBuyoutStripItemEvent.java" + }, + "typescript": { + "symbol": "RENTABLE_EXTEND_RENT_OR_BUYOUT_STRIP_ITEM", + "className": "ExtendRentOrBuyoutStripItemMessageComposer", + "path": "packages/communication/src/messages/outgoing/room/furniture/ExtendRentOrBuyoutStripItemMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "stripId" + }, + { + "kind": "scalar", + "type": "boolean", + "name": "buyout" + } + ] + }, { "name": "USER_EFFECT_ACTIVATE", "direction": "client_to_server", @@ -9611,32 +9843,32 @@ { "kind": "scalar", "type": "boolean", - "name": "" + "name": "isWallItem" }, { "kind": "scalar", "type": "string", - "name": "" + "name": "furniTypeName" }, { "kind": "scalar", "type": "boolean", - "name": "" + "name": "buyout" }, { "kind": "scalar", "type": "int", - "name": "" + "name": "priceInCredits" }, { "kind": "scalar", "type": "int", - "name": "" + "name": "priceInActivityPoints" }, { "kind": "scalar", "type": "int", - "name": "" + "name": "activityPointType" } ] }, @@ -10388,6 +10620,31 @@ "kind": "scalar", "type": "int", "name": "" + }, + { + "kind": "scalar", + "type": "boolean", + "name": "wiredWhisperDisabled" + }, + { + "kind": "scalar", + "type": "int", + "name": "chatMode" + }, + { + "kind": "scalar", + "type": "int", + "name": "chatBubbleWidth" + }, + { + "kind": "scalar", + "type": "int", + "name": "chatScrollSpeed" + }, + { + "kind": "scalar", + "type": "int", + "name": "onlineIndicatorPreference" } ] }, @@ -12650,9 +12907,9 @@ "direction": "server_to_client", "header": 1868, "java": { - "symbol": "RentableSpaceUnknown2Composer", - "className": "RentableSpaceUnknown2Composer", - "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/rentablespaces/RentableSpaceUnknown2Composer.java" + "symbol": "RentableSpaceRentFailedComposer", + "className": "RentableSpaceRentFailedComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/rentablespaces/RentableSpaceRentFailedComposer.java" }, "typescript": { "symbol": "RENTABLE_SPACE_RENT_FAILED", @@ -12834,7 +13091,7 @@ { "kind": "scalar", "type": "int", - "name": "" + "name": "reason" } ] }, @@ -12956,9 +13213,9 @@ "direction": "server_to_client", "header": 2046, "java": { - "symbol": "RentableSpaceUnknownComposer", - "className": "RentableSpaceUnknownComposer", - "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/rentablespaces/RentableSpaceUnknownComposer.java" + "symbol": "RentableSpaceRentOkComposer", + "className": "RentableSpaceRentOkComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/rentablespaces/RentableSpaceRentOkComposer.java" }, "typescript": { "symbol": "RENTABLE_SPACE_RENT_OK", @@ -13081,7 +13338,7 @@ { "kind": "scalar", "type": "int", - "name": "" + "name": "expiryTime" } ] }, @@ -14345,28 +14602,6 @@ } ] }, - { - "name": "QUEST_CANCELLED", - "direction": "server_to_client", - "header": 3027, - "java": { - "symbol": "QuestExpiredComposer", - "className": "QuestExpiredComposer", - "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/QuestExpiredComposer.java" - }, - "typescript": { - "symbol": "QUEST_CANCELLED", - "className": "QuestCancelledMessageParser", - "path": "packages/communication/src/messages/parser/quest/QuestCancelledMessageParser.ts" - }, - "fields": [ - { - "kind": "scalar", - "type": "boolean", - "name": "" - } - ] - }, { "name": "MESSENGER_FOLLOW_FAILED", "direction": "server_to_client", @@ -14765,6 +15000,33 @@ } ] }, + { + "name": "MODIFY_CUSTOM_FILTER_RESULT", + "direction": "server_to_client", + "header": 3333, + "java": { + "symbol": "CustomWordFilterModifyResultComposer", + "className": "CustomWordFilterModifyResultComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/CustomWordFilterModifyResultComposer.java" + }, + "typescript": { + "symbol": "MODIFY_CUSTOM_FILTER_RESULT", + "className": "ModifyCustomFilterResultParser", + "path": "packages/communication/src/messages/parser/user/wordfilter/ModifyCustomFilterResultParser.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "result" + }, + { + "kind": "scalar", + "type": "string", + "name": "word" + } + ] + }, { "name": "REDEEM_VOUCHER_OK", "direction": "server_to_client", @@ -15168,32 +15430,32 @@ { "kind": "scalar", "type": "boolean", - "name": "" + "name": "rented" }, { "kind": "scalar", "type": "int", - "name": "" + "name": "canRentErrorCode" }, { "kind": "scalar", "type": "int", - "name": "" + "name": "renterId" }, { "kind": "scalar", "type": "string", - "name": "" + "name": "renterName" }, { "kind": "scalar", "type": "int", - "name": "" + "name": "timeRemaining" }, { "kind": "scalar", "type": "int", - "name": "" + "name": "price" } ] }, @@ -17144,29 +17406,461 @@ "name": "itemId" } ] - } - ], - "unpaired": [ - { - "direction": "client_to_server", - "side": "java", - "header": 295, - "symbol": "PingEvent", - "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/PingEvent.java", - "reason": "Header is registered only by the Polaris Emulator packet registry" - }, - { - "direction": "client_to_server", - "side": "java", - "header": 1523, - "symbol": "RequestFriendsEvent", - "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/RequestFriendsEvent.java", - "reason": "Header is registered only by the Polaris Emulator packet registry" }, { + "name": "GROUP_UNBLOCK_MEMBER", "direction": "client_to_server", - "side": "java", - "header": 2313, + "header": 2864, + "java": { + "symbol": "GuildUnblockMemberEvent", + "className": "GuildUnblockMemberEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildUnblockMemberEvent.java" + }, + "typescript": { + "symbol": "GROUP_UNBLOCK_MEMBER", + "className": "UnblockGroupMemberMessageComposer", + "path": "packages/communication/src/messages/outgoing/user/UnblockGroupMemberMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "guildId" + }, + { + "kind": "scalar", + "type": "int", + "name": "userId" + } + ] + }, + { + "name": "GET_HOT_LOOKS", + "direction": "client_to_server", + "header": 9360, + "java": { + "symbol": "GetHotLooksEvent", + "className": "GetHotLooksEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/hotlooks/GetHotLooksEvent.java" + }, + "typescript": { + "symbol": "GET_HOT_LOOKS", + "className": "GetHotLooksComposer", + "path": "packages/communication/src/messages/outgoing/hotlooks/GetHotLooksComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "count" + } + ] + }, + { + "name": "GET_QUESTS", + "direction": "client_to_server", + "header": 3333, + "java": { + "symbol": "GetQuestsEvent", + "className": "GetQuestsEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/GetQuestsEvent.java" + }, + "typescript": { + "symbol": "GET_QUESTS", + "className": "GetQuestsMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/GetQuestsMessageComposer.ts" + }, + "fields": [] + }, + { + "name": "GET_SEASONAL_QUESTS_ONLY", + "direction": "client_to_server", + "header": 1190, + "java": { + "symbol": "GetSeasonalQuestsOnlyEvent", + "className": "GetSeasonalQuestsOnlyEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/GetSeasonalQuestsOnlyEvent.java" + }, + "typescript": { + "symbol": "GET_SEASONAL_QUESTS_ONLY", + "className": "GetSeasonalQuestsOnlyMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/GetSeasonalQuestsOnlyMessageComposer.ts" + }, + "fields": [] + }, + { + "name": "ACCEPT_QUEST", + "direction": "client_to_server", + "header": 3604, + "java": { + "symbol": "AcceptQuestEvent", + "className": "AcceptQuestEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/AcceptQuestEvent.java" + }, + "typescript": { + "symbol": "ACCEPT_QUEST", + "className": "AcceptQuestMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/AcceptQuestMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "questId" + } + ] + }, + { + "name": "ACTIVATE_QUEST", + "direction": "client_to_server", + "header": 793, + "java": { + "symbol": "ActivateQuestEvent", + "className": "ActivateQuestEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/ActivateQuestEvent.java" + }, + "typescript": { + "symbol": "ACTIVATE_QUEST", + "className": "ActivateQuestMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/ActivateQuestMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "questId" + } + ] + }, + { + "name": "REJECT_QUEST", + "direction": "client_to_server", + "header": 2397, + "java": { + "symbol": "RejectQuestEvent", + "className": "RejectQuestEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/RejectQuestEvent.java" + }, + "typescript": { + "symbol": "REJECT_QUEST", + "className": "RejectQuestMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/RejectQuestMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "questId" + } + ] + }, + { + "name": "CANCEL_QUEST", + "direction": "client_to_server", + "header": 3133, + "java": { + "symbol": "CancelDailyQuestEvent", + "className": "CancelDailyQuestEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/CancelDailyQuestEvent.java" + }, + "typescript": { + "symbol": "CANCEL_QUEST", + "className": "CancelQuestMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/CancelQuestMessageComposer.ts" + }, + "fields": [] + }, + { + "name": "GET_DAILY_QUEST", + "direction": "client_to_server", + "header": 2486, + "java": { + "symbol": "GetDailyQuestEvent", + "className": "GetDailyQuestEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/GetDailyQuestEvent.java" + }, + "typescript": { + "symbol": "GET_DAILY_QUEST", + "className": "GetDailyQuestMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/GetDailyQuestMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "boolean", + "name": "easy" + }, + { + "kind": "scalar", + "type": "int", + "name": "index" + } + ] + }, + { + "name": "OPEN_QUEST_TRACKER", + "direction": "client_to_server", + "header": 2750, + "java": { + "symbol": "OpenQuestTrackerEvent", + "className": "OpenQuestTrackerEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/OpenQuestTrackerEvent.java" + }, + "typescript": { + "symbol": "OPEN_QUEST_TRACKER", + "className": "OpenQuestTrackerMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/OpenQuestTrackerMessageComposer.ts" + }, + "fields": [] + }, + { + "name": "START_CAMPAIGN", + "direction": "client_to_server", + "header": 1697, + "java": { + "symbol": "StartCampaignEvent", + "className": "StartCampaignEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/StartCampaignEvent.java" + }, + "typescript": { + "symbol": "START_CAMPAIGN", + "className": "StartCampaignMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/StartCampaignMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "campaignCode" + } + ] + }, + { + "name": "GET_DAILY_TASKS", + "direction": "client_to_server", + "header": 4100, + "java": { + "symbol": "GetDailyTasksEvent", + "className": "GetDailyTasksEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/GetDailyTasksEvent.java" + }, + "typescript": { + "symbol": "GET_DAILY_TASKS", + "className": "GetDailyTasksMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/GetDailyTasksMessageComposer.ts" + }, + "fields": [] + }, + { + "name": "CLAIM_DAILY_TASK", + "direction": "client_to_server", + "header": 4101, + "java": { + "symbol": "ClaimDailyTaskEvent", + "className": "ClaimDailyTaskEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/ClaimDailyTaskEvent.java" + }, + "typescript": { + "symbol": "CLAIM_DAILY_TASK", + "className": "ClaimDailyTaskMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/ClaimDailyTaskMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "int", + "name": "taskId" + } + ] + }, + { + "name": "GET_REWARD_TRACKS", + "direction": "client_to_server", + "header": 9450, + "java": { + "symbol": "GetRewardTracksEvent", + "className": "GetRewardTracksEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/GetRewardTracksEvent.java" + }, + "typescript": { + "symbol": "GET_REWARD_TRACKS", + "className": "GetRewardTracksMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/GetRewardTracksMessageComposer.ts" + }, + "fields": [] + }, + { + "name": "CLAIM_REWARD_TRACK_PRIZE", + "direction": "client_to_server", + "header": 1111, + "java": { + "symbol": "ClaimRewardTrackPrizeEvent", + "className": "ClaimRewardTrackPrizeEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/ClaimRewardTrackPrizeEvent.java" + }, + "typescript": { + "symbol": "CLAIM_REWARD_TRACK_PRIZE", + "className": "ClaimRewardTrackPrizeMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/ClaimRewardTrackPrizeMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "trackId" + }, + { + "kind": "scalar", + "type": "string", + "name": "rewardId" + } + ] + }, + { + "name": "PURCHASE_REWARD_TRACK_PREMIUM", + "direction": "client_to_server", + "header": 3022, + "java": { + "symbol": "PurchaseRewardTrackPremiumEvent", + "className": "PurchaseRewardTrackPremiumEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/quests/PurchaseRewardTrackPremiumEvent.java" + }, + "typescript": { + "symbol": "PURCHASE_REWARD_TRACK_PREMIUM", + "className": "PurchaseRewardTrackPremiumMessageComposer", + "path": "packages/communication/src/messages/outgoing/quest/PurchaseRewardTrackPremiumMessageComposer.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "trackId" + } + ] + }, + { + "name": "REWARD_TRACK_CLAIM_RESULT", + "direction": "server_to_client", + "header": 9451, + "java": { + "symbol": "RewardTrackClaimResultComposer", + "className": "RewardTrackClaimResultComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/RewardTrackClaimResultComposer.java" + }, + "typescript": { + "symbol": "REWARD_TRACK_CLAIM_RESULT", + "className": "RewardTrackClaimResultMessageParser", + "path": "packages/communication/src/messages/parser/quest/RewardTrackClaimResultMessageParser.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "trackId" + }, + { + "kind": "scalar", + "type": "string", + "name": "rewardId" + }, + { + "kind": "scalar", + "type": "int", + "name": "resultCode" + } + ] + }, + { + "name": "REWARD_TRACK_PROGRESS", + "direction": "server_to_client", + "header": 9452, + "java": { + "symbol": "RewardTrackProgressComposer", + "className": "RewardTrackProgressComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/RewardTrackProgressComposer.java" + }, + "typescript": { + "symbol": "REWARD_TRACK_PROGRESS", + "className": "RewardTrackProgressMessageParser", + "path": "packages/communication/src/messages/parser/quest/RewardTrackProgressMessageParser.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "trackId" + }, + { + "kind": "scalar", + "type": "string", + "name": "taskId" + }, + { + "kind": "scalar", + "type": "int", + "name": "progressCount" + }, + { + "kind": "scalar", + "type": "int", + "name": "points" + } + ] + }, + { + "name": "REWARD_TRACK_PREMIUM_PURCHASE_RESULT", + "direction": "server_to_client", + "header": 2248, + "java": { + "symbol": "RewardTrackPremiumPurchaseResultComposer", + "className": "RewardTrackPremiumPurchaseResultComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/RewardTrackPremiumPurchaseResultComposer.java" + }, + "typescript": { + "symbol": "REWARD_TRACK_PREMIUM_PURCHASE_RESULT", + "className": "RewardTrackPremiumPurchaseResultMessageParser", + "path": "packages/communication/src/messages/parser/quest/RewardTrackPremiumPurchaseResultMessageParser.ts" + }, + "fields": [ + { + "kind": "scalar", + "type": "string", + "name": "trackId" + }, + { + "kind": "scalar", + "type": "int", + "name": "resultCode" + }, + { + "kind": "scalar", + "type": "int", + "name": "points" + } + ] + } + ], + "unpaired": [ + { + "direction": "client_to_server", + "side": "java", + "header": 295, + "symbol": "PingEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/PingEvent.java", + "reason": "Header is registered only by the Polaris Emulator packet registry" + }, + { + "direction": "client_to_server", + "side": "java", + "header": 1523, + "symbol": "RequestFriendsEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/RequestFriendsEvent.java", + "reason": "Header is registered only by the Polaris Emulator packet registry" + }, + { + "direction": "client_to_server", + "side": "java", + "header": 2313, "symbol": "UpdateUIFlagsEvent", "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UpdateUIFlagsEvent.java", "reason": "Header is registered only by the Polaris Emulator packet registry" @@ -17435,14 +18129,6 @@ "path": "packages/communication/src/messages/outgoing/gifts/TryPhoneNumberMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 793, - "symbol": "ACTIVATE_QUEST", - "path": "packages/communication/src/messages/outgoing/quest/ActivateQuestMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17459,14 +18145,6 @@ "path": "packages/communication/src/messages/outgoing/competition/ForwardToRandomCompetitionRoomMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 872, - "symbol": "RENTABLE_SPACE_STATUS", - "path": "packages/communication/src/messages/outgoing/room/furniture/RentableSpaceStatusMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17491,14 +18169,6 @@ "path": "packages/communication/src/messages/outgoing/handshake/VersionCheckMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 1071, - "symbol": "RENTABLE_EXTEND_RENT_OR_BUYOUT_FURNI", - "path": "packages/communication/src/messages/outgoing/room/furniture/ExtendRentOrBuyoutFurniMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17531,14 +18201,6 @@ "path": "packages/communication/src/messages/outgoing/userclassification/PeerUsersClassificationMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 1190, - "symbol": "GET_SEASONAL_QUESTS_ONLY", - "path": "packages/communication/src/messages/outgoing/quest/GetSeasonalQuestsOnlyMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17555,22 +18217,6 @@ "path": "packages/communication/src/messages/outgoing/game/score/Game2GetWeeklyFriendsLeaderboardComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 1265, - "symbol": "ROOM_SETTINGS_UPDATE_ROOM_CATEGORY_AND_TRADE", - "path": "packages/communication/src/messages/outgoing/roomsettings/UpdateRoomCategoryAndTradeSettingsComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 1296, - "symbol": "GET_QUIZ_QUESTIONS", - "path": "packages/communication/src/messages/outgoing/help/GetQuizQuestionsComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17659,14 +18305,6 @@ "path": "packages/communication/src/messages/outgoing/moderation/DefaultSanctionMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 1697, - "symbol": "START_CAMPAIGN", - "path": "packages/communication/src/messages/outgoing/quest/StartCampaignMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17707,14 +18345,6 @@ "path": "packages/communication/src/messages/outgoing/competition/GetIsUserPartOfCompetitionMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 2115, - "symbol": "RENTABLE_EXTEND_RENT_OR_BUYOUT_STRIP_ITEM", - "path": "packages/communication/src/messages/outgoing/room/furniture/ExtendRentOrBuyoutStripItemMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17771,14 +18401,6 @@ "path": "packages/communication/src/messages/outgoing/game/lobby/LeaveQueueMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 2397, - "symbol": "REJECT_QUEST", - "path": "packages/communication/src/messages/outgoing/quest/RejectQuestMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17819,14 +18441,6 @@ "path": "packages/communication/src/messages/outgoing/navigator/UpdateRoomThumbnailMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 2486, - "symbol": "GET_DAILY_QUEST", - "path": "packages/communication/src/messages/outgoing/quest/GetDailyQuestMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17843,14 +18457,6 @@ "path": "packages/communication/src/messages/outgoing/game/arena/Game2GameChatMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 2518, - "symbol": "RENTABLE_GET_RENT_OR_BUYOUT_OFFER", - "path": "packages/communication/src/messages/outgoing/room/furniture/GetRentOrBuyoutOfferMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17955,14 +18561,6 @@ "path": "packages/communication/src/messages/outgoing/gifts/ResetPhoneNumberStateMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 2750, - "symbol": "OPEN_QUEST_TRACKER", - "path": "packages/communication/src/messages/outgoing/quest/OpenQuestTrackerMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -17987,14 +18585,6 @@ "path": "packages/communication/src/messages/outgoing/navigator/RoomAdSearchMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 2864, - "symbol": "GROUP_UNBLOCK_MEMBER", - "path": "packages/communication/src/messages/outgoing/user/UnblockGroupMemberMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -18027,14 +18617,6 @@ "path": "packages/communication/src/messages/outgoing/room/session/ChangeQueueMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 3133, - "symbol": "CANCEL_QUEST", - "path": "packages/communication/src/messages/outgoing/quest/CancelQuestMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -18099,14 +18681,6 @@ "path": "packages/communication/src/messages/outgoing/user/data/IgnoreUserIdComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 3333, - "symbol": "GET_QUESTS", - "path": "packages/communication/src/messages/outgoing/quest/GetQuestsMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -18147,14 +18721,6 @@ "path": "packages/communication/src/messages/outgoing/landingview/votes/CommunityGoalVoteMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 3604, - "symbol": "ACCEPT_QUEST", - "path": "packages/communication/src/messages/outgoing/quest/AcceptQuestMessageComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -18163,14 +18729,6 @@ "path": "packages/communication/src/messages/outgoing/help/DeletePendingCallsForHelpMessageComposer.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "client_to_server", - "side": "typescript", - "header": 3720, - "symbol": "POST_QUIZ_ANSWERS", - "path": "packages/communication/src/messages/outgoing/help/PostQuizAnswersComposer.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "client_to_server", "side": "typescript", @@ -18315,14 +18873,6 @@ "path": "packages/communication/src/messages/parser/game/lobby/GameInviteMessageParser.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "server_to_client", - "side": "typescript", - "header": 1122, - "symbol": "SEASONAL_QUESTS", - "path": "packages/communication/src/messages/parser/quest/SeasonalQuestsParser.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "server_to_client", "side": "typescript", @@ -18363,14 +18913,6 @@ "path": "packages/communication/src/messages/parser/game/directory/Game2JoiningGameFailedMessageParser.ts", "reason": "Header is registered only by the Octane Renderer packet registry" }, - { - "direction": "server_to_client", - "side": "typescript", - "header": 1878, - "symbol": "QUEST_DAILY", - "path": "packages/communication/src/messages/parser/quest/QuestDailyMessageParser.ts", - "reason": "Header is registered only by the Octane Renderer packet registry" - }, { "direction": "server_to_client", "side": "typescript", @@ -19813,6 +20355,22 @@ }, "reason": "Java analyzer: Data-dependent packet operations in IfStmt inside handle; TypeScript analyzer: Outgoing message array _data is mutated with push" }, + { + "name": "POST_QUIZ_ANSWERS", + "direction": "client_to_server", + "header": 3720, + "java": { + "symbol": "PostQuizAnswersEvent", + "className": "PostQuizAnswersEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/habboway/PostQuizAnswersEvent.java" + }, + "typescript": { + "symbol": "POST_QUIZ_ANSWERS", + "className": "PostQuizAnswersComposer", + "path": "packages/communication/src/messages/outgoing/help/PostQuizAnswersComposer.ts" + }, + "reason": "Java analyzer: Data-dependent packet operations in ForStmt inside handle; TypeScript analyzer: Outgoing value answerIds.length has no statically resolvable wire type" + }, { "name": "MODTOOL_SANCTION_TRADELOCK", "direction": "client_to_server", @@ -23205,6 +23763,22 @@ }, "reason": "TypeScript analyzer: Data-dependent packet operations in IfStatement inside parse" }, + { + "name": "CUSTOM_FILTER_RESULT", + "direction": "server_to_client", + "header": 3883, + "java": { + "symbol": "CustomWordFilterWordsComposer", + "className": "CustomWordFilterWordsComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/CustomWordFilterWordsComposer.java" + }, + "typescript": { + "symbol": "CUSTOM_FILTER_RESULT", + "className": "CustomFilterResultParser", + "path": "packages/communication/src/messages/parser/user/wordfilter/CustomFilterResultParser.ts" + }, + "reason": "Java analyzer: Data-dependent packet operations in ForEachStmt inside composeInternal; TypeScript analyzer: Data-dependent packet operations in WhileStatement inside parse" + }, { "name": "MARKETPLACE_OWN_ITEMS", "direction": "server_to_client", @@ -24356,6 +24930,182 @@ "path": "packages/communication/src/messages/parser/catalog/metadata/CatalogProductMetadataMessageParser.ts" }, "reason": "Java analyzer: Data-dependent packet operations in ForStmt inside composeInternal; TypeScript analyzer: Data-dependent packet operations in ForStatement inside parse" + }, + { + "name": "USER_SETTINGS_CHAT_PREFERENCES", + "direction": "client_to_server", + "header": 2506, + "java": { + "symbol": "SaveChatPreferencesEvent", + "className": "SaveChatPreferencesEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveChatPreferencesEvent.java" + }, + "typescript": { + "symbol": "USER_SETTINGS_CHAT_PREFERENCES", + "className": "UserSettingsChatPreferencesComposer", + "path": "packages/communication/src/messages/outgoing/user/settings/UserSettingsChatPreferencesComposer.ts" + }, + "reason": "Java analyzer: Packet fields delegated to external reader in UserPreferencePackets inside handle" + }, + { + "name": "USER_SETTINGS_ONLINE_INDICATOR", + "direction": "client_to_server", + "header": 818, + "java": { + "symbol": "SaveOnlineIndicatorPreferenceEvent", + "className": "SaveOnlineIndicatorPreferenceEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveOnlineIndicatorPreferenceEvent.java" + }, + "typescript": { + "symbol": "USER_SETTINGS_ONLINE_INDICATOR", + "className": "UserSettingsOnlineIndicatorComposer", + "path": "packages/communication/src/messages/outgoing/user/settings/UserSettingsOnlineIndicatorComposer.ts" + }, + "reason": "Java analyzer: Packet fields delegated to external reader in UserPreferencePackets inside handle" + }, + { + "name": "WIRED_MENU_SETTINGS_SAVE", + "direction": "client_to_server", + "header": 1226, + "java": { + "symbol": "SaveWiredMenuSettingsEvent", + "className": "SaveWiredMenuSettingsEvent", + "path": "Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveWiredMenuSettingsEvent.java" + }, + "typescript": { + "symbol": "WIRED_MENU_SETTINGS_SAVE", + "className": "WiredMenuSettingsComposer", + "path": "packages/communication/src/messages/outgoing/roomevents/WiredMenuSettingsComposer.ts" + }, + "reason": "Java analyzer: Packet fields delegated to external reader in UserPreferencePackets inside handle" + }, + { + "name": "HOT_LOOKS", + "direction": "server_to_client", + "header": 9360, + "java": { + "symbol": "HotLooksComposer", + "className": "HotLooksComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/hotlooks/HotLooksComposer.java" + }, + "typescript": { + "symbol": "HOT_LOOKS", + "className": "HotLooksParser", + "path": "packages/communication/src/messages/parser/hotlooks/HotLooksParser.ts" + }, + "reason": "Java analyzer: Data-dependent packet operations in ForEachStmt inside composeInternal; TypeScript analyzer: Data-dependent packet operations in ForStatement inside parse" + }, + { + "name": "QUEST_CANCELLED", + "direction": "server_to_client", + "header": 3027, + "java": { + "symbol": "QuestExpiredComposer", + "className": "QuestExpiredComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/QuestExpiredComposer.java" + }, + "typescript": { + "symbol": "QUEST_CANCELLED", + "className": "QuestCancelledMessageParser", + "path": "packages/communication/src/messages/parser/quest/QuestCancelledMessageParser.ts" + }, + "reason": "Java analyzer: Packet fields delegated to external serializer serialize inside composeInternal; TypeScript analyzer: Packet fields delegated to external constructor QuestMessageData inside parse" + }, + { + "name": "SEASONAL_QUESTS", + "direction": "server_to_client", + "header": 1122, + "java": { + "symbol": "SeasonalQuestsComposer", + "className": "SeasonalQuestsComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/SeasonalQuestsComposer.java" + }, + "typescript": { + "symbol": "SEASONAL_QUESTS", + "className": "SeasonalQuestsParser", + "path": "packages/communication/src/messages/parser/quest/SeasonalQuestsParser.ts" + }, + "reason": "Java analyzer: Data-dependent packet operations in ForEachStmt inside composeInternal; TypeScript analyzer: Data-dependent packet operations in ForStatement inside parse" + }, + { + "name": "QUEST_DAILY", + "direction": "server_to_client", + "header": 1878, + "java": { + "symbol": "DailyQuestComposer", + "className": "DailyQuestComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/DailyQuestComposer.java" + }, + "typescript": { + "symbol": "QUEST_DAILY", + "className": "QuestDailyMessageParser", + "path": "packages/communication/src/messages/parser/quest/QuestDailyMessageParser.ts" + }, + "reason": "Java analyzer: Packet fields delegated to external serializer serialize inside composeInternal; TypeScript analyzer: Packet fields delegated to external constructor QuestMessageData inside parse" + }, + { + "name": "ACTIVE_DAILY_TASKS", + "direction": "server_to_client", + "header": 2900, + "java": { + "symbol": "ActiveDailyTasksComposer", + "className": "ActiveDailyTasksComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/ActiveDailyTasksComposer.java" + }, + "typescript": { + "symbol": "ACTIVE_DAILY_TASKS", + "className": "ActiveDailyTasksMessageParser", + "path": "packages/communication/src/messages/parser/quest/ActiveDailyTasksMessageParser.ts" + }, + "reason": "Java analyzer: Data-dependent packet operations in ForEachStmt inside composeInternal; TypeScript analyzer: Data-dependent packet operations in ForStatement inside parse" + }, + { + "name": "DAILY_TASKS_ADDED", + "direction": "server_to_client", + "header": 670, + "java": { + "symbol": "DailyTasksAddedComposer", + "className": "DailyTasksAddedComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/DailyTasksAddedComposer.java" + }, + "typescript": { + "symbol": "DAILY_TASKS_ADDED", + "className": "ActiveDailyTasksMessageParser", + "path": "packages/communication/src/messages/parser/quest/ActiveDailyTasksMessageParser.ts" + }, + "reason": "Java analyzer: Data-dependent packet operations in ForEachStmt inside composeInternal; TypeScript analyzer: Data-dependent packet operations in ForStatement inside parse" + }, + { + "name": "DAILY_TASK_UPDATED", + "direction": "server_to_client", + "header": 9450, + "java": { + "symbol": "DailyTaskUpdatedComposer", + "className": "DailyTaskUpdatedComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/DailyTaskUpdatedComposer.java" + }, + "typescript": { + "symbol": "DAILY_TASK_UPDATED", + "className": "DailyTaskUpdatedMessageParser", + "path": "packages/communication/src/messages/parser/quest/DailyTaskUpdatedMessageParser.ts" + }, + "reason": "TypeScript analyzer: the 64-bit task id is read through the external helper DailyTaskData.readLong inside parse; the Java side writes it as two ints" + }, + { + "name": "REWARD_TRACKS", + "direction": "server_to_client", + "header": 2327, + "java": { + "symbol": "RewardTracksComposer", + "className": "RewardTracksComposer", + "path": "Emulator/src/main/java/com/eu/habbo/messages/outgoing/quests/RewardTracksComposer.java" + }, + "typescript": { + "symbol": "REWARD_TRACKS", + "className": "RewardTracksMessageParser", + "path": "packages/communication/src/messages/parser/quest/RewardTracksMessageParser.ts" + }, + "reason": "Java analyzer: Data-dependent packet operations in ForEachStmt inside composeInternal; TypeScript analyzer: Data-dependent packet operations in ForStatement inside parse" } ] }