Merge branch 'Dev' into merge-duckie-main-2026-05-06

This commit is contained in:
DuckieTM
2026-05-25 18:48:34 +02:00
committed by GitHub
77 changed files with 1559 additions and 201 deletions
@@ -203,6 +203,17 @@ export class CommunicationManager implements ICommunicationManager
this._connection.removeMessageEvent(event);
}
public subscribeMessage<T extends IMessageEvent>(eventCtor: new (callback: (event: T) => void) => T, handler: (event: T) => void): () => void
{
if(!eventCtor || !handler) return () => {};
const event = new eventCtor(handler);
this.registerMessageEvent(event);
return () => this.removeMessageEvent(event);
}
public get connection(): IConnection
{
return this._connection;
@@ -1,4 +1,4 @@
import { ICodec, IConnection, IMessageComposer, IMessageConfiguration, IMessageDataWrapper, IMessageEvent, WebSocketEventEnum } from '@nitrots/api';
import { ICodec, IConnection, IMessageComposer, IMessageConfiguration, IMessageDataWrapper, IMessageEvent, IMessageParser, WebSocketEventEnum } from '@nitrots/api';
import { GetConfiguration } from '@nitrots/configuration';
import { GetEventDispatcher, NitroEvent, NitroEventType, ReconnectEvent } from '@nitrots/events';
import { NitroLogger } from '@nitrots/utils';
@@ -509,7 +509,7 @@ export class SocketConnection implements IConnection
try
{
const parser = new events[0].parserClass();
const parser = new (events[0].parserClass as new () => IMessageParser)();
if(!parser || !parser.flush() || !parser.parse(wrapper)) return null;
@@ -38,17 +38,17 @@ export async function deriveAesKey(sharedSecret: ArrayBuffer): Promise<CryptoKey
);
}
export async function aesGcmEncrypt(key: CryptoKey, nonce: Uint8Array, plaintext: ArrayBuffer): Promise<ArrayBuffer>
export async function aesGcmEncrypt(key: CryptoKey, nonce: Uint8Array<ArrayBuffer>, plaintext: ArrayBuffer): Promise<ArrayBuffer>
{
return window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce, tagLength: GCM_TAG_LEN * 8 }, key, plaintext);
}
export async function aesGcmDecrypt(key: CryptoKey, nonce: Uint8Array, ciphertextWithTag: ArrayBuffer): Promise<ArrayBuffer>
export async function aesGcmDecrypt(key: CryptoKey, nonce: Uint8Array<ArrayBuffer>, ciphertextWithTag: ArrayBuffer): Promise<ArrayBuffer>
{
return window.crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce, tagLength: GCM_TAG_LEN * 8 }, key, ciphertextWithTag);
}
export function randomNonce(): Uint8Array
export function randomNonce(): Uint8Array<ArrayBuffer>
{
const n = new Uint8Array(NONCE_LEN);
window.crypto.getRandomValues(n);
@@ -4,9 +4,9 @@ export class CatalogAdminSavePageComposer implements IMessageComposer<Constructo
{
private _data: ConstructorParameters<typeof CatalogAdminSavePageComposer>;
constructor(pageId: number, caption: string, caption2: string, layout: string, iconType: number, minRank: number, visible: boolean, enabled: boolean, orderNum: number, parentId: number, headline: string, teaser: string, textDetails: string, targetCatalogType: string, catalogMode: string = 'NORMAL')
constructor(pageId: number, caption: string, caption2: string, layout: string, iconType: number, minRank: number, visible: boolean, enabled: boolean, orderNum: number, parentId: number, headline: string, teaser: string, textDetails: string, targetCatalogType: string, catalogMode: string = 'NORMAL', pageText1: string = '')
{
this._data = [ pageId, caption, caption2, layout, iconType, minRank, visible, enabled, orderNum, parentId, headline, teaser, textDetails, targetCatalogType, catalogMode ];
this._data = [ pageId, caption, caption2, layout, iconType, minRank, visible, enabled, orderNum, parentId, headline, teaser, textDetails, targetCatalogType, catalogMode, pageText1 ];
}
dispose(): void
@@ -1,12 +1,23 @@
import { IMessageComposer } from '@nitrots/api';
export class RoomEnterComposer implements IMessageComposer<ConstructorParameters<typeof RoomEnterComposer>>
{
private _data: ConstructorParameters<typeof RoomEnterComposer>;
type RoomEnterPayload = [ number, string, number?, number? ];
constructor(roomId: number, password: string = null)
export class RoomEnterComposer implements IMessageComposer<RoomEnterPayload>
{
private _data: RoomEnterPayload;
/**
* Optional spawnX/spawnY let the server resume the avatar at a
* specific tile when re-entering the same room — used by the
* reconnect flow. Arcturus' RequestRoomLoadEvent reads both ints
* only if `packet.remaining >= 8`, so omitting them keeps the
* legacy enter-via-door behavior.
*/
constructor(roomId: number, password: string = null, spawnX?: number, spawnY?: number)
{
this._data = [roomId, password];
this._data = (spawnX !== undefined && spawnY !== undefined)
? [ roomId, password, spawnX, spawnY ]
: [ roomId, password ];
}
public getMessageArray()
@@ -32,7 +32,8 @@ implements
chatBubbleWeight: number,
chatBubbleSpeed: number,
chatDistance: number,
chatFloodProtection: number
chatFloodProtection: number,
allowUnderpass?: boolean
)
{
//@ts-ignore
@@ -67,6 +68,8 @@ implements
chatDistance,
chatFloodProtection
);
if(allowUnderpass !== undefined) this._data.push(allowUnderpass);
}
public getMessageArray()
@@ -4,9 +4,9 @@ export class RoomUnitBackgroundComposer implements IMessageComposer<ConstructorP
{
private _data: ConstructorParameters<typeof RoomUnitBackgroundComposer>;
constructor(backgroundImage: number, backgroundStand: number, backgroundOverlay: number, backgroundCard: number = 0)
constructor(backgroundImage: number, backgroundStand: number, backgroundOverlay: number, backgroundCard: number = 0, backgroundBorder: number = 0)
{
this._data = [ backgroundImage, backgroundStand, backgroundOverlay, backgroundCard ];
this._data = [ backgroundImage, backgroundStand, backgroundOverlay, backgroundCard, backgroundBorder ];
}
public getMessageArray()
@@ -1,8 +1,8 @@
import { IMessageComposer } from '@nitrots/api';
export class WiredRoomSettingsRequestComposer implements IMessageComposer<ConstructorParameters<typeof WiredRoomSettingsRequestComposer>>
export class WiredRoomSettingsRequestComposer implements IMessageComposer<[]>
{
public getMessageArray()
public getMessageArray(): []
{
return [];
}
@@ -1,8 +1,8 @@
import { IMessageComposer } from '@nitrots/api';
export class WiredUserVariablesRequestComposer implements IMessageComposer<ConstructorParameters<typeof WiredUserVariablesRequestComposer>>
export class WiredUserVariablesRequestComposer implements IMessageComposer<[]>
{
public getMessageArray()
public getMessageArray(): []
{
return [];
}
@@ -5,6 +5,7 @@ export class NodeData
private _visible: boolean;
private _icon: number;
private _pageId: number;
private _parentId: number;
private _pageName: string;
private _localization: string;
private _children: NodeData[];
@@ -23,6 +24,7 @@ export class NodeData
this._visible = false;
this._icon = 0;
this._pageId = -1;
this._parentId = -1;
this._pageName = null;
this._localization = null;
this._children = [];
@@ -43,6 +45,7 @@ export class NodeData
this._visible = wrapper.readBoolean();
this._icon = wrapper.readInt();
this._pageId = wrapper.readInt();
this._parentId = wrapper.readInt();
this._pageName = wrapper.readString();
this._localization = wrapper.readString();
@@ -92,6 +95,11 @@ export class NodeData
return this._pageId;
}
public get parentId(): number
{
return this._parentId;
}
public get pageName(): string
{
return this._pageName;
@@ -19,17 +19,16 @@ export class PetBreedingMessageParser implements IMessageParser
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean {
if (!wrapper || wrapper.bytesAvailable < 12) {
return false;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper || !wrapper.bytesAvailable) return false;
this._state = wrapper.readInt();
this._ownPetId = wrapper.readInt();
this._otherPetId = wrapper.readInt();
this._state = wrapper.readInt();
this._ownPetId = wrapper.readInt();
this._otherPetId = wrapper.readInt();
return true;
}
return true;
}
public get state(): number
{
@@ -45,12 +45,19 @@ export class GetGuestRoomResultMessageParser implements IMessageParser
this.data.canMute = wrapper.readBoolean();
this._chat = new RoomChatSettings(wrapper);
if(wrapper.bytesAvailable)
{
this._hotelTimeZoneId = wrapper.readString();
this._hotelCurrentTimeMs = Number(wrapper.readString()) || 0;
if(wrapper.bytesAvailable) this._roomItemLimit = wrapper.readInt();
}
// Optional trailing blocks, one tier per emulator release:
// block 1: hotel timezone id + current time ms (2 strings)
// block 2: room item limit (1 int)
// Flat early-return chain so an older server stops cleanly at
// whichever block it doesn't ship. Defaults from flush().
if(!wrapper.bytesAvailable) return true;
this._hotelTimeZoneId = wrapper.readString();
this._hotelCurrentTimeMs = Number(wrapper.readString()) || 0;
if(!wrapper.bytesAvailable) return true;
this._roomItemLimit = wrapper.readInt();
return true;
}
@@ -11,6 +11,7 @@ export class RoomUnitInfoParser implements IMessageParser
private _standId: number;
private _overlayId: number;
private _cardBackgroundId: number;
private _borderId: number;
private _nickIcon: string;
private _prefixText: string;
private _prefixColor: string;
@@ -30,6 +31,7 @@ export class RoomUnitInfoParser implements IMessageParser
this._standId = 0;
this._overlayId = 0;
this._cardBackgroundId = 0;
this._borderId = 0;
this._nickIcon = '';
this._prefixText = '';
this._prefixColor = '';
@@ -61,6 +63,7 @@ export class RoomUnitInfoParser implements IMessageParser
this._prefixEffect = (wrapper.bytesAvailable ? wrapper.readString() : '');
this._prefixFont = (wrapper.bytesAvailable ? wrapper.readString() : '');
this._displayOrder = (wrapper.bytesAvailable ? wrapper.readString() : 'icon-prefix-name');
this._borderId = (wrapper.bytesAvailable ? wrapper.readInt() : 0);
return true;
}
@@ -110,6 +113,11 @@ export class RoomUnitInfoParser implements IMessageParser
return this._cardBackgroundId;
}
public get borderId(): number
{
return this._borderId;
}
public get nickIcon(): string
{
return this._nickIcon;
@@ -146,6 +146,17 @@ export class RoomUnitParser implements IMessageParser
user.roomEntryMethod = wrapper.readString();
user.roomEntryTeleportId = wrapper.readInt();
// Arcturus appends a trailing borderId int per user
// (RoomUsersComposer, after the Infostand Borders feature)
// for every record — habbo, bot, rentable bot — using 0 as
// the constant for the records that have no border. The
// read MUST be unconditional: a bytesAvailable guard would
// be semantically wrong here (the guard answers "any byte
// left in the whole packet?" not "any byte left for THIS
// user"), and skipping the read would leave 4 bytes per
// record and cascade-corrupt every subsequent user in the
// roster.
user.borderId = wrapper.readInt();
i++;
}
@@ -45,6 +45,7 @@ export class UserMessageData
private _isModerator: boolean = false;
private _roomEntryMethod: string = 'unknown';
private _roomEntryTeleportId: number = 0;
private _borderId: number = 0;
private _isReadOnly: boolean = false;
constructor(k: number)
@@ -579,4 +580,17 @@ export class UserMessageData
this._roomEntryTeleportId = k;
}
}
public get borderId(): number
{
return this._borderId;
}
public set borderId(k: number)
{
if(!this._isReadOnly)
{
this._borderId = k;
}
}
}
@@ -37,6 +37,7 @@ export class RoomSettingsData
private _roomModerationSettings: RoomModerationSettings = null;
private _chatSettings: RoomChatSettings = null;
private _allowNavigatorDynamicCats: boolean = false;
private _allowUnderpass: boolean = false;
public static from(settings: RoomSettingsData)
{
@@ -65,6 +66,7 @@ export class RoomSettingsData
instance._roomModerationSettings = settings._roomModerationSettings;
instance._chatSettings = settings._chatSettings;
instance._allowNavigatorDynamicCats = settings._allowNavigatorDynamicCats;
instance._allowUnderpass = settings._allowUnderpass;
return instance;
}
@@ -329,4 +331,14 @@ export class RoomSettingsData
{
this._allowNavigatorDynamicCats = flag;
}
public get allowUnderpass(): boolean
{
return this._allowUnderpass;
}
public set allowUnderpass(flag: boolean)
{
this._allowUnderpass = flag;
}
}
@@ -49,6 +49,10 @@ export class RoomSettingsDataParser implements IMessageParser
this._roomSettingsData.allowNavigatorDynamicCats = wrapper.readBoolean();
this._roomSettingsData.roomModerationSettings = new RoomModerationSettings(wrapper);
// Custom Arcturus extension: trailing int (0/1) for the underpass toggle.
// Older servers may not emit it; default stays false when absent.
if(wrapper.bytesAvailable) this._roomSettingsData.allowUnderpass = (wrapper.readInt() === 1);
return true;
}
@@ -5,12 +5,24 @@ export class UserPermissionsParser implements IMessageParser
private _clubLevel: number;
private _securityLevel: number;
private _isAmbassador: boolean;
private _rankId: number;
private _rankName: string;
private _rankBadge: string;
private _rankPrefix: string;
private _rankPrefixColor: string;
private _permissions: Map<string, number> = new Map();
public flush(): boolean
{
this._clubLevel = 0;
this._securityLevel = 0;
this._isAmbassador = false;
this._rankId = 0;
this._rankName = '';
this._rankBadge = '';
this._rankPrefix = '';
this._rankPrefixColor = '';
this._permissions = new Map();
return true;
}
@@ -23,6 +35,37 @@ export class UserPermissionsParser implements IMessageParser
this._securityLevel = wrapper.readInt();
this._isAmbassador = wrapper.readBoolean();
// Optional trailing block (Arcturus-Morningstar-Extended ≥ 4.2.10):
// rank metadata + resolved permission map appended in a
// backward-compatible way. Older emulators don't write these
// bytes so we keep the defaults from flush().
if(!wrapper.bytesAvailable) return true;
this._rankId = wrapper.readInt();
this._rankName = wrapper.readString();
this._rankBadge = wrapper.readString();
this._rankPrefix = wrapper.readString();
this._rankPrefixColor = wrapper.readString();
if(!wrapper.bytesAvailable) return true;
// Resolved permission map: int count + (string key, int value)*.
// value 1 = ALLOWED, 2 = ROOM_OWNER. Only entries with
// PermissionSetting != DISALLOWED are sent; absence on the client
// means "no" (useHasPermission(key) returns false).
const count = wrapper.readInt();
const permissions = new Map<string, number>();
for(let i = 0; i < count; i++)
{
const key = wrapper.readString();
const value = wrapper.readInt();
permissions.set(key, value);
}
this._permissions = permissions;
return true;
}
@@ -40,4 +83,34 @@ export class UserPermissionsParser implements IMessageParser
{
return this._isAmbassador;
}
public get rankId(): number
{
return this._rankId;
}
public get rankName(): string
{
return this._rankName;
}
public get rankBadge(): string
{
return this._rankBadge;
}
public get rankPrefix(): string
{
return this._rankPrefix;
}
public get rankPrefixColor(): string
{
return this._rankPrefixColor;
}
public get permissions(): ReadonlyMap<string, number>
{
return this._permissions;
}
}
@@ -84,34 +84,35 @@ export class UserProfileParser implements IMessageParser
this._secondsSinceLastVisit = wrapper.readInt();
this._openProfileWindow = wrapper.readBoolean();
if(wrapper.bytesAvailable)
{
this._backgroundId = wrapper.readInt();
this._standId = wrapper.readInt();
this._overlayId = wrapper.readInt();
// Optional trailing blocks, one tier per emulator release:
// block 1: background / stand / overlay (3 ints)
// block 2: card background (1 int)
// block 3: nick icon (1 string)
// block 4: prefix decoration set (6 strings)
// Each tier early-returns to keep the parser tolerant of older
// servers that don't ship the later blocks. Defaults set by flush().
if(!wrapper.bytesAvailable) return true;
this._cardBackgroundId = (wrapper.bytesAvailable ? wrapper.readInt() : 0);
this._backgroundId = wrapper.readInt();
this._standId = wrapper.readInt();
this._overlayId = wrapper.readInt();
if(wrapper.bytesAvailable)
{
this._nickIcon = wrapper.readString();
if(!wrapper.bytesAvailable) return true;
if(wrapper.bytesAvailable)
{
this._prefixText = wrapper.readString();
this._prefixColor = wrapper.readString();
this._prefixIcon = wrapper.readString();
this._prefixEffect = wrapper.readString();
this._prefixFont = wrapper.readString();
this._displayOrder = wrapper.readString();
this._cardBackgroundId = wrapper.readInt();
if(wrapper.bytesAvailable)
{
this._totalBadges = wrapper.readInt();
}
}
}
}
if(!wrapper.bytesAvailable) return true;
this._nickIcon = wrapper.readString();
if(!wrapper.bytesAvailable) return true;
this._prefixText = wrapper.readString();
this._prefixColor = wrapper.readString();
this._prefixIcon = wrapper.readString();
this._prefixEffect = wrapper.readString();
this._prefixFont = wrapper.readString();
this._displayOrder = wrapper.readString();
return true;
}