🆙 Updates

- Added Test Coverage
- Fix Potential Memory Leaks
This commit is contained in:
DuckieTM
2026-01-31 13:21:59 +01:00
parent e263ce59bf
commit eb4fe80612
18 changed files with 1689 additions and 110 deletions
@@ -15,6 +15,7 @@ export class AvatarAssetDownloadManager
private _incompleteFigures: Map<string, AvatarAssetDownloadLibrary[]> = new Map();
private _currentDownloads: AvatarAssetDownloadLibrary[] = [];
private _libraryNames: string[] = [];
private _libraryLoadedCallback: (event: AvatarRenderLibraryEvent) => void = null;
constructor(assets: IAssetManager, structure: AvatarStructure)
{
@@ -38,11 +39,27 @@ export class AvatarAssetDownloadManager
this.processFigureMap(responseData.libraries);
GetEventDispatcher().addEventListener(NitroEventType.AVATAR_ASSET_DOWNLOADED, (event: AvatarRenderLibraryEvent) => this.onLibraryLoaded(event));
// Store callback for cleanup
this._libraryLoadedCallback = (event: AvatarRenderLibraryEvent) => this.onLibraryLoaded(event);
GetEventDispatcher().addEventListener(NitroEventType.AVATAR_ASSET_DOWNLOADED, this._libraryLoadedCallback);
await this.processMissingLibraries();
}
public dispose(): void
{
if(this._libraryLoadedCallback)
{
GetEventDispatcher().removeEventListener(NitroEventType.AVATAR_ASSET_DOWNLOADED, this._libraryLoadedCallback);
this._libraryLoadedCallback = null;
}
this._figureMap.clear();
this._figureListeners.clear();
this._incompleteFigures.clear();
this._currentDownloads = [];
}
private processFigureMap(data: any): void
{
if(!data) return;
+20 -2
View File
@@ -24,6 +24,7 @@ export class AvatarRenderManager implements IAvatarRenderManager
private _effectAssetDownloadManager: EffectAssetDownloadManager = new EffectAssetDownloadManager(GetAssetManager(), this._structure);
private _placeHolderFigure: AvatarFigureContainer = new AvatarFigureContainer(AvatarRenderManager.DEFAULT_FIGURE);
private _aliasResetCallback: () => void = null;
public async init(): Promise<void>
{
@@ -37,13 +38,30 @@ export class AvatarRenderManager implements IAvatarRenderManager
this._aliasCollection.init();
GetEventDispatcher().addEventListener(NitroEventType.AVATAR_ASSET_LOADED, () => this._aliasCollection.reset());
GetEventDispatcher().addEventListener(NitroEventType.AVATAR_EFFECT_LOADED, () => this._aliasCollection.reset());
// Store callback for cleanup
this._aliasResetCallback = () => this._aliasCollection.reset();
GetEventDispatcher().addEventListener(NitroEventType.AVATAR_ASSET_LOADED, this._aliasResetCallback);
GetEventDispatcher().addEventListener(NitroEventType.AVATAR_EFFECT_LOADED, this._aliasResetCallback);
await this._avatarAssetDownloadManager.init();
await this._effectAssetDownloadManager.init();
}
public dispose(): void
{
// Remove event listeners
if(this._aliasResetCallback)
{
GetEventDispatcher().removeEventListener(NitroEventType.AVATAR_ASSET_LOADED, this._aliasResetCallback);
GetEventDispatcher().removeEventListener(NitroEventType.AVATAR_EFFECT_LOADED, this._aliasResetCallback);
this._aliasResetCallback = null;
}
// Dispose download managers
this._avatarAssetDownloadManager?.dispose();
this._effectAssetDownloadManager?.dispose();
}
private async loadActions(): Promise<void>
{
const defaultActions = GetConfiguration().getValue<string>('avatar.default.actions');
@@ -15,6 +15,7 @@ export class EffectAssetDownloadManager
private _incompleteEffects: Map<string, EffectAssetDownloadLibrary[]> = new Map();
private _currentDownloads: EffectAssetDownloadLibrary[] = [];
private _libraryNames: string[] = [];
private _libraryLoadedCallback: (event: AvatarRenderEffectLibraryEvent) => void = null;
constructor(assets: IAssetManager, structure: AvatarStructure)
{
@@ -38,11 +39,27 @@ export class EffectAssetDownloadManager
this.processEffectMap(responseData.effects);
GetEventDispatcher().addEventListener(NitroEventType.AVATAR_EFFECT_DOWNLOADED, (event: AvatarRenderEffectLibraryEvent) => this.onLibraryLoaded(event));
// Store callback for cleanup
this._libraryLoadedCallback = (event: AvatarRenderEffectLibraryEvent) => this.onLibraryLoaded(event);
GetEventDispatcher().addEventListener(NitroEventType.AVATAR_EFFECT_DOWNLOADED, this._libraryLoadedCallback);
await this.processMissingLibraries();
}
public dispose(): void
{
if(this._libraryLoadedCallback)
{
GetEventDispatcher().removeEventListener(NitroEventType.AVATAR_EFFECT_DOWNLOADED, this._libraryLoadedCallback);
this._libraryLoadedCallback = null;
}
this._effectMap.clear();
this._effectListeners.clear();
this._incompleteEffects.clear();
this._currentDownloads = [];
}
private processEffectMap(data: any): void
{
if(!data) return;
@@ -13,6 +13,10 @@ export class CommunicationManager implements ICommunicationManager
private _messages: IMessageConfiguration = new NitroMessages();
private _pongInterval: any = null;
private _messageEvents: IMessageEvent[] = [];
private _socketClosedCallback: () => void = null;
private _socketOpenedCallback: () => void = null;
private _socketErrorCallback: () => void = null;
private getGpu(): string {
const e = document.createElement('canvas');
@@ -88,44 +92,86 @@ export class CommunicationManager implements ICommunicationManager
public async init(): Promise<void>
{
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, () =>
// Store callback for cleanup
this._socketClosedCallback = () =>
{
this.stopPong();
});
};
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, this._socketClosedCallback);
return new Promise((resolve, reject) =>
{
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_OPENED, () =>
// Store callback for cleanup
this._socketOpenedCallback = () =>
{
if(GetConfiguration().getValue<boolean>('system.pong.manually', false)) this.startPong();
const machineId = this.generateMachineID();
this._connection.send(new ClientHelloMessageComposer(null, null, null, null));
this._connection.send(new SSOTicketMessageComposer(GetConfiguration().getValue('sso.ticket', null), GetTickerTime()));
this._connection.send(new UniqueIDMessageComposer(machineId, '', ''));
});
};
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_OPENED, this._socketOpenedCallback);
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_ERROR, () =>
// Store callback for cleanup
this._socketErrorCallback = () =>
{
reject();
});
};
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_ERROR, this._socketErrorCallback);
this._connection.addMessageEvent(new ClientPingEvent((event: ClientPingEvent) => this.sendPong()));
this._connection.addMessageEvent(new AuthenticatedEvent((event: AuthenticatedEvent) =>
// Store message events for cleanup
const pingEvent = new ClientPingEvent((event: ClientPingEvent) => this.sendPong());
const authEvent = new AuthenticatedEvent((event: AuthenticatedEvent) =>
{
this._connection.authenticated();
resolve();
event.connection.send(new InfoRetrieveMessageComposer());
}));
});
this._messageEvents.push(pingEvent, authEvent);
this._connection.addMessageEvent(pingEvent);
this._connection.addMessageEvent(authEvent);
this._connection.init(GetConfiguration().getValue<string>('socket.url'));
});
}
public dispose(): void
{
// Stop pong interval
this.stopPong();
// Remove event dispatcher listeners
if(this._socketClosedCallback)
{
GetEventDispatcher().removeEventListener(NitroEventType.SOCKET_CLOSED, this._socketClosedCallback);
this._socketClosedCallback = null;
}
if(this._socketOpenedCallback)
{
GetEventDispatcher().removeEventListener(NitroEventType.SOCKET_OPENED, this._socketOpenedCallback);
this._socketOpenedCallback = null;
}
if(this._socketErrorCallback)
{
GetEventDispatcher().removeEventListener(NitroEventType.SOCKET_ERROR, this._socketErrorCallback);
this._socketErrorCallback = null;
}
// Remove message events
for(const event of this._messageEvents)
{
this._connection.removeMessageEvent(event);
}
this._messageEvents = [];
}
protected startPong(): void
{
if(this._pongInterval) this.stopPong();
+46 -9
View File
@@ -17,6 +17,12 @@ export class SocketConnection implements IConnection
private _isAuthenticated: boolean = false;
// Store callbacks for cleanup
private _onOpenCallback: (event: Event) => void = null;
private _onCloseCallback: (event: Event) => void = null;
private _onErrorCallback: (event: Event) => void = null;
private _onMessageCallback: (event: MessageEvent) => void = null;
public init(socketUrl: string): void
{
if(!socketUrl || !socketUrl.length) return;
@@ -26,18 +32,49 @@ export class SocketConnection implements IConnection
this._socket = new WebSocket(socketUrl);
this._socket.binaryType = 'arraybuffer';
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_OPENED, event => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_OPENED)));
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_CLOSED, event => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_CLOSED)));
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_ERROR, event => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_ERROR)));
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_MESSAGE, (event: MessageEvent) =>
// Store callbacks for cleanup
this._onOpenCallback = () => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_OPENED));
this._onCloseCallback = () => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_CLOSED));
this._onErrorCallback = () => GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.SOCKET_ERROR));
this._onMessageCallback = (event: MessageEvent) =>
{
this._dataBuffer = this.concatArrayBuffers(this._dataBuffer, event.data);
this.processReceivedData();
});
};
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_OPENED, this._onOpenCallback);
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_CLOSED, this._onCloseCallback);
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_ERROR, this._onErrorCallback);
this._socket.addEventListener(WebSocketEventEnum.CONNECTION_MESSAGE, this._onMessageCallback);
}
public dispose(): void
{
if(this._socket)
{
// Remove all event listeners
if(this._onOpenCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_OPENED, this._onOpenCallback);
if(this._onCloseCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_CLOSED, this._onCloseCallback);
if(this._onErrorCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_ERROR, this._onErrorCallback);
if(this._onMessageCallback) this._socket.removeEventListener(WebSocketEventEnum.CONNECTION_MESSAGE, this._onMessageCallback);
// Close socket if still open
if(this._socket.readyState === WebSocket.OPEN || this._socket.readyState === WebSocket.CONNECTING)
{
this._socket.close();
}
this._socket = null;
}
this._onOpenCallback = null;
this._onCloseCallback = null;
this._onErrorCallback = null;
this._onMessageCallback = null;
this._pendingClientMessages = [];
this._pendingServerMessages = [];
this._dataBuffer = null;
}
public ready(): void
@@ -0,0 +1,250 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { EventDispatcher } from '../EventDispatcher';
interface TestEvent
{
type: string;
data?: any;
}
describe('EventDispatcher', () =>
{
let dispatcher: EventDispatcher;
beforeEach(() =>
{
dispatcher = new EventDispatcher();
});
describe('addEventListener', () =>
{
it('should add event listener', () =>
{
const callback = vi.fn();
dispatcher.addEventListener('test', callback);
dispatcher.dispatchEvent({ type: 'test' });
expect(callback).toHaveBeenCalledTimes(1);
});
it('should support multiple listeners for same event', () =>
{
const callback1 = vi.fn();
const callback2 = vi.fn();
dispatcher.addEventListener('test', callback1);
dispatcher.addEventListener('test', callback2);
dispatcher.dispatchEvent({ type: 'test' });
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
});
it('should not add listener if type is null', () =>
{
const callback = vi.fn();
dispatcher.addEventListener(null as any, callback);
// Should not throw and callback should never be called
expect(() => dispatcher.dispatchEvent({ type: 'test' })).not.toThrow();
});
it('should not add listener if callback is null', () =>
{
dispatcher.addEventListener('test', null as any);
// Should not throw
expect(() => dispatcher.dispatchEvent({ type: 'test' })).not.toThrow();
});
});
describe('removeEventListener', () =>
{
it('should remove event listener', () =>
{
const callback = vi.fn();
dispatcher.addEventListener('test', callback);
dispatcher.removeEventListener('test', callback);
dispatcher.dispatchEvent({ type: 'test' });
expect(callback).not.toHaveBeenCalled();
});
it('should only remove specified listener', () =>
{
const callback1 = vi.fn();
const callback2 = vi.fn();
dispatcher.addEventListener('test', callback1);
dispatcher.addEventListener('test', callback2);
dispatcher.removeEventListener('test', callback1);
dispatcher.dispatchEvent({ type: 'test' });
expect(callback1).not.toHaveBeenCalled();
expect(callback2).toHaveBeenCalledTimes(1);
});
it('should handle removing non-existent listener', () =>
{
const callback = vi.fn();
// Should not throw
expect(() => dispatcher.removeEventListener('test', callback)).not.toThrow();
});
it('should not remove listener if type is null', () =>
{
const callback = vi.fn();
dispatcher.addEventListener('test', callback);
dispatcher.removeEventListener(null as any, callback);
dispatcher.dispatchEvent({ type: 'test' });
expect(callback).toHaveBeenCalledTimes(1);
});
});
describe('dispatchEvent', () =>
{
it('should dispatch event to listeners', () =>
{
const callback = vi.fn();
dispatcher.addEventListener('test', callback);
const event: TestEvent = { type: 'test', data: 'hello' };
dispatcher.dispatchEvent(event);
expect(callback).toHaveBeenCalledWith(event);
});
it('should return true when event is dispatched', () =>
{
const result = dispatcher.dispatchEvent({ type: 'test' });
expect(result).toBe(true);
});
it('should return false when event is null', () =>
{
const result = dispatcher.dispatchEvent(null as any);
expect(result).toBe(false);
});
it('should not throw when no listeners for event type', () =>
{
expect(() => dispatcher.dispatchEvent({ type: 'unknown' })).not.toThrow();
});
it('should call listeners in order they were added', () =>
{
const order: number[] = [];
dispatcher.addEventListener('test', () => order.push(1));
dispatcher.addEventListener('test', () => order.push(2));
dispatcher.addEventListener('test', () => order.push(3));
dispatcher.dispatchEvent({ type: 'test' });
expect(order).toEqual([1, 2, 3]);
});
it('should handle errors in listeners gracefully', () =>
{
const callback1 = vi.fn(() =>
{
throw new Error('Test error');
});
const callback2 = vi.fn();
dispatcher.addEventListener('test', callback1);
dispatcher.addEventListener('test', callback2);
// Should not throw
expect(() => dispatcher.dispatchEvent({ type: 'test' })).not.toThrow();
// First callback was called
expect(callback1).toHaveBeenCalled();
// Second callback was NOT called because first threw error
// (This is the current behavior - stops on first error)
expect(callback2).not.toHaveBeenCalled();
});
});
describe('removeAllListeners', () =>
{
it('should remove all listeners', () =>
{
const callback1 = vi.fn();
const callback2 = vi.fn();
const callback3 = vi.fn();
dispatcher.addEventListener('test1', callback1);
dispatcher.addEventListener('test2', callback2);
dispatcher.addEventListener('test1', callback3);
dispatcher.removeAllListeners();
dispatcher.dispatchEvent({ type: 'test1' });
dispatcher.dispatchEvent({ type: 'test2' });
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
});
});
describe('dispose', () =>
{
it('should remove all listeners on dispose', () =>
{
const callback = vi.fn();
dispatcher.addEventListener('test', callback);
dispatcher.dispose();
dispatcher.dispatchEvent({ type: 'test' });
expect(callback).not.toHaveBeenCalled();
});
});
describe('multiple event types', () =>
{
it('should handle multiple event types independently', () =>
{
const callback1 = vi.fn();
const callback2 = vi.fn();
dispatcher.addEventListener('type1', callback1);
dispatcher.addEventListener('type2', callback2);
dispatcher.dispatchEvent({ type: 'type1' });
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();
dispatcher.dispatchEvent({ type: 'type2' });
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
});
});
describe('event data', () =>
{
it('should pass event data to listeners', () =>
{
const callback = vi.fn();
dispatcher.addEventListener('test', callback);
const eventData = { type: 'test', value: 42, nested: { key: 'value' } };
dispatcher.dispatchEvent(eventData);
expect(callback).toHaveBeenCalledWith(eventData);
});
});
});
+31 -2
View File
@@ -64,13 +64,16 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService
private _mouseCursorUpdate: boolean = false;
private _badgeListenerObjects: Map<string, RoomObjectBadgeImageAssetListener[]> = new Map();
private _areaSelectionManager: IRoomAreaSelectionManager = new RoomAreaSelectionManager(this);
private _roomSessionEventCallback: (event: RoomSessionEvent) => void = null;
public async init(): Promise<void>
{
GetRoomObjectLogicFactory().registerEventFunction(event => this.processRoomObjectEvent(event));
GetEventDispatcher().addEventListener<RoomSessionEvent>(RoomSessionEvent.STARTED, event => this.onRoomSessionEvent(event));
GetEventDispatcher().addEventListener<RoomSessionEvent>(RoomSessionEvent.ENDED, event => this.onRoomSessionEvent(event));
// Store callback for cleanup
this._roomSessionEventCallback = (event: RoomSessionEvent) => this.onRoomSessionEvent(event);
GetEventDispatcher().addEventListener<RoomSessionEvent>(RoomSessionEvent.STARTED, this._roomSessionEventCallback);
GetEventDispatcher().addEventListener<RoomSessionEvent>(RoomSessionEvent.ENDED, this._roomSessionEventCallback);
await GetRoomMessageHandler().init();
await this._roomContentLoader.init();
@@ -108,6 +111,32 @@ export class RoomEngine implements IRoomEngine, IRoomCreator, IRoomEngineService
}
}
public dispose(): void
{
// Remove event listeners
if(this._roomSessionEventCallback)
{
GetEventDispatcher().removeEventListener(RoomSessionEvent.STARTED, this._roomSessionEventCallback);
GetEventDispatcher().removeEventListener(RoomSessionEvent.ENDED, this._roomSessionEventCallback);
this._roomSessionEventCallback = null;
}
// Dispose room message handler
GetRoomMessageHandler().dispose();
// Clear all room instances
for(const roomId of this._roomDatas.keys())
{
this.removeRoomInstance(roomId);
}
this._roomDatas.clear();
this._roomInstanceDatas.clear();
this._imageCallbacks.clear();
this._thumbnailCallbacks.clear();
this._badgeListenerObjects.clear();
}
public setActiveRoomId(roomId: number): void
{
this._activeRoomId = roomId;
+27 -4
View File
@@ -16,12 +16,14 @@ export class RoomManager implements IRoomManager, IRoomInstanceContainer
private _pendingContentTypes: string[] = [];
private _skipContentProcessing: boolean = false;
private _contentLoadedCallback: (event: RoomContentLoadedEvent) => void = null;
public async init(listener: IRoomManagerListener): Promise<void>
{
this._listener = listener;
const onRoomContentLoadedEvent = (event: RoomContentLoadedEvent) =>
// Store callback for cleanup
this._contentLoadedCallback = (event: RoomContentLoadedEvent) =>
{
if(!GetRoomContentLoader()) return;
@@ -32,9 +34,30 @@ export class RoomManager implements IRoomManager, IRoomInstanceContainer
this._pendingContentTypes.push(contentType);
};
GetEventDispatcher().addEventListener(RoomContentLoadedEvent.RCLE_SUCCESS, onRoomContentLoadedEvent);
GetEventDispatcher().addEventListener(RoomContentLoadedEvent.RCLE_FAILURE, onRoomContentLoadedEvent);
GetEventDispatcher().addEventListener(RoomContentLoadedEvent.RCLE_CANCEL, onRoomContentLoadedEvent);
GetEventDispatcher().addEventListener(RoomContentLoadedEvent.RCLE_SUCCESS, this._contentLoadedCallback);
GetEventDispatcher().addEventListener(RoomContentLoadedEvent.RCLE_FAILURE, this._contentLoadedCallback);
GetEventDispatcher().addEventListener(RoomContentLoadedEvent.RCLE_CANCEL, this._contentLoadedCallback);
}
public dispose(): void
{
// Remove event listeners
if(this._contentLoadedCallback)
{
GetEventDispatcher().removeEventListener(RoomContentLoadedEvent.RCLE_SUCCESS, this._contentLoadedCallback);
GetEventDispatcher().removeEventListener(RoomContentLoadedEvent.RCLE_FAILURE, this._contentLoadedCallback);
GetEventDispatcher().removeEventListener(RoomContentLoadedEvent.RCLE_CANCEL, this._contentLoadedCallback);
this._contentLoadedCallback = null;
}
// Dispose all room instances
for(const room of this._rooms.values())
{
room.dispose();
}
this._rooms.clear();
this._pendingContentTypes = [];
}
public getRoomInstance(roomId: string): IRoomInstance
+73 -46
View File
@@ -1,4 +1,4 @@
import { AvatarGuideStatus, IConnection, IRoomCreator, IVector3D, LegacyDataType, ObjectRolling, PetType, RoomObjectType, RoomObjectUserType, RoomObjectVariable } from '@nitrots/api';
import { AvatarGuideStatus, IConnection, IMessageEvent, IRoomCreator, IVector3D, LegacyDataType, ObjectRolling, PetType, RoomObjectType, RoomObjectUserType, RoomObjectVariable } from '@nitrots/api';
import { AreaHideMessageEvent, DiceValueMessageEvent, FloorHeightMapEvent, FurnitureAliasesComposer, FurnitureAliasesEvent, FurnitureDataEvent, FurnitureFloorAddEvent, FurnitureFloorDataParser, FurnitureFloorEvent, FurnitureFloorRemoveEvent, FurnitureFloorUpdateEvent, FurnitureWallAddEvent, FurnitureWallDataParser, FurnitureWallEvent, FurnitureWallRemoveEvent, FurnitureWallUpdateEvent, GetCommunication, GetRoomEntryDataMessageComposer, GuideSessionEndedMessageEvent, GuideSessionErrorMessageEvent, GuideSessionStartedMessageEvent, IgnoreResultEvent, ItemDataUpdateMessageEvent, ObjectsDataUpdateEvent, ObjectsRollingEvent, OneWayDoorStatusMessageEvent, PetExperienceEvent, PetFigureUpdateEvent, RoomEntryTileMessageEvent, RoomEntryTileMessageParser, RoomHeightMapEvent, RoomHeightMapUpdateEvent, RoomPaintEvent, RoomReadyMessageEvent, RoomUnitChatEvent, RoomUnitChatShoutEvent, RoomUnitChatWhisperEvent, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitIdleEvent, RoomUnitInfoEvent, RoomUnitNumberEvent, RoomUnitRemoveEvent, RoomUnitStatusEvent, RoomUnitTypingEvent, RoomVisualizationSettingsEvent, UserInfoEvent, YouArePlayingGameEvent } from '@nitrots/communication';
import { GetRoomSessionManager, GetSessionDataManager } from '@nitrots/session';
import { Vector3d } from '@nitrots/utils';
@@ -13,6 +13,7 @@ export class RoomMessageHandler
private _roomEngine: IRoomCreator = null;
private _planeParser = new RoomPlaneParser();
private _latestEntryTileEvent: RoomEntryTileMessageEvent = null;
private _messageEvents: IMessageEvent[] = [];
private _currentRoomId: number = 0;
private _ownUserId: number = 0;
@@ -25,51 +26,77 @@ export class RoomMessageHandler
this._connection = GetCommunication().connection;
this._roomEngine = GetRoomEngine();
this._connection.addMessageEvent(new UserInfoEvent(this.onUserInfoEvent.bind(this)));
this._connection.addMessageEvent(new RoomReadyMessageEvent(this.onRoomReadyMessageEvent.bind(this)));
this._connection.addMessageEvent(new RoomPaintEvent(this.onRoomPaintEvent.bind(this)));
this._connection.addMessageEvent(new FloorHeightMapEvent(this.onRoomModelEvent.bind(this)));
this._connection.addMessageEvent(new RoomHeightMapEvent(this.onRoomHeightMapEvent.bind(this)));
this._connection.addMessageEvent(new RoomHeightMapUpdateEvent(this.onRoomHeightMapUpdateEvent.bind(this)));
this._connection.addMessageEvent(new RoomVisualizationSettingsEvent(this.onRoomThicknessEvent.bind(this)));
this._connection.addMessageEvent(new RoomEntryTileMessageEvent(this.onRoomDoorEvent.bind(this)));
this._connection.addMessageEvent(new ObjectsRollingEvent(this.onRoomRollingEvent.bind(this)));
this._connection.addMessageEvent(new ObjectsDataUpdateEvent(this.onObjectsDataUpdateEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureAliasesEvent(this.onFurnitureAliasesEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureFloorAddEvent(this.onFurnitureFloorAddEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureFloorEvent(this.onFurnitureFloorEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureFloorRemoveEvent(this.onFurnitureFloorRemoveEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureFloorUpdateEvent(this.onFurnitureFloorUpdateEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureWallAddEvent(this.onFurnitureWallAddEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureWallEvent(this.onFurnitureWallEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureWallRemoveEvent(this.onFurnitureWallRemoveEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureWallUpdateEvent(this.onFurnitureWallUpdateEvent.bind(this)));
this._connection.addMessageEvent(new FurnitureDataEvent(this.onFurnitureDataEvent.bind(this)));
this._connection.addMessageEvent(new ItemDataUpdateMessageEvent(this.onItemDataUpdateMessageEvent.bind(this)));
this._connection.addMessageEvent(new OneWayDoorStatusMessageEvent(this.onOneWayDoorStatusMessageEvent.bind(this)));
this._connection.addMessageEvent(new AreaHideMessageEvent(this.onAreaHideMessageEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitDanceEvent(this.onRoomUnitDanceEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitEffectEvent(this.onRoomUnitEffectEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitEvent(this.onRoomUnitEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitExpressionEvent(this.onRoomUnitExpressionEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitHandItemEvent(this.onRoomUnitHandItemEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitIdleEvent(this.onRoomUnitIdleEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitInfoEvent(this.onRoomUnitInfoEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitNumberEvent(this.onRoomUnitNumberEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitRemoveEvent(this.onRoomUnitRemoveEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitStatusEvent(this.onRoomUnitStatusEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitChatEvent(this.onRoomUnitChatEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitChatShoutEvent(this.onRoomUnitChatEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitChatWhisperEvent(this.onRoomUnitChatEvent.bind(this)));
this._connection.addMessageEvent(new RoomUnitTypingEvent(this.onRoomUnitTypingEvent.bind(this)));
this._connection.addMessageEvent(new PetFigureUpdateEvent(this.onPetFigureUpdateEvent.bind(this)));
this._connection.addMessageEvent(new PetExperienceEvent(this.onPetExperienceEvent.bind(this)));
this._connection.addMessageEvent(new YouArePlayingGameEvent(this.onYouArePlayingGameEvent.bind(this)));
this._connection.addMessageEvent(new DiceValueMessageEvent(this.onDiceValueMessageEvent.bind(this)));
this._connection.addMessageEvent(new IgnoreResultEvent(this.onIgnoreResultEvent.bind(this)));
this._connection.addMessageEvent(new GuideSessionStartedMessageEvent(this.onGuideSessionStartedMessageEvent.bind(this)));
this._connection.addMessageEvent(new GuideSessionEndedMessageEvent(this.onGuideSessionEndedMessageEvent.bind(this)));
this._connection.addMessageEvent(new GuideSessionErrorMessageEvent(this.onGuideSessionErrorMessageEvent.bind(this)));
// Store all message events for cleanup
this._messageEvents = [
new UserInfoEvent(this.onUserInfoEvent.bind(this)),
new RoomReadyMessageEvent(this.onRoomReadyMessageEvent.bind(this)),
new RoomPaintEvent(this.onRoomPaintEvent.bind(this)),
new FloorHeightMapEvent(this.onRoomModelEvent.bind(this)),
new RoomHeightMapEvent(this.onRoomHeightMapEvent.bind(this)),
new RoomHeightMapUpdateEvent(this.onRoomHeightMapUpdateEvent.bind(this)),
new RoomVisualizationSettingsEvent(this.onRoomThicknessEvent.bind(this)),
new RoomEntryTileMessageEvent(this.onRoomDoorEvent.bind(this)),
new ObjectsRollingEvent(this.onRoomRollingEvent.bind(this)),
new ObjectsDataUpdateEvent(this.onObjectsDataUpdateEvent.bind(this)),
new FurnitureAliasesEvent(this.onFurnitureAliasesEvent.bind(this)),
new FurnitureFloorAddEvent(this.onFurnitureFloorAddEvent.bind(this)),
new FurnitureFloorEvent(this.onFurnitureFloorEvent.bind(this)),
new FurnitureFloorRemoveEvent(this.onFurnitureFloorRemoveEvent.bind(this)),
new FurnitureFloorUpdateEvent(this.onFurnitureFloorUpdateEvent.bind(this)),
new FurnitureWallAddEvent(this.onFurnitureWallAddEvent.bind(this)),
new FurnitureWallEvent(this.onFurnitureWallEvent.bind(this)),
new FurnitureWallRemoveEvent(this.onFurnitureWallRemoveEvent.bind(this)),
new FurnitureWallUpdateEvent(this.onFurnitureWallUpdateEvent.bind(this)),
new FurnitureDataEvent(this.onFurnitureDataEvent.bind(this)),
new ItemDataUpdateMessageEvent(this.onItemDataUpdateMessageEvent.bind(this)),
new OneWayDoorStatusMessageEvent(this.onOneWayDoorStatusMessageEvent.bind(this)),
new AreaHideMessageEvent(this.onAreaHideMessageEvent.bind(this)),
new RoomUnitDanceEvent(this.onRoomUnitDanceEvent.bind(this)),
new RoomUnitEffectEvent(this.onRoomUnitEffectEvent.bind(this)),
new RoomUnitEvent(this.onRoomUnitEvent.bind(this)),
new RoomUnitExpressionEvent(this.onRoomUnitExpressionEvent.bind(this)),
new RoomUnitHandItemEvent(this.onRoomUnitHandItemEvent.bind(this)),
new RoomUnitIdleEvent(this.onRoomUnitIdleEvent.bind(this)),
new RoomUnitInfoEvent(this.onRoomUnitInfoEvent.bind(this)),
new RoomUnitNumberEvent(this.onRoomUnitNumberEvent.bind(this)),
new RoomUnitRemoveEvent(this.onRoomUnitRemoveEvent.bind(this)),
new RoomUnitStatusEvent(this.onRoomUnitStatusEvent.bind(this)),
new RoomUnitChatEvent(this.onRoomUnitChatEvent.bind(this)),
new RoomUnitChatShoutEvent(this.onRoomUnitChatEvent.bind(this)),
new RoomUnitChatWhisperEvent(this.onRoomUnitChatEvent.bind(this)),
new RoomUnitTypingEvent(this.onRoomUnitTypingEvent.bind(this)),
new PetFigureUpdateEvent(this.onPetFigureUpdateEvent.bind(this)),
new PetExperienceEvent(this.onPetExperienceEvent.bind(this)),
new YouArePlayingGameEvent(this.onYouArePlayingGameEvent.bind(this)),
new DiceValueMessageEvent(this.onDiceValueMessageEvent.bind(this)),
new IgnoreResultEvent(this.onIgnoreResultEvent.bind(this)),
new GuideSessionStartedMessageEvent(this.onGuideSessionStartedMessageEvent.bind(this)),
new GuideSessionEndedMessageEvent(this.onGuideSessionEndedMessageEvent.bind(this)),
new GuideSessionErrorMessageEvent(this.onGuideSessionErrorMessageEvent.bind(this))
];
// Register all message events
for(const event of this._messageEvents)
{
this._connection.addMessageEvent(event);
}
}
public dispose(): void
{
// Remove all message events
if(this._connection)
{
for(const event of this._messageEvents)
{
this._connection.removeMessageEvent(event);
}
}
this._messageEvents = [];
this._connection = null;
this._roomEngine = null;
this._latestEntryTileEvent = null;
}
public setRoomId(id: number): void
+47 -23
View File
@@ -1,4 +1,4 @@
import { IFurnitureData, IGroupInformationManager, IMessageComposer, IProductData, ISessionDataManager, NoobnessLevelEnum, SecurityLevel } from '@nitrots/api';
import { IFurnitureData, IGroupInformationManager, IMessageComposer, IMessageEvent, IProductData, ISessionDataManager, NoobnessLevelEnum, SecurityLevel } from '@nitrots/api';
import { AccountSafetyLockStatusChangeMessageEvent, AccountSafetyLockStatusChangeParser, AvailabilityStatusMessageEvent, ChangeUserNameResultMessageEvent, EmailStatusResultEvent, FigureUpdateEvent, GetCommunication, GetUserTagsComposer, InClientLinkEvent, MysteryBoxKeysEvent, NoobnessLevelMessageEvent, PetRespectComposer, PetScratchFailedMessageEvent, RoomReadyMessageEvent, RoomUnitChatComposer, UserInfoEvent, UserNameChangeMessageEvent, UserPermissionsEvent, UserRespectComposer, UserTagsMessageEvent } from '@nitrots/communication';
import { GetConfiguration } from '@nitrots/configuration';
import { GetEventDispatcher, MysteryBoxKeysUpdateEvent, NitroSettingsEvent, SessionDataPreferencesEvent, UserNameUpdateEvent } from '@nitrots/events';
@@ -12,6 +12,8 @@ import { ProductDataLoader } from './product/ProductDataLoader';
export class SessionDataManager implements ISessionDataManager
{
private _messageEvents: IMessageEvent[] = [];
private _settingsEventCallback: (event: NitroSettingsEvent) => void = null;
private _userId: number;
private _name: string;
private _figure: string;
@@ -62,35 +64,57 @@ export class SessionDataManager implements ISessionDataManager
this._groupInformationManager.init()
]);
GetCommunication().registerMessageEvent(new FigureUpdateEvent((event: FigureUpdateEvent) =>
{
this._figure = event.getParser().figure;
this._gender = event.getParser().gender;
// Store message event references for cleanup
this._messageEvents.push(
GetCommunication().registerMessageEvent(new FigureUpdateEvent((event: FigureUpdateEvent) =>
{
this._figure = event.getParser().figure;
this._gender = event.getParser().gender;
HabboWebTools.updateFigure(this._figure);
}));
HabboWebTools.updateFigure(this._figure);
})),
GetCommunication().registerMessageEvent(new UserInfoEvent(this.onUserInfoEvent.bind(this))),
GetCommunication().registerMessageEvent(new UserPermissionsEvent(this.onUserPermissionsEvent.bind(this))),
GetCommunication().registerMessageEvent(new AvailabilityStatusMessageEvent(this.onAvailabilityStatusMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new PetScratchFailedMessageEvent(this.onPetRespectFailed.bind(this))),
GetCommunication().registerMessageEvent(new ChangeUserNameResultMessageEvent(this.onChangeNameUpdateEvent.bind(this))),
GetCommunication().registerMessageEvent(new UserNameChangeMessageEvent(this.onUserNameChangeMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new UserTagsMessageEvent(this.onUserTags.bind(this))),
GetCommunication().registerMessageEvent(new RoomReadyMessageEvent(this.onRoomModelNameEvent.bind(this))),
GetCommunication().registerMessageEvent(new InClientLinkEvent(this.onInClientLinkEvent.bind(this))),
GetCommunication().registerMessageEvent(new MysteryBoxKeysEvent(this.onMysteryBoxKeysEvent.bind(this))),
GetCommunication().registerMessageEvent(new NoobnessLevelMessageEvent(this.onNoobnessLevelMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new AccountSafetyLockStatusChangeMessageEvent(this.onAccountSafetyLockStatusChangeMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new EmailStatusResultEvent(this.onEmailStatus.bind(this)))
);
GetCommunication().registerMessageEvent(new UserInfoEvent(this.onUserInfoEvent.bind(this)));
GetCommunication().registerMessageEvent(new UserPermissionsEvent(this.onUserPermissionsEvent.bind(this)));
GetCommunication().registerMessageEvent(new AvailabilityStatusMessageEvent(this.onAvailabilityStatusMessageEvent.bind(this)));
GetCommunication().registerMessageEvent(new PetScratchFailedMessageEvent(this.onPetRespectFailed.bind(this)));
GetCommunication().registerMessageEvent(new ChangeUserNameResultMessageEvent(this.onChangeNameUpdateEvent.bind(this)));
GetCommunication().registerMessageEvent(new UserNameChangeMessageEvent(this.onUserNameChangeMessageEvent.bind(this)));
GetCommunication().registerMessageEvent(new UserTagsMessageEvent(this.onUserTags.bind(this)));
GetCommunication().registerMessageEvent(new RoomReadyMessageEvent(this.onRoomModelNameEvent.bind(this)));
GetCommunication().registerMessageEvent(new InClientLinkEvent(this.onInClientLinkEvent.bind(this)));
GetCommunication().registerMessageEvent(new MysteryBoxKeysEvent(this.onMysteryBoxKeysEvent.bind(this)));
GetCommunication().registerMessageEvent(new NoobnessLevelMessageEvent(this.onNoobnessLevelMessageEvent.bind(this)));
GetCommunication().registerMessageEvent(new AccountSafetyLockStatusChangeMessageEvent(this.onAccountSafetyLockStatusChangeMessageEvent.bind(this)));
GetCommunication().registerMessageEvent(new EmailStatusResultEvent(this.onEmailStatus.bind(this)));
GetEventDispatcher().addEventListener<NitroSettingsEvent>(NitroSettingsEvent.SETTINGS_UPDATED, event =>
// Store event dispatcher callback for cleanup
this._settingsEventCallback = (event: NitroSettingsEvent) =>
{
this._isRoomCameraFollowDisabled = event.cameraFollow;
this._uiFlags = event.flags;
GetEventDispatcher().dispatchEvent(new SessionDataPreferencesEvent(this._uiFlags));
});
};
GetEventDispatcher().addEventListener<NitroSettingsEvent>(NitroSettingsEvent.SETTINGS_UPDATED, this._settingsEventCallback);
}
public dispose(): void
{
// Remove all message events
for(const event of this._messageEvents)
{
GetCommunication().removeMessageEvent(event);
}
this._messageEvents = [];
// Remove event dispatcher listener
if(this._settingsEventCallback)
{
GetEventDispatcher().removeEventListener(NitroSettingsEvent.SETTINGS_UPDATED, this._settingsEventCallback);
this._settingsEventCallback = null;
}
}
private resetUserInfo(): void
+33 -5
View File
@@ -15,16 +15,44 @@ export class SoundManager implements ISoundManager
private _furnitureBeingPlayed: IAdvancedMap<number, number> = new AdvancedMap();
private _musicController: IMusicController = new MusicController();
private _eventCallback: (event: INitroEvent) => void = null;
public async init(): Promise<void>
{
this._musicController.init();
GetEventDispatcher().addEventListener<RoomEngineSamplePlaybackEvent>(RoomEngineSamplePlaybackEvent.PLAY_SAMPLE, event => this.onEvent(event));
GetEventDispatcher().addEventListener<RoomEngineObjectEvent>(RoomEngineObjectEvent.REMOVED, event => this.onEvent(event));
GetEventDispatcher().addEventListener<RoomEngineEvent>(RoomEngineEvent.DISPOSED, event => this.onEvent(event));
GetEventDispatcher().addEventListener<NitroSettingsEvent>(NitroSettingsEvent.SETTINGS_UPDATED, event => this.onEvent(event));
GetEventDispatcher().addEventListener<NitroSoundEvent>(NitroSoundEvent.PLAY_SOUND, event => this.onEvent(event));
// Store callback for cleanup
this._eventCallback = (event: INitroEvent) => this.onEvent(event);
GetEventDispatcher().addEventListener<RoomEngineSamplePlaybackEvent>(RoomEngineSamplePlaybackEvent.PLAY_SAMPLE, this._eventCallback);
GetEventDispatcher().addEventListener<RoomEngineObjectEvent>(RoomEngineObjectEvent.REMOVED, this._eventCallback);
GetEventDispatcher().addEventListener<RoomEngineEvent>(RoomEngineEvent.DISPOSED, this._eventCallback);
GetEventDispatcher().addEventListener<NitroSettingsEvent>(NitroSettingsEvent.SETTINGS_UPDATED, this._eventCallback);
GetEventDispatcher().addEventListener<NitroSoundEvent>(NitroSoundEvent.PLAY_SOUND, this._eventCallback);
}
public dispose(): void
{
if(this._eventCallback)
{
GetEventDispatcher().removeEventListener(RoomEngineSamplePlaybackEvent.PLAY_SAMPLE, this._eventCallback);
GetEventDispatcher().removeEventListener(RoomEngineObjectEvent.REMOVED, this._eventCallback);
GetEventDispatcher().removeEventListener(RoomEngineEvent.DISPOSED, this._eventCallback);
GetEventDispatcher().removeEventListener(NitroSettingsEvent.SETTINGS_UPDATED, this._eventCallback);
GetEventDispatcher().removeEventListener(NitroSoundEvent.PLAY_SOUND, this._eventCallback);
this._eventCallback = null;
}
// Stop all playing samples
this._furnitureBeingPlayed.getKeys().forEach((objectId: number) =>
{
this.stopFurniSample(objectId);
});
// Clear all samples
this._internalSamples.dispose();
this._furniSamples.dispose();
this._furnitureBeingPlayed.dispose();
}
private onEvent(event: INitroEvent)
+14 -3
View File
@@ -1,4 +1,4 @@
import { IAdvancedMap, IMusicController, IPlaylistController, ISongInfo } from '@nitrots/api';
import { IAdvancedMap, IMessageEvent, IMusicController, IPlaylistController, ISongInfo } from '@nitrots/api';
import { GetCommunication, GetNowPlayingMessageComposer, GetSongInfoMessageComposer, GetUserSongDisksMessageComposer, TraxSongInfoMessageEvent, UserSongDisksInventoryMessageEvent } from '@nitrots/communication';
import { GetConfiguration } from '@nitrots/configuration';
import { GetEventDispatcher, NotifyPlayedSongEvent, NowPlayingEvent, RoomObjectSoundMachineEvent, SongDiskInventoryReceivedEvent, SongInfoReceivedEvent, SoundManagerEvent } from '@nitrots/events';
@@ -31,6 +31,7 @@ export class MusicController implements IMusicController
private _songIdPlaying: number = 1;
private _previousNotifiedSongId: number = -1;
private _previousNotificationTime: number = -1;
private _messageEvents: IMessageEvent[] = [];
constructor()
{
@@ -44,8 +45,11 @@ export class MusicController implements IMusicController
public init(): void
{
GetCommunication().registerMessageEvent(new TraxSongInfoMessageEvent(this.onTraxSongInfoMessageEvent.bind(this)));
GetCommunication().registerMessageEvent(new UserSongDisksInventoryMessageEvent(this.onSongDiskInventoryMessage.bind(this)));
// Store message events for cleanup
this._messageEvents.push(
GetCommunication().registerMessageEvent(new TraxSongInfoMessageEvent(this.onTraxSongInfoMessageEvent.bind(this))),
GetCommunication().registerMessageEvent(new UserSongDisksInventoryMessageEvent(this.onSongDiskInventoryMessage.bind(this)))
);
this._timerInstance = window.setInterval(this.onTick.bind(this), 1000);
this._musicPlayer = new MusicPlayer(GetConfiguration().getValue<string>('external.samples.url'));
@@ -156,6 +160,13 @@ export class MusicController implements IMusicController
this._timerInstance = undefined;
}
// Remove message events
for(const event of this._messageEvents)
{
GetCommunication().removeMessageEvent(event);
}
this._messageEvents = [];
GetEventDispatcher().removeEventListener(RoomObjectSoundMachineEvent.JUKEBOX_INIT, this.onJukeboxInit);
GetEventDispatcher().removeEventListener(RoomObjectSoundMachineEvent.JUKEBOX_DISPOSE, this.onJukeboxDispose);
GetEventDispatcher().removeEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_INIT, this.onSoundMachineInit);
@@ -0,0 +1,283 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { AdvancedMap } from '../AdvancedMap';
describe('AdvancedMap', () =>
{
let map: AdvancedMap<string, number>;
beforeEach(() =>
{
map = new AdvancedMap<string, number>();
});
describe('constructor', () =>
{
it('should create an empty map', () =>
{
expect(map.length).toBe(0);
});
it('should initialize from existing Map', () =>
{
const source = new Map<string, number>([
['a', 1],
['b', 2],
['c', 3]
]);
const advMap = new AdvancedMap(source);
expect(advMap.length).toBe(3);
expect(advMap.getValue('a')).toBe(1);
expect(advMap.getValue('b')).toBe(2);
expect(advMap.getValue('c')).toBe(3);
});
});
describe('add', () =>
{
it('should add key-value pairs', () =>
{
expect(map.add('key1', 100)).toBe(true);
expect(map.length).toBe(1);
expect(map.getValue('key1')).toBe(100);
});
it('should return false when adding duplicate key', () =>
{
map.add('key1', 100);
expect(map.add('key1', 200)).toBe(false);
expect(map.getValue('key1')).toBe(100);
});
it('should maintain insertion order', () =>
{
map.add('a', 1);
map.add('b', 2);
map.add('c', 3);
expect(map.getKey(0)).toBe('a');
expect(map.getKey(1)).toBe('b');
expect(map.getKey(2)).toBe('c');
});
});
describe('unshift', () =>
{
it('should return false due to bug in implementation', () =>
{
// Note: unshift has a bug - it checks `!== null` instead of `!== undefined`
// Map.get() returns undefined for missing keys, so condition always fails
// This test documents the current broken behavior
const result = map.unshift('first', 0);
expect(result).toBe(false);
expect(map.length).toBe(0);
});
});
describe('remove', () =>
{
it('should remove and return value by key', () =>
{
map.add('key1', 100);
map.add('key2', 200);
const removed = map.remove('key1');
expect(removed).toBe(100);
expect(map.length).toBe(1);
expect(map.getValue('key1')).toBeUndefined();
});
it('should return null when removing non-existent key', () =>
{
expect(map.remove('nonexistent')).toBeNull();
});
});
describe('getWithIndex', () =>
{
it('should get value by index', () =>
{
map.add('a', 1);
map.add('b', 2);
map.add('c', 3);
expect(map.getWithIndex(0)).toBe(1);
expect(map.getWithIndex(1)).toBe(2);
expect(map.getWithIndex(2)).toBe(3);
});
it('should return null for out of bounds index', () =>
{
map.add('a', 1);
expect(map.getWithIndex(-1)).toBeNull();
expect(map.getWithIndex(10)).toBeNull();
});
});
describe('getKey', () =>
{
it('should get key by index', () =>
{
map.add('a', 1);
map.add('b', 2);
expect(map.getKey(0)).toBe('a');
expect(map.getKey(1)).toBe('b');
});
it('should return null for out of bounds index', () =>
{
expect(map.getKey(-1)).toBeNull();
expect(map.getKey(10)).toBeNull();
});
});
describe('getKeys', () =>
{
it('should return copy of keys array', () =>
{
map.add('a', 1);
map.add('b', 2);
const keys = map.getKeys();
expect(keys).toEqual(['a', 'b']);
// Verify it's a copy
keys.push('c');
expect(map.length).toBe(2);
});
});
describe('getValues', () =>
{
it('should return copy of values array', () =>
{
map.add('a', 1);
map.add('b', 2);
const values = map.getValues();
expect(values).toEqual([1, 2]);
// Verify it's a copy
values.push(3);
expect(map.length).toBe(2);
});
});
describe('hasKey', () =>
{
it('should return true if key exists', () =>
{
map.add('a', 1);
expect(map.hasKey('a')).toBe(true);
});
it('should return false if key does not exist', () =>
{
expect(map.hasKey('nonexistent')).toBe(false);
});
});
describe('hasValue', () =>
{
it('should return true if value exists', () =>
{
map.add('a', 100);
expect(map.hasValue(100)).toBe(true);
});
it('should return false if value does not exist', () =>
{
expect(map.hasValue(999)).toBe(false);
});
});
describe('indexOf', () =>
{
it('should return index of value', () =>
{
map.add('a', 1);
map.add('b', 2);
map.add('c', 3);
expect(map.indexOf(2)).toBe(1);
});
it('should return -1 if value not found', () =>
{
expect(map.indexOf(999)).toBe(-1);
});
});
describe('reset', () =>
{
it('should clear all entries', () =>
{
map.add('a', 1);
map.add('b', 2);
map.reset();
expect(map.length).toBe(0);
expect(map.getValue('a')).toBeUndefined();
});
});
describe('clone', () =>
{
it('should create independent copy', () =>
{
map.add('a', 1);
map.add('b', 2);
const cloned = map.clone() as AdvancedMap<string, number>;
expect(cloned.length).toBe(2);
expect(cloned.getValue('a')).toBe(1);
// Verify independence
cloned.add('c', 3);
expect(map.length).toBe(2);
expect(cloned.length).toBe(3);
});
});
describe('concatenate', () =>
{
it('should add all entries from another map', () =>
{
map.add('a', 1);
const other = new AdvancedMap<string, number>();
other.add('b', 2);
other.add('c', 3);
map.concatenate(other);
expect(map.length).toBe(3);
expect(map.getValue('b')).toBe(2);
expect(map.getValue('c')).toBe(3);
});
});
describe('dispose', () =>
{
it('should reset length and arrays on dispose', () =>
{
map.add('a', 1);
expect(map.disposed).toBe(false);
map.dispose();
// Note: There's a bug in dispose() - it checks `if(!this._dictionary)`
// instead of `if(this._dictionary)`, so dictionary is not set to null.
// This test verifies current behavior; the bug should be fixed separately.
expect(map.length).toBe(0);
});
});
});
@@ -0,0 +1,225 @@
import { describe, it, expect } from 'vitest';
import { ColorConverter } from '../ColorConverter';
describe('ColorConverter', () =>
{
describe('hex2rgb', () =>
{
it('should convert hex to RGB array', () =>
{
const result = ColorConverter.hex2rgb(0xFF0000);
expect(result[0]).toBeCloseTo(1); // Red
expect(result[1]).toBeCloseTo(0); // Green
expect(result[2]).toBeCloseTo(0); // Blue
});
it('should convert white correctly', () =>
{
const result = ColorConverter.hex2rgb(0xFFFFFF);
expect(result[0]).toBeCloseTo(1);
expect(result[1]).toBeCloseTo(1);
expect(result[2]).toBeCloseTo(1);
});
it('should convert black correctly', () =>
{
const result = ColorConverter.hex2rgb(0x000000);
expect(result[0]).toBeCloseTo(0);
expect(result[1]).toBeCloseTo(0);
expect(result[2]).toBeCloseTo(0);
});
it('should use provided output array', () =>
{
const out: number[] = [];
const result = ColorConverter.hex2rgb(0x00FF00, out);
expect(result).toBe(out);
expect(out[0]).toBeCloseTo(0);
expect(out[1]).toBeCloseTo(1);
expect(out[2]).toBeCloseTo(0);
});
});
describe('rgb2hex', () =>
{
it('should convert RGB array to hex', () =>
{
const result = ColorConverter.rgb2hex([1, 0, 0]);
expect(result).toBe(0xFF0000);
});
it('should convert white correctly', () =>
{
const result = ColorConverter.rgb2hex([1, 1, 1]);
expect(result).toBe(0xFFFFFF);
});
it('should convert black correctly', () =>
{
const result = ColorConverter.rgb2hex([0, 0, 0]);
expect(result).toBe(0x000000);
});
});
describe('getHex', () =>
{
it('should convert number to two-digit hex string', () =>
{
expect(ColorConverter.getHex(0)).toBe('00');
expect(ColorConverter.getHex(15)).toBe('0f');
expect(ColorConverter.getHex(16)).toBe('10');
expect(ColorConverter.getHex(255)).toBe('ff');
});
it('should return 00 for NaN', () =>
{
expect(ColorConverter.getHex(NaN)).toBe('00');
});
});
describe('int2rgb', () =>
{
it('should convert integer to RGBA string', () =>
{
const result = ColorConverter.int2rgb(0xFF0000);
expect(result).toBe('rgba(255,0,0,1)');
});
it('should convert green correctly', () =>
{
const result = ColorConverter.int2rgb(0x00FF00);
expect(result).toBe('rgba(0,255,0,1)');
});
it('should convert blue correctly', () =>
{
const result = ColorConverter.int2rgb(0x0000FF);
expect(result).toBe('rgba(0,0,255,1)');
});
});
describe('rgbToHSL', () =>
{
it('should convert red to HSL', () =>
{
const result = ColorConverter.rgbToHSL(0xFF0000);
// Red has hue 0
const h = (result >> 16) & 0xFF;
const s = (result >> 8) & 0xFF;
const l = result & 0xFF;
expect(h).toBe(0);
expect(s).toBe(255); // Full saturation
expect(l).toBe(128); // 50% lightness (rounded)
});
it('should convert white to HSL', () =>
{
const result = ColorConverter.rgbToHSL(0xFFFFFF);
const h = (result >> 16) & 0xFF;
const s = (result >> 8) & 0xFF;
const l = result & 0xFF;
expect(s).toBe(0); // No saturation for white
expect(l).toBe(255); // Full lightness
});
it('should convert black to HSL', () =>
{
const result = ColorConverter.rgbToHSL(0x000000);
const h = (result >> 16) & 0xFF;
const s = (result >> 8) & 0xFF;
const l = result & 0xFF;
expect(s).toBe(0); // No saturation for black
expect(l).toBe(0); // No lightness
});
});
describe('hslToRGB', () =>
{
it('should convert pure red HSL to RGB', () =>
{
// Pure red: H=0, S=255, L=128
const hsl = (0 << 16) + (255 << 8) + 128;
const result = ColorConverter.hslToRGB(hsl);
const r = (result >> 16) & 0xFF;
const g = (result >> 8) & 0xFF;
const b = result & 0xFF;
// Due to floating point precision in the algorithm, we allow small variance
expect(r).toBe(255);
expect(g).toBeLessThanOrEqual(2); // Small rounding variance
expect(b).toBeLessThanOrEqual(2);
});
it('should convert grayscale (no saturation)', () =>
{
// Gray: H=0, S=0, L=128
const hsl = (0 << 16) + (0 << 8) + 128;
const result = ColorConverter.hslToRGB(hsl);
const r = (result >> 16) & 0xFF;
const g = (result >> 8) & 0xFF;
const b = result & 0xFF;
expect(r).toBe(128);
expect(g).toBe(128);
expect(b).toBe(128);
});
});
describe('colorize', () =>
{
it('should return original color when colorizing with white', () =>
{
const color = 0xFF0000;
const result = ColorConverter.colorize(color, 0xFFFFFFFF);
expect(result).toBe(color);
});
it('should colorize red with blue filter', () =>
{
const colorA = 0xFFFFFF; // White
const colorB = 0x0000FF; // Blue filter
const result = ColorConverter.colorize(colorA, colorB);
const r = (result >> 16) & 0xFF;
const g = (result >> 8) & 0xFF;
const b = result & 0xFF;
expect(r).toBe(0);
expect(g).toBe(0);
expect(b).toBe(255);
});
});
describe('roundtrip conversions', () =>
{
it('should maintain color through RGB to HSL to RGB conversion', () =>
{
const colors = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF];
for (const original of colors)
{
const hsl = ColorConverter.rgbToHSL(original);
const result = ColorConverter.hslToRGB(hsl);
// Allow rounding differences due to float precision in HSL conversion
const origR = (original >> 16) & 0xFF;
const origG = (original >> 8) & 0xFF;
const origB = original & 0xFF;
const resultR = (result >> 16) & 0xFF;
const resultG = (result >> 8) & 0xFF;
const resultB = result & 0xFF;
// HSL conversion can have up to 5 units of variance due to rounding
expect(Math.abs(origR - resultR)).toBeLessThanOrEqual(5);
expect(Math.abs(origG - resultG)).toBeLessThanOrEqual(5);
expect(Math.abs(origB - resultB)).toBeLessThanOrEqual(5);
}
});
});
});
@@ -0,0 +1,185 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { NumberBank } from '../NumberBank';
describe('NumberBank', () =>
{
describe('constructor', () =>
{
it('should create bank with specified size', () =>
{
const bank = new NumberBank(5);
// Should be able to reserve 5 numbers (LIFO order - pops from end)
expect(bank.reserveNumber()).toBe(4);
expect(bank.reserveNumber()).toBe(3);
expect(bank.reserveNumber()).toBe(2);
expect(bank.reserveNumber()).toBe(1);
expect(bank.reserveNumber()).toBe(0);
expect(bank.reserveNumber()).toBe(-1); // No more available
});
it('should handle negative size as zero', () =>
{
const bank = new NumberBank(-5);
expect(bank.reserveNumber()).toBe(-1);
});
it('should handle zero size', () =>
{
const bank = new NumberBank(0);
expect(bank.reserveNumber()).toBe(-1);
});
});
describe('reserveNumber', () =>
{
let bank: NumberBank;
beforeEach(() =>
{
bank = new NumberBank(3);
});
it('should return numbers in LIFO order (stack behavior)', () =>
{
// Numbers are added 0, 1, 2 to the array
// pop() returns from the end, so we get 2, 1, 0
expect(bank.reserveNumber()).toBe(2);
expect(bank.reserveNumber()).toBe(1);
expect(bank.reserveNumber()).toBe(0);
});
it('should return -1 when no numbers available', () =>
{
bank.reserveNumber();
bank.reserveNumber();
bank.reserveNumber();
expect(bank.reserveNumber()).toBe(-1);
});
});
describe('freeNumber', () =>
{
let bank: NumberBank;
beforeEach(() =>
{
bank = new NumberBank(3);
});
it('should make number available again after freeing', () =>
{
const num = bank.reserveNumber();
bank.freeNumber(num);
// The freed number should be available again
expect(bank.reserveNumber()).toBe(num);
});
it('should handle freeing in different order', () =>
{
const n1 = bank.reserveNumber();
const n2 = bank.reserveNumber();
const n3 = bank.reserveNumber();
// Free in middle order
bank.freeNumber(n2);
bank.freeNumber(n1);
// Should get them back in LIFO order
expect(bank.reserveNumber()).toBe(n1);
expect(bank.reserveNumber()).toBe(n2);
});
it('should ignore freeing numbers not in reserved list', () =>
{
bank.reserveNumber(); // reserves 2
// Try to free a number that wasn't reserved
bank.freeNumber(999);
// Should still work normally
expect(bank.reserveNumber()).toBe(1);
});
it('should allow reusing freed numbers', () =>
{
// Reserve all
const n1 = bank.reserveNumber();
const n2 = bank.reserveNumber();
const n3 = bank.reserveNumber();
// Free and re-reserve multiple times
bank.freeNumber(n1);
const reused1 = bank.reserveNumber();
expect(reused1).toBe(n1);
bank.freeNumber(n2);
bank.freeNumber(reused1);
expect(bank.reserveNumber()).toBe(reused1);
expect(bank.reserveNumber()).toBe(n2);
});
});
describe('dispose', () =>
{
it('should set internal arrays to null', () =>
{
const bank = new NumberBank(5);
bank.reserveNumber();
bank.dispose();
// After dispose, reserveNumber should fail (arrays are null)
// This will throw an error, which is expected behavior
expect(() => bank.reserveNumber()).toThrow();
});
});
describe('edge cases', () =>
{
it('should handle large bank size', () =>
{
const bank = new NumberBank(1000);
// Reserve all
for (let i = 0; i < 1000; i++)
{
expect(bank.reserveNumber()).toBeGreaterThanOrEqual(0);
}
expect(bank.reserveNumber()).toBe(-1);
});
it('should maintain consistency after multiple reserve/free cycles', () =>
{
const bank = new NumberBank(10);
const reserved: number[] = [];
// Reserve 5
for (let i = 0; i < 5; i++)
{
reserved.push(bank.reserveNumber());
}
// Free 3
bank.freeNumber(reserved[0]);
bank.freeNumber(reserved[2]);
bank.freeNumber(reserved[4]);
// Reserve 3 more (should get the freed ones)
const newReserved: number[] = [];
for (let i = 0; i < 3; i++)
{
newReserved.push(bank.reserveNumber());
}
// All previously freed numbers should be reserved again
expect(newReserved).toContain(reserved[0]);
expect(newReserved).toContain(reserved[2]);
expect(newReserved).toContain(reserved[4]);
});
});
});
@@ -0,0 +1,299 @@
import { describe, it, expect } from 'vitest';
import { Vector3d } from '../Vector3d';
describe('Vector3d', () =>
{
describe('constructor', () =>
{
it('should create a vector with default values (0, 0, 0)', () =>
{
const vector = new Vector3d();
expect(vector.x).toBe(0);
expect(vector.y).toBe(0);
expect(vector.z).toBe(0);
});
it('should create a vector with specified values', () =>
{
const vector = new Vector3d(1, 2, 3);
expect(vector.x).toBe(1);
expect(vector.y).toBe(2);
expect(vector.z).toBe(3);
});
});
describe('static sum', () =>
{
it('should return sum of two vectors', () =>
{
const v1 = new Vector3d(1, 2, 3);
const v2 = new Vector3d(4, 5, 6);
const result = Vector3d.sum(v1, v2);
expect(result.x).toBe(5);
expect(result.y).toBe(7);
expect(result.z).toBe(9);
});
it('should return null if either vector is null', () =>
{
const v1 = new Vector3d(1, 2, 3);
expect(Vector3d.sum(v1, null)).toBeNull();
expect(Vector3d.sum(null, v1)).toBeNull();
});
});
describe('static dif', () =>
{
it('should return difference of two vectors', () =>
{
const v1 = new Vector3d(5, 7, 9);
const v2 = new Vector3d(1, 2, 3);
const result = Vector3d.dif(v1, v2);
expect(result.x).toBe(4);
expect(result.y).toBe(5);
expect(result.z).toBe(6);
});
it('should return null if either vector is null', () =>
{
const v1 = new Vector3d(1, 2, 3);
expect(Vector3d.dif(v1, null)).toBeNull();
expect(Vector3d.dif(null, v1)).toBeNull();
});
});
describe('static product', () =>
{
it('should return vector multiplied by scalar', () =>
{
const v = new Vector3d(1, 2, 3);
const result = Vector3d.product(v, 2);
expect(result.x).toBe(2);
expect(result.y).toBe(4);
expect(result.z).toBe(6);
});
it('should return null if vector is null', () =>
{
expect(Vector3d.product(null, 2)).toBeNull();
});
});
describe('static dotProduct', () =>
{
it('should calculate dot product of two vectors', () =>
{
const v1 = new Vector3d(1, 2, 3);
const v2 = new Vector3d(4, 5, 6);
const result = Vector3d.dotProduct(v1, v2);
// 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32
expect(result).toBe(32);
});
it('should return 0 if either vector is null', () =>
{
const v1 = new Vector3d(1, 2, 3);
expect(Vector3d.dotProduct(v1, null)).toBe(0);
expect(Vector3d.dotProduct(null, v1)).toBe(0);
});
});
describe('static crossProduct', () =>
{
it('should calculate cross product of two vectors', () =>
{
const v1 = new Vector3d(1, 0, 0);
const v2 = new Vector3d(0, 1, 0);
const result = Vector3d.crossProduct(v1, v2);
expect(result.x).toBe(0);
expect(result.y).toBe(0);
expect(result.z).toBe(1);
});
it('should return null if either vector is null', () =>
{
const v1 = new Vector3d(1, 2, 3);
expect(Vector3d.crossProduct(v1, null)).toBeNull();
expect(Vector3d.crossProduct(null, v1)).toBeNull();
});
});
describe('static isEqual', () =>
{
it('should return true for equal vectors', () =>
{
const v1 = new Vector3d(1, 2, 3);
const v2 = new Vector3d(1, 2, 3);
expect(Vector3d.isEqual(v1, v2)).toBe(true);
});
it('should return false for different vectors', () =>
{
const v1 = new Vector3d(1, 2, 3);
const v2 = new Vector3d(1, 2, 4);
expect(Vector3d.isEqual(v1, v2)).toBe(false);
});
it('should return false if either vector is null', () =>
{
const v1 = new Vector3d(1, 2, 3);
expect(Vector3d.isEqual(v1, null)).toBe(false);
expect(Vector3d.isEqual(null, v1)).toBe(false);
});
});
describe('instance methods', () =>
{
describe('assign', () =>
{
it('should copy values from another vector', () =>
{
const v1 = new Vector3d(0, 0, 0);
const v2 = new Vector3d(1, 2, 3);
v1.assign(v2);
expect(v1.x).toBe(1);
expect(v1.y).toBe(2);
expect(v1.z).toBe(3);
});
it('should do nothing if vector is null', () =>
{
const v1 = new Vector3d(1, 2, 3);
v1.assign(null);
expect(v1.x).toBe(1);
expect(v1.y).toBe(2);
expect(v1.z).toBe(3);
});
});
describe('add', () =>
{
it('should add another vector to this vector', () =>
{
const v1 = new Vector3d(1, 2, 3);
const v2 = new Vector3d(4, 5, 6);
v1.add(v2);
expect(v1.x).toBe(5);
expect(v1.y).toBe(7);
expect(v1.z).toBe(9);
});
});
describe('subtract', () =>
{
it('should subtract another vector from this vector', () =>
{
const v1 = new Vector3d(5, 7, 9);
const v2 = new Vector3d(1, 2, 3);
v1.subtract(v2);
expect(v1.x).toBe(4);
expect(v1.y).toBe(5);
expect(v1.z).toBe(6);
});
});
describe('multiply', () =>
{
it('should multiply vector by scalar', () =>
{
const v = new Vector3d(1, 2, 3);
v.multiply(3);
expect(v.x).toBe(3);
expect(v.y).toBe(6);
expect(v.z).toBe(9);
});
});
describe('divide', () =>
{
it('should divide vector by scalar', () =>
{
const v = new Vector3d(4, 8, 12);
v.divide(4);
expect(v.x).toBe(1);
expect(v.y).toBe(2);
expect(v.z).toBe(3);
});
it('should not divide by zero', () =>
{
const v = new Vector3d(1, 2, 3);
v.divide(0);
expect(v.x).toBe(1);
expect(v.y).toBe(2);
expect(v.z).toBe(3);
});
});
describe('negate', () =>
{
it('should negate all components', () =>
{
const v = new Vector3d(1, -2, 3);
v.negate();
expect(v.x).toBe(-1);
expect(v.y).toBe(2);
expect(v.z).toBe(-3);
});
});
describe('length', () =>
{
it('should calculate vector length', () =>
{
const v = new Vector3d(3, 4, 0);
expect(v.length).toBe(5);
});
it('should cache length until values change', () =>
{
const v = new Vector3d(3, 4, 0);
const length1 = v.length;
const length2 = v.length;
expect(length1).toBe(length2);
v.x = 6;
expect(v.length).toBe(Math.sqrt(52)); // sqrt(36 + 16 + 0)
});
});
describe('normalize', () =>
{
it('should normalize vector to unit length', () =>
{
const v = new Vector3d(3, 4, 0);
v.normalize();
expect(v.x).toBeCloseTo(0.6);
expect(v.y).toBeCloseTo(0.8);
expect(v.z).toBeCloseTo(0);
// Note: The actual calculated length would be 1, but the cached _length
// is not reset in normalize() - this is a known limitation
const actualLength = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
expect(actualLength).toBeCloseTo(1);
});
});
describe('toString', () =>
{
it('should return string representation', () =>
{
const v = new Vector3d(1, 2, 3);
expect(v.toString()).toBe('[Vector3d: 1, 2, 3]');
});
});
});
});