UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

5 lines 61.1 kB
{ "version": 3, "sources": ["../src/Room.ts"], "sourcesContent": ["import { unpack } from '@colyseus/msgpackr';\nimport { decode, type Iterator, $changes } from '@colyseus/schema';\n\nimport Clock from '@colyseus/timer';\nimport { EventEmitter } from 'events';\nimport { logger } from './Logger.js';\n\nimport { Presence } from './presence/Presence.js';\n\nimport { NoneSerializer } from './serializer/NoneSerializer.js';\nimport { SchemaSerializer } from './serializer/SchemaSerializer.js';\nimport { Serializer } from './serializer/Serializer.js';\n\nimport { ErrorCode, getMessageBytes, Protocol } from './Protocol.js';\nimport { Deferred, generateId, wrapTryCatch } from './utils/Utils.js';\nimport { isDevMode } from './utils/DevMode.js';\n\nimport { debugAndPrintError, debugMatchMaking, debugMessage } from './Debug.js';\nimport { RoomCache } from './matchmaker/driver/api.js';\nimport { ServerError } from './errors/ServerError.js';\nimport { AuthContext, Client, ClientPrivate, ClientArray, ClientState, ISendOptions } from './Transport.js';\nimport { OnAuthException, OnCreateException, OnDisposeException, OnJoinException, OnLeaveException, OnMessageException, RoomException, SimulationIntervalException, TimedEventException } from './errors/RoomExceptions.js';\n\nconst DEFAULT_PATCH_RATE = 1000 / 20; // 20fps (50ms)\nconst DEFAULT_SIMULATION_INTERVAL = 1000 / 60; // 60fps (16.66ms)\nconst noneSerializer = new NoneSerializer();\n\nexport const DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);\n\nexport type SimulationCallback = (deltaTime: number) => void;\n\nexport interface IBroadcastOptions extends ISendOptions {\n except?: Client | Client[];\n}\n\nexport enum RoomInternalState {\n CREATING = 0,\n CREATED = 1,\n DISPOSING = 2,\n}\n\nexport type ExtractUserData<T> = T extends ClientArray<infer U> ? U : never;\nexport type ExtractAuthData<T> = T extends ClientArray<infer _, infer U> ? U : never;\n\n/**\n * A Room class is meant to implement a game session, and/or serve as the communication channel\n * between a group of clients.\n *\n * - Rooms are created on demand during matchmaking by default\n * - Room classes must be exposed using `.define()`\n */\nexport abstract class Room<State extends object= any, Metadata= any, UserData = any, AuthData = any> {\n\n /**\n * This property will change on these situations:\n * - The maximum number of allowed clients has been reached (`maxClients`)\n * - You manually locked, or unlocked the room using lock() or `unlock()`.\n *\n * @readonly\n */\n public get locked() {\n return this.#_locked;\n }\n\n public get metadata() {\n return this.listing.metadata;\n }\n\n public listing: RoomCache<Metadata>;\n\n /**\n * Timing events tied to the room instance.\n * Intervals and timeouts are cleared when the room is disposed.\n */\n public clock: Clock = new Clock();\n\n #_roomId: string;\n #_roomName: string;\n #_onLeaveConcurrent: number = 0; // number of onLeave calls in progress\n\n /**\n * Maximum number of clients allowed to connect into the room. When room reaches this limit,\n * it is locked automatically. Unless the room was explicitly locked by you via `lock()` method,\n * the room will be unlocked as soon as a client disconnects from it.\n */\n public maxClients: number = Infinity;\n #_maxClientsReached: boolean = false;\n #_maxClients: number;\n\n /**\n * Automatically dispose the room when last client disconnects.\n *\n * @default true\n */\n public autoDispose: boolean = true;\n #_autoDispose: boolean;\n\n /**\n * Frequency to send the room state to connected clients, in milliseconds.\n *\n * @default 50ms (20fps)\n */\n public patchRate: number = DEFAULT_PATCH_RATE;\n #_patchRate: number;\n #_patchInterval: NodeJS.Timeout;\n\n /**\n * The state instance you provided to `setState()`.\n */\n public state: State;\n #_state: State;\n\n /**\n * The presence instance. Check Presence API for more details.\n *\n * @see {@link https://docs.colyseus.io/colyseus/server/presence/|Presence API}\n */\n public presence: Presence;\n\n /**\n * The array of connected clients.\n *\n * @see {@link https://docs.colyseus.io/colyseus/server/room/#client|Client instance}\n */\n public clients: ClientArray<UserData, AuthData> = new ClientArray();\n\n /** @internal */\n public _events = new EventEmitter();\n\n // seat reservation & reconnection\n protected seatReservationTime: number = DEFAULT_SEAT_RESERVATION_TIME;\n protected reservedSeats: { [sessionId: string]: [any, any, boolean?, boolean?] } = {};\n protected reservedSeatTimeouts: { [sessionId: string]: NodeJS.Timeout } = {};\n\n protected _reconnections: { [reconnectionToken: string]: [string, Deferred] } = {};\n private _reconnectingSessionId = new Map<string, string>();\n\n private onMessageHandlers: {\n [id: string]: {\n callback: (...args: any[]) => void,\n validate?: (data: unknown) => any,\n }\n } = {\n '__no_message_handler': {\n callback: (client: Client, messageType: string, _: unknown) => {\n const errorMessage = `room onMessage for \"${messageType}\" not registered.`;\n debugAndPrintError(`${errorMessage} (roomId: ${this.roomId})`);\n\n if (isDevMode) {\n // send error code to client in development mode\n client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);\n\n } else {\n // immediately close the connection in production\n client.leave(Protocol.WS_CLOSE_WITH_ERROR, errorMessage);\n }\n }\n }\n };\n\n private _serializer: Serializer<State> = noneSerializer;\n private _afterNextPatchQueue: Array<[string | Client, IArguments]> = [];\n\n private _simulationInterval: NodeJS.Timeout;\n\n private _internalState: RoomInternalState = RoomInternalState.CREATING;\n\n private _lockedExplicitly: boolean = false;\n #_locked: boolean = false;\n\n // this timeout prevents rooms that are created by one process, but no client\n // ever had success joining into it on the specified interval.\n private _autoDisposeTimeout: NodeJS.Timeout;\n\n constructor() {\n this._events.once('dispose', () => {\n this._dispose()\n .catch((e) => debugAndPrintError(`onDispose error: ${(e && e.stack || e.message || e || 'promise rejected')} (roomId: ${this.roomId})`))\n .finally(() => this._events.emit('disconnect'));\n });\n\n /**\n * If `onUncaughtException` is defined, it will automatically catch exceptions\n */\n if (this.onUncaughtException !== undefined) {\n this.#registerUncaughtExceptionHandlers();\n }\n }\n\n /**\n * This method is called by the MatchMaker before onCreate()\n * @internal\n */\n protected __init() {\n this.#_state = this.state;\n this.#_autoDispose = this.autoDispose;\n this.#_patchRate = this.patchRate;\n this.#_maxClients = this.maxClients;\n\n Object.defineProperties(this, {\n state: {\n enumerable: true,\n get: () => this.#_state,\n set: (newState: State) => {\n if (newState?.constructor[Symbol.metadata] !== undefined || newState[$changes] !== undefined) {\n this.setSerializer(new SchemaSerializer());\n } else if ('_definition' in newState) {\n throw new Error(\"@colyseus/schema v2 compatibility currently missing (reach out if you need it)\");\n } else if ($changes === undefined) {\n throw new Error(\"Multiple @colyseus/schema versions detected. Please make sure you don't have multiple versions of @colyseus/schema installed.\");\n }\n this._serializer.reset(newState);\n this.#_state = newState;\n },\n },\n\n maxClients: {\n enumerable: true,\n get: () => this.#_maxClients,\n set: (value: number) => {\n this.#_maxClients = value;\n\n if (this._internalState === RoomInternalState.CREATED) {\n const hasReachedMaxClients = this.hasReachedMaxClients();\n\n // unlock room if maxClients has been increased\n if (!this._lockedExplicitly && this.#_maxClientsReached && !hasReachedMaxClients) {\n this.#_maxClientsReached = false;\n this.#_locked = false;\n this.listing.locked = false;\n }\n\n // lock room if maxClients has been decreased\n if (hasReachedMaxClients) {\n this.#_maxClientsReached = true;\n this.#_locked = true;\n this.listing.locked = true;\n }\n\n this.listing.maxClients = value;\n this.listing.save();\n }\n },\n },\n\n autoDispose: {\n enumerable: true,\n get: () => this.#_autoDispose,\n set: (value: boolean) => {\n if (\n value !== this.#_autoDispose &&\n this._internalState !== RoomInternalState.DISPOSING\n ) {\n this.#_autoDispose = value;\n this.resetAutoDisposeTimeout();\n }\n },\n },\n\n patchRate: {\n enumerable: true,\n get: () => this.#_patchRate,\n set: (milliseconds: number) => {\n this.#_patchRate = milliseconds;\n // clear previous interval in case called setPatchRate more than once\n if (this.#_patchInterval) {\n clearInterval(this.#_patchInterval);\n this.#_patchInterval = undefined;\n }\n if (milliseconds !== null && milliseconds !== 0) {\n this.#_patchInterval = setInterval(() => this.broadcastPatch(), milliseconds);\n }\n },\n },\n });\n\n // set patch interval, now with the setter\n this.patchRate = this.#_patchRate;\n\n // set state, now with the setter\n if (this.#_state) {\n this.state = this.#_state;\n }\n\n // set default _autoDisposeTimeout\n this.resetAutoDisposeTimeout(this.seatReservationTime);\n\n this.clock.start();\n }\n\n /**\n * The name of the room you provided as first argument for `gameServer.define()`.\n *\n * @returns roomName string\n */\n public get roomName() { return this.#_roomName; }\n /**\n * Setting the name of the room. Overwriting this property is restricted.\n *\n * @param roomName\n */\n public set roomName(roomName: string) {\n if (this.#_roomName) {\n // prevent user from setting roomName after it has been defined.\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"'roomName' cannot be overwritten.\");\n }\n this.#_roomName = roomName;\n }\n\n /**\n * A unique, auto-generated, 9-character-long id of the room.\n * You may replace `this.roomId` during `onCreate()`.\n *\n * @returns roomId string\n */\n public get roomId() { return this.#_roomId; }\n\n /**\n * Setting the roomId, is restricted in room lifetime except upon room creation.\n *\n * @param roomId\n * @returns roomId string\n */\n public set roomId(roomId: string) {\n if (this._internalState !== RoomInternalState.CREATING && !isDevMode) {\n // prevent user from setting roomId after room has been created.\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"'roomId' can only be overridden upon room creation.\");\n }\n this.#_roomId = roomId;\n }\n\n // Optional abstract methods\n public onBeforePatch?(state: State): void | Promise<any>;\n public onCreate?(options: any): void | Promise<any>;\n public onJoin?(client: Client<UserData, AuthData>, options?: any, auth?: AuthData): void | Promise<any>;\n public onLeave?(client: Client<UserData, AuthData>, consented?: boolean): void | Promise<any>;\n public onDispose?(): void | Promise<any>;\n\n /**\n * Define a custom exception handler.\n * If defined, all lifecycle hooks will be wrapped by try/catch, and the exception will be forwarded to this method.\n *\n * These methods will be wrapped by try/catch:\n * - `onMessage`\n * - `onAuth` / `onJoin` / `onLeave` / `onCreate` / `onDispose`\n * - `clock.setTimeout` / `clock.setInterval`\n * - `setSimulationInterval`\n *\n * (Experimental: this feature is subject to change in the future - we're currently getting feedback to improve it)\n */\n public onUncaughtException?(error: RoomException<this>, methodName: 'onCreate' | 'onAuth' | 'onJoin' | 'onLeave' | 'onDispose' | 'onMessage' | 'setSimulationInterval' | 'setInterval' | 'setTimeout'): void;\n\n public onAuth(\n client: Client<UserData, AuthData>,\n options: any,\n context: AuthContext\n ): any | Promise<any> {\n return true;\n }\n\n static async onAuth(\n token: string,\n options: any,\n context: AuthContext\n ): Promise<unknown> {\n return true;\n }\n\n /**\n * This method is called during graceful shutdown of the server process\n * You may override this method to dispose the room in your own way.\n *\n * Once process reaches room count of 0, the room process will be terminated.\n */\n public onBeforeShutdown() {\n this.disconnect(\n (isDevMode)\n ? Protocol.WS_CLOSE_DEVMODE_RESTART\n : Protocol.WS_CLOSE_CONSENTED\n );\n }\n\n /**\n * devMode: When `devMode` is enabled, `onCacheRoom` method is called during\n * graceful shutdown.\n *\n * Implement this method to return custom data to be cached. `onRestoreRoom`\n * will be called with the data returned by `onCacheRoom`\n */\n public onCacheRoom?(): any;\n\n /**\n * devMode: When `devMode` is enabled, `onRestoreRoom` method is called during\n * process startup, with the data returned by the `onCacheRoom` method.\n */\n public onRestoreRoom?(cached?: any): void;\n\n /**\n * Returns whether the sum of connected clients and reserved seats exceeds maximum number of clients.\n *\n * @returns boolean\n */\n public hasReachedMaxClients(): boolean {\n return (\n (this.clients.length + Object.keys(this.reservedSeats).length) >= this.maxClients ||\n this._internalState === RoomInternalState.DISPOSING\n );\n }\n\n /**\n * Set the number of seconds a room can wait for a client to effectively join the room.\n * You should consider how long your `onAuth()` will have to wait for setting a different seat reservation time.\n * The default value is 15 seconds. You may set the `COLYSEUS_SEAT_RESERVATION_TIME`\n * environment variable if you'd like to change the seat reservation time globally.\n *\n * @default 15 seconds\n *\n * @param seconds - number of seconds.\n * @returns The modified Room object.\n */\n public setSeatReservationTime(seconds: number) {\n this.seatReservationTime = seconds;\n return this;\n }\n\n public hasReservedSeat(sessionId: string, reconnectionToken?: string): boolean {\n const reservedSeat = this.reservedSeats[sessionId];\n\n // seat reservation not found / expired\n if (reservedSeat === undefined) {\n return false;\n }\n\n if (reservedSeat[3]) {\n // reconnection\n return (\n reconnectionToken &&\n this._reconnections[reconnectionToken]?.[0] === sessionId &&\n this._reconnectingSessionId.has(sessionId)\n );\n\n } else {\n // seat reservation not consumed\n return reservedSeat[2] === false;\n }\n }\n\n public checkReconnectionToken(reconnectionToken: string) {\n const sessionId = this._reconnections[reconnectionToken]?.[0];\n const reservedSeat = this.reservedSeats[sessionId];\n\n if (reservedSeat && reservedSeat[3]) {\n this._reconnectingSessionId.set(sessionId, reconnectionToken);\n return sessionId;\n\n } else {\n return undefined;\n }\n }\n\n /**\n * (Optional) Set a simulation interval that can change the state of the game.\n * The simulation interval is your game loop.\n *\n * @default 16.6ms (60fps)\n *\n * @param onTickCallback - You can implement your physics or world updates here!\n * This is a good place to update the room state.\n * @param delay - Interval delay on executing `onTickCallback` in milliseconds.\n */\n public setSimulationInterval(onTickCallback?: SimulationCallback, delay: number = DEFAULT_SIMULATION_INTERVAL): void {\n // clear previous interval in case called setSimulationInterval more than once\n if (this._simulationInterval) { clearInterval(this._simulationInterval); }\n\n if (onTickCallback) {\n if (this.onUncaughtException !== undefined) {\n onTickCallback = wrapTryCatch(onTickCallback, this.onUncaughtException.bind(this), SimulationIntervalException, 'setSimulationInterval');\n }\n\n this._simulationInterval = setInterval(() => {\n this.clock.tick();\n onTickCallback(this.clock.deltaTime);\n }, delay);\n }\n }\n\n /**\n * @deprecated Use `.patchRate=` instead.\n */\n public setPatchRate(milliseconds: number | null): void {\n this.patchRate = milliseconds;\n }\n\n /**\n * @deprecated Use `.state =` instead.\n */\n public setState(newState: State) {\n this.state = newState;\n }\n\n public setSerializer(serializer: Serializer<State>) {\n this._serializer = serializer;\n }\n\n public async setMetadata(meta: Partial<Metadata>) {\n if (!this.listing.metadata) {\n this.listing.metadata = meta as Metadata;\n\n } else {\n for (const field in meta) {\n if (!meta.hasOwnProperty(field)) { continue; }\n this.listing.metadata[field] = meta[field];\n }\n\n // `MongooseDriver` workaround: persit metadata mutations\n if ('markModified' in this.listing) {\n (this.listing as any).markModified('metadata');\n }\n }\n\n if (this._internalState === RoomInternalState.CREATED) {\n await this.listing.save();\n }\n }\n\n public async setPrivate(bool: boolean = true) {\n if (this.listing.private === bool) return;\n\n this.listing.private = bool;\n\n if (this._internalState === RoomInternalState.CREATED) {\n await this.listing.save();\n }\n\n this._events.emit('visibility-change', bool);\n }\n\n /**\n * Locking the room will remove it from the pool of available rooms for new clients to connect to.\n */\n public async lock() {\n // rooms locked internally aren't explicit locks.\n this._lockedExplicitly = (arguments[0] === undefined);\n\n // skip if already locked.\n if (this.#_locked) { return; }\n\n this.#_locked = true;\n\n await this.listing.updateOne({\n $set: { locked: this.#_locked },\n });\n\n this._events.emit('lock');\n }\n\n /**\n * Unlocking the room returns it to the pool of available rooms for new clients to connect to.\n */\n public async unlock() {\n // only internal usage passes arguments to this function.\n if (arguments[0] === undefined) {\n this._lockedExplicitly = false;\n }\n\n // skip if already locked\n if (!this.#_locked) { return; }\n\n this.#_locked = false;\n\n await this.listing.updateOne({\n $set: { locked: this.#_locked },\n });\n\n this._events.emit('unlock');\n }\n\n public send(client: Client, type: string | number, message: any, options?: ISendOptions): void;\n public send(client: Client, messageOrType: any, messageOrOptions?: any | ISendOptions, options?: ISendOptions): void {\n logger.warn('DEPRECATION WARNING: use client.send(...) instead of this.send(client, ...)');\n client.send(messageOrType, messageOrOptions, options);\n }\n\n public broadcast(type: string | number, message?: any, options?: IBroadcastOptions) {\n if (options && options.afterNextPatch) {\n delete options.afterNextPatch;\n this._afterNextPatchQueue.push(['broadcast', arguments]);\n return;\n }\n\n this.broadcastMessageType(type, message, options);\n }\n\n /**\n * Broadcast bytes (UInt8Arrays) to a particular room\n */\n public broadcastBytes(type: string | number, message: Uint8Array, options: IBroadcastOptions) {\n if (options && options.afterNextPatch) {\n delete options.afterNextPatch;\n this._afterNextPatchQueue.push(['broadcastBytes', arguments]);\n return;\n }\n\n this.broadcastMessageType(type as string, message, options);\n }\n\n /**\n * Checks whether mutations have occurred in the state, and broadcast them to all connected clients.\n */\n public broadcastPatch() {\n if (this.onBeforePatch) {\n this.onBeforePatch(this.state);\n }\n\n if (!this._simulationInterval) {\n this.clock.tick();\n }\n\n if (!this.state) {\n return false;\n }\n\n const hasChanges = this._serializer.applyPatches(this.clients, this.state);\n\n // broadcast messages enqueued for \"after patch\"\n this._dequeueAfterPatchMessages();\n\n return hasChanges;\n }\n\n public onMessage<T = any>(\n messageType: '*',\n callback: (client: Client<UserData, AuthData>, type: string | number, message: T) => void\n );\n public onMessage<T = any>(\n messageType: string | number,\n callback: (client: Client<UserData, AuthData>, message: T) => void,\n validate?: (message: unknown) => T,\n );\n public onMessage<T = any>(\n messageType: '*' | string | number,\n callback: (...args: any[]) => void,\n validate?: (message: unknown) => T,\n ) {\n this.onMessageHandlers[messageType] = (this.onUncaughtException !== undefined)\n ? { validate, callback: wrapTryCatch(callback, this.onUncaughtException.bind(this), OnMessageException, 'onMessage', false, messageType) }\n : { validate, callback };\n\n\n // returns a method to unbind the callback\n return () => delete this.onMessageHandlers[messageType];\n }\n\n /**\n * Disconnect all connected clients, and then dispose the room.\n *\n * @param closeCode WebSocket close code (default = 4000, which is a \"consented leave\")\n * @returns Promise<void>\n */\n public disconnect(closeCode: number = Protocol.WS_CLOSE_CONSENTED): Promise<any> {\n // skip if already disposing\n if (this._internalState === RoomInternalState.DISPOSING) {\n return Promise.resolve(`disconnect() ignored: room (${this.roomId}) is already disposing.`);\n\n } else if (this._internalState === RoomInternalState.CREATING) {\n throw new Error(\"cannot disconnect during onCreate()\");\n }\n\n this._internalState = RoomInternalState.DISPOSING;\n this.listing.remove();\n\n this.#_autoDispose = true;\n\n const delayedDisconnection = new Promise<void>((resolve) =>\n this._events.once('disconnect', () => resolve()));\n\n // reject pending reconnections\n for (const [_, reconnection] of Object.values(this._reconnections)) {\n reconnection.reject(new Error(\"disconnecting\"));\n }\n\n let numClients = this.clients.length;\n if (numClients > 0) {\n // clients may have `async onLeave`, room will be disposed after they're fulfilled\n while (numClients--) {\n this._forciblyCloseClient(this.clients[numClients] as Client & ClientPrivate, closeCode);\n }\n\n } else {\n // no clients connected, dispose immediately.\n this._events.emit('dispose');\n }\n\n return delayedDisconnection;\n }\n\n public async ['_onJoin'](client: Client & ClientPrivate, authContext: AuthContext) {\n const sessionId = client.sessionId;\n\n // generate unique private reconnection token\n client.reconnectionToken = generateId();\n\n if (this.reservedSeatTimeouts[sessionId]) {\n clearTimeout(this.reservedSeatTimeouts[sessionId]);\n delete this.reservedSeatTimeouts[sessionId];\n }\n\n // clear auto-dispose timeout.\n if (this._autoDisposeTimeout) {\n clearTimeout(this._autoDisposeTimeout);\n this._autoDisposeTimeout = undefined;\n }\n\n // get seat reservation options and clear it\n const [joinOptions, authData, isConsumed, isWaitingReconnection] = this.reservedSeats[sessionId];\n\n //\n // TODO: remove this check on 1.0.0\n // - the seat reservation is used to keep track of number of clients and their pending seats (see `hasReachedMaxClients`)\n // - when we fully migrate to static onAuth(), the seat reservation can be removed immediately here\n // - if async onAuth() is in use, the seat reservation is removed after onAuth() is fulfilled.\n // - mark reservation as \"consumed\"\n //\n if (isConsumed) {\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, \"already consumed\");\n }\n this.reservedSeats[sessionId][2] = true; // flag seat reservation as \"consumed\"\n debugMatchMaking('consuming seat reservation, sessionId: \\'%s\\' (roomId: %s)', client.sessionId, this.roomId);\n\n // share \"after next patch queue\" reference with every client.\n client._afterNextPatchQueue = this._afterNextPatchQueue;\n\n // add temporary callback to keep track of disconnections during `onJoin`.\n client.ref['onleave'] = (_) => client.state = ClientState.LEAVING;\n client.ref.once('close', client.ref['onleave']);\n\n if (isWaitingReconnection) {\n const previousReconnectionToken = this._reconnectingSessionId.get(sessionId);\n if (previousReconnectionToken) {\n this.clients.push(client);\n //\n // await for reconnection:\n // (end user may customize the reconnection token at this step)\n //\n await this._reconnections[previousReconnectionToken]?.[1].resolve(client);\n\n } else {\n const errorMessage = (process.env.NODE_ENV === 'production')\n ? \"already consumed\" // trick possible fraudsters...\n : \"bad reconnection token\" // ...or developers\n throw new ServerError(ErrorCode.MATCHMAKE_EXPIRED, errorMessage);\n }\n\n } else {\n try {\n if (authData) {\n client.auth = authData;\n\n } else if (this.onAuth !== Room.prototype.onAuth) {\n try {\n client.auth = await this.onAuth(client, joinOptions, authContext);\n\n if (!client.auth) {\n throw new ServerError(ErrorCode.AUTH_FAILED, 'onAuth failed');\n }\n\n } catch (e) {\n // remove seat reservation\n delete this.reservedSeats[sessionId];\n await this._decrementClientCount();\n throw e;\n }\n }\n\n //\n // On async onAuth, client may have been disconnected.\n //\n if (client.state === ClientState.LEAVING) {\n throw new ServerError(Protocol.WS_CLOSE_GOING_AWAY, 'already disconnected');\n }\n\n this.clients.push(client);\n\n //\n // Flag sessionId as non-enumarable so hasReachedMaxClients() doesn't count it\n // (https://github.com/colyseus/colyseus/issues/726)\n //\n Object.defineProperty(this.reservedSeats, sessionId, {\n value: this.reservedSeats[sessionId],\n enumerable: false,\n });\n\n if (this.onJoin) {\n await this.onJoin(client, joinOptions, client.auth);\n }\n\n // @ts-ignore: client left during `onJoin`, call _onLeave immediately.\n if (client.state === ClientState.LEAVING) {\n throw new Error(\"early_leave\");\n\n } else {\n // remove seat reservation\n delete this.reservedSeats[sessionId];\n\n // emit 'join' to room handler\n this._events.emit('join', client);\n }\n\n } catch (e) {\n await this._onLeave(client, Protocol.WS_CLOSE_GOING_AWAY);\n\n // remove seat reservation\n delete this.reservedSeats[sessionId];\n\n // make sure an error code is provided.\n if (!e.code) {\n e.code = ErrorCode.APPLICATION_ERROR;\n }\n\n throw e;\n }\n }\n\n // state might already be ClientState.LEAVING here\n if (client.state === ClientState.JOINING) {\n client.ref.removeListener('close', client.ref['onleave']);\n\n // only bind _onLeave after onJoin has been successful\n client.ref['onleave'] = this._onLeave.bind(this, client);\n client.ref.once('close', client.ref['onleave']);\n\n // allow client to send messages after onJoin has succeeded.\n client.ref.on('message', this._onMessage.bind(this, client));\n\n // confirm room id that matches the room name requested to join\n client.raw(getMessageBytes[Protocol.JOIN_ROOM](\n client.reconnectionToken,\n this._serializer.id,\n this._serializer.handshake && this._serializer.handshake(),\n ));\n }\n }\n\n /**\n * Allow the specified client to reconnect into the room. Must be used inside `onLeave()` method.\n * If seconds is provided, the reconnection is going to be cancelled after the provided amount of seconds.\n *\n * @param previousClient - The client which is to be waiting until re-connection happens.\n * @param seconds - Timeout period on re-connection in seconds.\n *\n * @returns Deferred<Client> - The differed is a promise like type.\n * This type can forcibly reject the promise by calling `.reject()`.\n */\n public allowReconnection(previousClient: Client, seconds: number | \"manual\"): Deferred<Client> {\n //\n // Return rejected promise if client has never fully JOINED.\n //\n // (having `_enqueuedMessages !== undefined` means that the client has never been at \"ClientState.JOINED\" state)\n //\n if ((previousClient as unknown as ClientPrivate)._enqueuedMessages !== undefined) {\n // @ts-ignore\n return Promise.reject(new Error(\"not joined\"));\n }\n\n if (seconds === undefined) { // TODO: remove this check\n console.warn(\"DEPRECATED: allowReconnection() requires a second argument. Using \\\"manual\\\" mode.\");\n seconds = \"manual\";\n }\n\n if (seconds === \"manual\") {\n seconds = Infinity;\n }\n\n if (this._internalState === RoomInternalState.DISPOSING) {\n // @ts-ignore\n return Promise.reject(new Error(\"disposing\"));\n }\n\n const sessionId = previousClient.sessionId;\n const reconnectionToken = previousClient.reconnectionToken;\n\n this._reserveSeat(sessionId, true, previousClient.auth, seconds, true);\n\n // keep reconnection reference in case the user reconnects into this room.\n const reconnection = new Deferred<Client & ClientPrivate>();\n this._reconnections[reconnectionToken] = [sessionId, reconnection];\n\n if (seconds !== Infinity) {\n // expire seat reservation after timeout\n this.reservedSeatTimeouts[sessionId] = setTimeout(() =>\n reconnection.reject(false), seconds * 1000);\n }\n\n const cleanup = () => {\n delete this._reconnections[reconnectionToken];\n delete this.reservedSeats[sessionId];\n delete this.reservedSeatTimeouts[sessionId];\n this._reconnectingSessionId.delete(sessionId);\n };\n\n reconnection.\n then((newClient) => {\n newClient.auth = previousClient.auth;\n newClient.userData = previousClient.userData;\n newClient.view = previousClient.view;\n\n // for convenience: populate previous client reference with new client\n previousClient.state = ClientState.RECONNECTED;\n previousClient.ref = newClient.ref;\n previousClient.reconnectionToken = newClient.reconnectionToken;\n clearTimeout(this.reservedSeatTimeouts[sessionId]);\n cleanup();\n }).\n catch(() => {\n cleanup();\n this.resetAutoDisposeTimeout();\n });\n\n return reconnection;\n }\n\n protected resetAutoDisposeTimeout(timeoutInSeconds: number = 1) {\n clearTimeout(this._autoDisposeTimeout);\n\n if (!this.#_autoDispose) {\n return;\n }\n\n this._autoDisposeTimeout = setTimeout(() => {\n this._autoDisposeTimeout = undefined;\n this._disposeIfEmpty();\n }, timeoutInSeconds * 1000);\n }\n\n private broadcastMessageType(type: number | string, message?: any | Uint8Array, options: IBroadcastOptions = {}) {\n debugMessage(\"broadcast: %O (roomId: %s)\", message, this.roomId);\n\n const encodedMessage = (message instanceof Uint8Array)\n ? getMessageBytes.raw(Protocol.ROOM_DATA_BYTES, type, undefined, message)\n : getMessageBytes.raw(Protocol.ROOM_DATA, type, message)\n\n const except = (typeof (options.except) !== \"undefined\")\n ? Array.isArray(options.except)\n ? options.except\n : [options.except]\n : undefined;\n\n let numClients = this.clients.length;\n while (numClients--) {\n const client = this.clients[numClients];\n\n if (!except || !except.includes(client)) {\n client.enqueueRaw(encodedMessage);\n }\n }\n }\n\n protected sendFullState(client: Client): void {\n client.raw(this._serializer.getFullState(client));\n }\n\n protected _dequeueAfterPatchMessages() {\n const length = this._afterNextPatchQueue.length;\n\n if (length > 0) {\n for (let i = 0; i < length; i++) {\n const [target, args] = this._afterNextPatchQueue[i];\n\n if (target === \"broadcast\") {\n this.broadcast.apply(this, args);\n\n } else {\n (target as Client).raw.apply(target, args);\n }\n }\n\n // new messages may have been added in the meantime,\n // let's splice the ones that have been processed\n this._afterNextPatchQueue.splice(0, length);\n }\n }\n\n protected async _reserveSeat(\n sessionId: string,\n joinOptions: any = true,\n authData: any = undefined,\n seconds: number = this.seatReservationTime,\n allowReconnection: boolean = false,\n devModeReconnection?: boolean,\n ) {\n if (!allowReconnection && this.hasReachedMaxClients()) {\n return false;\n }\n\n this.reservedSeats[sessionId] = [joinOptions, authData, false, allowReconnection];\n\n if (!allowReconnection) {\n await this._incrementClientCount();\n\n this.reservedSeatTimeouts[sessionId] = setTimeout(async () => {\n delete this.reservedSeats[sessionId];\n delete this.reservedSeatTimeouts[sessionId];\n await this._decrementClientCount();\n }, seconds * 1000);\n\n this.resetAutoDisposeTimeout(seconds);\n }\n\n //\n // isDevMode workaround to allow players to reconnect on devMode\n //\n if (devModeReconnection) {\n this._reconnectingSessionId.set(sessionId, sessionId);\n }\n\n return true;\n }\n\n protected _disposeIfEmpty() {\n const willDispose = (\n this.#_onLeaveConcurrent === 0 && // no \"onLeave\" calls in progress\n this.#_autoDispose &&\n this._autoDisposeTimeout === undefined &&\n this.clients.length === 0 &&\n Object.keys(this.reservedSeats).length === 0\n );\n\n if (willDispose) {\n this._events.emit('dispose');\n }\n\n return willDispose;\n }\n\n protected async _dispose(): Promise<any> {\n this._internalState = RoomInternalState.DISPOSING;\n\n this.listing.remove();\n\n let userReturnData;\n if (this.onDispose) {\n userReturnData = this.onDispose();\n }\n\n if (this.#_patchInterval) {\n clearInterval(this.#_patchInterval);\n this.#_patchInterval = undefined;\n }\n\n if (this._simulationInterval) {\n clearInterval(this._simulationInterval);\n this._simulationInterval = undefined;\n }\n\n if (this._autoDisposeTimeout) {\n clearInterval(this._autoDisposeTimeout);\n this._autoDisposeTimeout = undefined;\n }\n\n // clear all timeouts/intervals + force to stop ticking\n this.clock.clear();\n this.clock.stop();\n\n return await (userReturnData || Promise.resolve());\n }\n\n protected _onMessage(client: Client & ClientPrivate, buffer: Buffer) {\n // skip if client is on LEAVING state.\n if (client.state === ClientState.LEAVING) { return; }\n\n const it: Iterator = { offset: 1 };\n const code = buffer[0];\n\n if (!buffer) {\n debugAndPrintError(`${this.roomName} (roomId: ${this.roomId}), couldn't decode message: ${buffer}`);\n return;\n }\n\n if (code === Protocol.ROOM_DATA) {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n const messageTypeHandler = this.onMessageHandlers[messageType];\n\n let message;\n try {\n message = (buffer.byteLength > it.offset)\n ? unpack(buffer.subarray(it.offset, buffer.byteLength))\n : undefined;\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.roomId);\n\n // custom message validation\n if (messageTypeHandler?.validate !== undefined) {\n message = messageTypeHandler.validate(message);\n }\n\n } catch (e) {\n debugAndPrintError(e);\n client.leave(Protocol.WS_CLOSE_WITH_ERROR);\n return;\n }\n\n if (messageTypeHandler) {\n messageTypeHandler.callback(client, message);\n\n } else {\n (this.onMessageHandlers['*'] || this.onMessageHandlers['__no_message_handler']).callback(client, messageType, message);\n }\n\n } else if (code === Protocol.ROOM_DATA_BYTES) {\n const messageType = (decode.stringCheck(buffer, it))\n ? decode.string(buffer, it)\n : decode.number(buffer, it);\n const messageTypeHandler = this.onMessageHandlers[messageType];\n\n let message = buffer.subarray(it.offset, buffer.byteLength);\n debugMessage(\"received: '%s' -> %j (roomId: %s)\", messageType, message, this.roomId);\n\n // custom message validation\n if (messageTypeHandler?.validate !== undefined) {\n message = messageTypeHandler.validate(message);\n }\n\n if (messageTypeHandler) {\n messageTypeHandler.callback(client, message);\n\n } else {\n (this.onMessageHandlers['*'] || this.onMessageHandlers['__no_message_handler']).callback(client, messageType, message);\n }\n\n } else if (code === Protocol.JOIN_ROOM && client.state === ClientState.JOINING) {\n // join room has been acknowledged by the client\n client.state = ClientState.JOINED;\n client._joinedAt = this.clock.elapsedTime;\n\n // send current state when new client joins the room\n if (this.state) {\n this.sendFullState(client);\n }\n\n // dequeue messages sent before client has joined effectively (on user-defined `onJoin`)\n if (client._enqueuedMessages.length > 0) {\n client._enqueuedMessages.forEach((enqueued) => client.raw(enqueued));\n }\n delete client._enqueuedMessages;\n\n } else if (code === Protocol.LEAVE_ROOM) {\n this._forciblyCloseClient(client, Protocol.WS_CLOSE_CONSENTED);\n }\n\n }\n\n protected _forciblyCloseClient(client: Client & ClientPrivate, closeCode: number) {\n // stop receiving messages from this client\n client.ref.removeAllListeners('message');\n\n // prevent \"onLeave\" from being called twice if player asks to leave\n client.ref.removeListener('close', client.ref['onleave']);\n\n // only effectively close connection when \"onLeave\" is fulfilled\n this._onLeave(client, closeCode).then(() => client.leave(closeCode));\n }\n\n protected async _onLeave(client: Client, code?: number): Promise<any> {\n debugMatchMaking('onLeave, sessionId: \\'%s\\' (close code: %d, roomId: %s)', client.sessionId, code, this.roomId);\n\n // call 'onLeave' method only if the client has been successfully accepted.\n client.state = ClientState.LEAVING;\n\n if (!this.clients.delete(client)) {\n // skip if client already left the room\n return;\n }\n\n if (this.onLeave) {\n try {\n this.#_onLeaveConcurrent++;\n await this.onLeave(client, (code === Protocol.WS_CLOSE_CONSENTED));\n\n } catch (e) {\n debugAndPrintError(`onLeave error: ${(e && e.message || e || 'promise rejected')} (roomId: ${this.roomId})`);\n\n } finally {\n this.#_onLeaveConcurrent--;\n }\n }\n\n // check for manual \"reconnection\" flow\n if (this._reconnections[client.reconnectionToken]) {\n this._reconnections[client.reconnectionToken][1].catch(async () => {\n await this._onAfterLeave(client);\n });\n\n // @ts-ignore (client.state may be modified at onLeave())\n } else if (client.state !== ClientState.RECONNECTED) {\n await this._onAfterLeave(client);\n }\n }\n\n protected async _onAfterLeave(client: Client) {\n // try to dispose immediately if client reconnection isn't set up.\n const willDispose = await this._decrementClientCount();\n\n // trigger 'leave' only if seat reservation has been fully consumed\n if (this.reservedSeats[client.sessionId] === undefined) {\n this._events.emit('leave', client, willDispose);\n }\n\n }\n\n protected async _incrementClientCount() {\n // lock automatically when maxClients is reached\n if (!this.#_locked && this.hasReachedMaxClients()) {\n this.#_maxClientsReached = true;\n this.lock.call(this, true);\n }\n\n await this.listing.updateOne({\n $inc: { clients: 1 },\n $set: { locked: this.#_locked },\n });\n }\n\n protected async _decrementClientCount() {\n const willDispose = this._disposeIfEmpty();\n\n if (this._internalState === RoomInternalState.DISPOSING) {\n return true;\n }\n\n // unlock if room is available for new connections\n if (!willDispose) {\n if (this.#_maxClientsReached && !this._lockedExplicitly) {\n this.#_maxClientsReached = false;\n this.unlock.call(this, true);\n }\n\n // update room listing cache\n await this.listing.updateOne({\n $inc: { clients: -1 },\n $set: { locked: this.#_locked },\n });\n }\n\n return willDispose;\n }\n\n #registerUncaughtExceptionHandlers() {\n const onUncaughtException = this.onUncaughtException.bind(this);\n const originalSetTimeout = this.clock.setTimeout;\n this.clock.setTimeout = (cb, timeout, ...args) => {\n return originalSetTimeout.call(this.clock, wrapTryCatch(cb, onUncaughtException, TimedEventException, 'setTimeout'), timeout, ...args);\n };\n\n const originalSetInterval = this.clock.setInterval;\n this.clock.setInterval = (cb, timeout, ...args) => {\n return originalSetInterval.call(this.clock, wrapTryCatch(cb, onUncaughtException, TimedEventException, 'setInterval'), timeout, ...args);\n };\n\n if (this.onCreate !== undefined) {\n this.onCreate = wrapTryCatch(this.onCreate.bind(this), onUncaughtException, OnCreateException, 'onCreate', true);\n }\n\n if (this.onAuth !== undefined) {\n this.onAuth = wrapTryCatch(this.onAuth.bind(this), onUncaughtException, OnAuthException, 'onAuth', true);\n }\n\n if (this.onJoin !== undefined) {\n this.onJoin = wrapTryCatch(this.onJoin.bind(this), onUncaughtException, OnJoinException, 'onJoin', true);\n }\n\n if (this.onLeave !== undefined) {\n this.onLeave = wrapTryCatch(this.onLeave.bind(this), onUncaughtException, OnLeaveException, 'onLeave', true);\n }\n\n if (this.onDispose !== undefined) {\n this.onDispose = wrapTryCatch(this.onDispose.bind(this), onUncaughtException, OnDisposeException, 'onDispose');\n }\n }\n\n}\n"], "mappings": ";AAAA,SAAS,cAAc;AACvB,SAAS,QAAuB,gBAAgB;AAEhD,OAAO,WAAW;AAClB,SAAS,oBAAoB;AAC7B,SAAS,cAAc;AAIvB,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AAGjC,SAAS,WAAW,iBAAiB,gBAAgB;AACrD,SAAS,UAAU,YAAY,oBAAoB;AACnD,SAAS,iBAAiB;AAE1B,SAAS,oBAAoB,kBAAkB,oBAAoB;AAEnE,SAAS,mBAAmB;AAC5B,SAA6C,aAAa,mBAAiC;AAC3F,SAAS,iBAAiB,mBAAmB,oBAAoB,iBAAiB,kBAAkB,oBAAmC,6BAA6B,2BAA2B;AAE/L,IAAM,qBAAqB,MAAO;AAClC,IAAM,8BAA8B,MAAO;AAC3C,IAAM,iBAAiB,IAAI,eAAe;AAEnC,IAAM,gCAAgC,OAAO,QAAQ,IAAI,kCAAkC,EAAE;AAQ7F,IAAK,oBAAL,kBAAKA,uBAAL;AACL,EAAAA,sCAAA,cAAW,KAAX;AACA,EAAAA,sCAAA,aAAU,KAAV;AACA,EAAAA,sCAAA,eAAY,KAAZ;AAHU,SAAAA;AAAA,GAAA;AAgBL,IAAe,OAAf,MAAe,MAA+E;AAAA,EA2HnG,cAAc;AApGd;AAAA;AAAA;AAAA;AAAA,SAAO,QAAe,IAAI,MAAM;AAIhC,+BAA8B;AAO9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAO,aAAqB;AAC5B,+BAA+B;AAQ/B;AAAA;AAAA;AAAA;AAAA;AAAA,SAAO,cAAuB;AAQ9B;AAAA;AAAA;AAAA;AAAA;AAAA,SAAO,YAAoB;AAsB3B;AAAA;AAAA;AAAA;AAAA;AAAA,SAAO,UAA2C,IAAI,YAAY;AAGlE;AAAA,SAAO,UAAU,IAAI,aAAa;AAGlC;AAAA,SAAU,sBAA8B;AACxC,SAAU,gBAAyE,CAAC;AACpF,SAAU,uBAAgE,CAAC;AAE3E,SAAU,iBAAsE,CAAC;AACjF,SAAQ,yBAAyB,oBAAI,IAAoB;AAEzD,SAAQ,oBAKJ;AAAA,MACA,wBAAwB;AAAA,QACtB,UAAU,CAAC,QAAgB,aAAqB,MAAe;AAC7D,gBAAM,eAAe,uBAAuB,WAAW;AACvD,6BAAmB,GAAG,YAAY,aAAa,KAAK,MAAM,GAAG;AAE7D,cAAI,WAAW;AAEb,mBAAO,MAAM,UAAU,iBAAiB,YAAY;AAAA,UAEtD,OAAO;AAEL,mBAAO,MAAM,SAAS,qBAAqB,YAAY;AAAA,UACzD;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEF,SAAQ,cAAiC;AACzC,SAAQ,uBAA6D,CAAC;AAItE,SAAQ,iBAAoC;AAE5C,SAAQ,oBAA6B;AACrC,oBAAoB;AAOlB,SAAK,QAAQ,KAAK,WAAW,MAAM;AACjC,WAAK,SAAS,EACX,MAAM,CAAC,MAAM,mBAAmB,oBAAqB,KAAK,EAAE,SAAS,EAAE,WAAW,KAAK,kBAAmB,aAAa,KAAK,MAAM,GAAG,CAAC,EACtI,QAAQ,MAAM,KAAK,QAAQ,KAAK,YAAY,CAAC;AAAA,IAClD,CAAC;AAKD,QAAI,KAAK,wBAAwB,QAAW;AAC1C,WAAK,mCAAmC;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA/HA,IAAW,SAAS;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAW,WAAW;AACpB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAUA;AAAA,EACA;AAAA,EACA;AAAA,EAQA;AAAA,EACA;AAAA,EAQA;AAAA,EAQA;AAAA,EACA;AAAA,EAMA;AAAA,EA0DA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBU,SAAS;AACjB,SAAK,UAAU,KAAK;AACpB,SAAK,gBAAgB,KAAK;AAC1B,SAAK,cAAc,KAAK;AACxB,SAAK,eAAe,KAAK;AAEzB,WAAO,iBAAiB,MAAM;AAAA,MAC5B,OAAO;AAAA,QACL,YAAY;AAAA,QACZ,KAAK,MAAM,KAAK;AAAA,QAChB,KAAK,CAAC,aAAoB;AACxB,cAAI,UAAU,YAAY,OAAO,QAAQ,MAAM,UAAa,SAAS,QAAQ,MAAM,QAAW;AAC5F,iBAAK,cAAc,IAAI,iBAAiB,CAAC;AAAA,UAC3C,WAAW,iBAAiB,UAAU;AACpC,kBAAM,IAAI,MAAM,gFAAgF;AAAA,UAClG,WAAW,aAAa,QAAW;AACjC,kBAAM,IAAI,MAAM,+HAA+H;AAAA,UACjJ;AACA,eAAK,YAAY,MAAM,QAAQ;AAC/B,eAAK,UAAU;AAAA,QACjB;AAAA,MACF;AAAA,MAEA,YAAY;AAAA,QACV,YAAY;AAAA,QACZ,KAAK,MAAM,KAAK;AAAA,QAChB,KAAK,CAAC,UAAkB;AACtB,eAAK,eAAe;AAEpB,cAAI,KAAK,mBAAmB,iBAA2B;AACrD,kBAAM,uBAAuB,KAAK,qBAAqB;AAGvD,gBAAI,CAAC,KAAK,qBAAqB,KAAK,uBAAuB,CAAC,sBAAsB;AAChF,mBAAK,sBAAsB;AAC3B,mBAAK,WAAW;AAChB,mBAAK,QAAQ,SAAS;AAAA,YACxB;AAGA,gBAAI,sBAAsB;AACxB,mBAAK,sBAAsB;AAC3B,mBAAK,WAAW;AAChB,mBAAK,QAAQ,SAAS;AAAA,YACxB;AAEA,iBAAK,QAAQ,aAAa;AAC1B,iBAAK,QAAQ,KAAK;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAAA,MAEA,aAAa;AAAA,QACX,YAAY;AAAA,QACZ,KAAK,MAAM,KAAK;AAAA,QAChB,KAAK,CAAC,UAAmB;AACvB,cACE,UAAU,KAAK,iBACf,KAAK,mBAAmB,mBACxB;AACA,iBAAK,gBAAgB;AACrB,iBAAK,wBAAwB;AAAA,UAC/B;AAAA,QACF;AAAA,MACF;AAAA,MAEA,WAAW;AAAA,QACT,YAAY;AAAA,QACZ,KAAK,MAAM,KAAK;AAAA,QAChB,KAAK,CAAC,iBAAyB;AAC7B,eAAK,cAAc;AAEnB,cAAI,KAAK,iBAAiB;AACxB,0BAAc,KAAK,eAAe;AAClC,iBAAK,kBAAkB;AAAA,UACzB;AACA,cAAI,iBAAiB,QAAQ,iBAAiB,GAAG;AAC/C,iBAAK,kBAAkB,YAAY,MAAM,KAAK,eAAe,GAAG,YAAY;AAAA,UAC9E;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAGD,SAAK,YAAY,KAAK;AAGtB,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,KAAK;AAAA,IACpB;AAGA,SAAK,wBAAwB,KAAK,mBAAmB;AAErD,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAW,WAAW;AAAE,WAAO,KAAK;AAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMhD,IAAW,SAAS,UAAkB;AACpC,QAAI,KAAK,YAAY;AAEnB,YAAM,IAAI,YAAY,UAAU,mBAAmB,mCAAmC;AAAA,IACxF;AACA,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,IAAW,SAAS;AAAE,WAAO,KAAK;AAAA,EAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ5C,IAAW,OAAO,QAAgB;AAChC,QAAI,KAAK,mBAAmB,oBAA8B,CAAC,WAAW;AAEpE,YAAM,IAAI,YAAY,UAAU,mBAAmB,qDAAqD;AAAA,IAC1G;AACA,SAAK,WAAW;AAAA,EAClB;AAAA,EAuBO,OACL,QACA,SACA,SACoB;AACpB,WAAO;AAAA,EACT;AAAA,EAEA,aAAa,OACX,OACA,SACA,SACkB;AAClB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQO,mBAAmB;AACxB,SAAK;AAAA,MACF,YACG,SAAS,2BACT,SAAS;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBO,uBAAgC;AACrC,WACG,KAAK,QAAQ,SAAS,OAAO,KAAK,KAAK,aAAa,EAAE,UAAW,KAAK,cACvE,KAAK,mBAAmB;AAAA,EAE5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaO,uBAAuB,SAAiB;AAC7C,SAAK,sBAAsB;AAC3B,WAAO;AAAA,EACT;AAAA,EAEO,gBAAgB,WAAmB,mBAAqC;AAC7E,UAAM,eAAe,KAAK,cAAc,SAAS;AAGjD,QAAI,iBAAiB,QAAW;AAC9B,aAAO;AAAA,IACT;AAEA,QAAI,aAAa,CAAC,GAAG;AAEnB,aACE,qBACA,KAAK,eAAe,iBAAiB,IAAI,CAAC,MAAM,aAChD,KAAK,uBAAuB,IAAI,SAAS;AAAA,IAG7C,OAAO;AAEL,aAAO,aAAa,CAAC,MAAM;AAAA,IAC7B;AAAA,EACF;AAAA,EAEO,uBAAuB,mBAA2B;AACvD,UAAM,YAAY,KAAK,eAAe,iBAAiB,IAAI,CAAC;AAC5D,UAAM,eAAe,KAAK,cAAc,SAAS;AAEjD,QAAI,gBAAgB,aAAa,CAAC,GAAG;AACnC,WAAK,uBAAuB,IAAI,WAAW,iBAAiB;AAC5D,aAAO;AAAA,IAET,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYO,sBAAsB,gBAAqC,QAAgB,6BAAmC;AAEnH,QAAI,KAAK,qBAAqB;AAAE,oBAAc,KAAK,mBAAmB;AAAA,IAAG;AAEzE,QAAI,gBAAgB;AAClB,UAAI,KAAK,wBAAwB,QAAW;AAC1C,yBAAiB,aAAa,gBAAgB,KAAK,oBAAoB,KAAK,IAAI,GAAG,6BAA6B,uBAAuB;AAAA,MACzI;AAEA,WAAK,sBAAsB,YAAY,MAAM;AAC3C,aAAK,MAAM,KAAK;AAChB,uBAAe,KAAK,MAAM,SAAS;AAAA,MACrC,GAAG,KAAK;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,aAAa,cAAmC;AACrD,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKO,SAAS,UAAiB;AAC/B,SAAK,QAAQ;AAAA,EACf;AAAA,EAEO,cAAc,YAA+B;AAClD,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,MAAa,YAAY,MAAyB;AAChD,QAAI,CAAC,KAAK,QAAQ,UAAU;AAC1B,WAAK,QAAQ,WAAW;AAAA,IAE1B,OAAO;AACL,iBAAW,SAAS,MAAM;AACxB,YAAI,CAAC,KAAK,eAAe,KAAK,GAAG;AAAE;AAAA,QAAU;AAC7C,aAAK,QAAQ,SAAS,KAAK,IAAI,KAAK,KAAK;AAAA,MAC3C;AAGA,UAAI,kBAAkB,KAAK,SAAS;AAClC,QAAC,KAAK,QAAgB,aAAa,UAAU;AAAA,MAC/C;AAAA,IACF;AAEA,QAAI,KAAK,mBAAmB,iBAA2B;AACrD,YAAM,KAAK,QAAQ,KAAK;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,MAAa,WAAW,OAAgB,MAAM;AAC5C,QAAI,KAAK,QAAQ,YAAY,KAAM;AAEnC,SAAK,QAAQ,UAAU;AAEvB,QAAI,KAAK,mBAAmB,iBAA2B;AACrD,YAAM,KAAK,QAAQ,KAAK;AAAA,IAC1B;AAEA,SAAK,QAAQ,KAAK,qBAAqB,IAAI;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,OAAO;AAElB,SAAK,oBAAqB,UAAU,CAAC,MAAM;AAG3C,QAAI,KAAK,UAAU;AAAE;AAAA,IAAQ;AAE7B,SAAK,WAAW;AAEhB,UAAM,KAAK,QAAQ,UAAU;AAAA,MAC3B,MAAM,EAAE,QAAQ,KAAK,SAAS;AAAA,IAChC,CAAC;AAED,SAAK,QAAQ,KAAK,MAAM;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,SAAS;AAEpB,QAAI,UAAU,CAAC,MAAM,QAAW;AAC9B,WAAK,oBAAoB;AAAA,IAC3B;AAGA,QAAI,CAAC,KAAK,UAAU;AAAE;AAAA,IAAQ;AAE9B,SAAK,WAAW;AAEhB,UAAM,KAAK,QAAQ,UAAU;AAAA,MAC3B,MAAM,EAAE,QAAQ,KAAK,SAAS;AAAA,IAChC,CAAC;AAED,SAAK,QAAQ,KAAK,QAAQ;AAAA,EAC5B;AAAA,EAGO,KAAK,QAAgB,eAAoB,