You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-19 15:06:20 +00:00
Merge branch 'Dev' into merge-duckie-main-2026-05-06
This commit is contained in:
@@ -12,9 +12,9 @@
|
||||
"@nitrots/api": "1.0.0",
|
||||
"@nitrots/events": "1.0.0",
|
||||
"@nitrots/utils": "1.0.0",
|
||||
"@thumbmarkjs/thumbmarkjs": "^1.8.1"
|
||||
"@thumbmarkjs/thumbmarkjs": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.8.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+2
-2
@@ -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 [];
|
||||
}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+8
-9
@@ -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
|
||||
{
|
||||
|
||||
+13
-6
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user