@colyseus/core 0.17.44 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/build/MatchMaker.cjs +22 -13
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.d.ts +10 -0
  4. package/build/MatchMaker.mjs +22 -14
  5. package/build/MatchMaker.mjs.map +2 -2
  6. package/build/Protocol.cjs +102 -37
  7. package/build/Protocol.cjs.map +2 -2
  8. package/build/Protocol.d.ts +33 -2
  9. package/build/Protocol.mjs +102 -37
  10. package/build/Protocol.mjs.map +2 -2
  11. package/build/Rewind.cjs +385 -0
  12. package/build/Rewind.cjs.map +7 -0
  13. package/build/Rewind.d.ts +291 -0
  14. package/build/Rewind.mjs +359 -0
  15. package/build/Rewind.mjs.map +7 -0
  16. package/build/Room.cjs +425 -149
  17. package/build/Room.cjs.map +3 -3
  18. package/build/Room.d.ts +314 -27
  19. package/build/Room.mjs +434 -152
  20. package/build/Room.mjs.map +3 -3
  21. package/build/RoomMessages.cjs +265 -0
  22. package/build/RoomMessages.cjs.map +7 -0
  23. package/build/RoomMessages.d.ts +49 -0
  24. package/build/RoomMessages.mjs +240 -0
  25. package/build/RoomMessages.mjs.map +7 -0
  26. package/build/RoomPlugin.cjs +252 -0
  27. package/build/RoomPlugin.cjs.map +7 -0
  28. package/build/RoomPlugin.d.ts +271 -0
  29. package/build/RoomPlugin.mjs +220 -0
  30. package/build/RoomPlugin.mjs.map +7 -0
  31. package/build/Server.cjs +49 -11
  32. package/build/Server.cjs.map +2 -2
  33. package/build/Server.d.ts +26 -0
  34. package/build/Server.mjs +50 -12
  35. package/build/Server.mjs.map +2 -2
  36. package/build/Transport.cjs +64 -2
  37. package/build/Transport.cjs.map +2 -2
  38. package/build/Transport.d.ts +108 -5
  39. package/build/Transport.mjs +63 -2
  40. package/build/Transport.mjs.map +2 -2
  41. package/build/errors/RoomExceptions.cjs +7 -4
  42. package/build/errors/RoomExceptions.cjs.map +2 -2
  43. package/build/errors/RoomExceptions.d.ts +15 -3
  44. package/build/errors/RoomExceptions.mjs +5 -3
  45. package/build/errors/RoomExceptions.mjs.map +2 -2
  46. package/build/index.cjs +30 -4
  47. package/build/index.cjs.map +2 -2
  48. package/build/index.d.ts +9 -5
  49. package/build/index.mjs +29 -6
  50. package/build/index.mjs.map +2 -2
  51. package/build/input/InputBuffer.cjs +536 -0
  52. package/build/input/InputBuffer.cjs.map +7 -0
  53. package/build/input/InputBuffer.d.ts +226 -0
  54. package/build/input/InputBuffer.mjs +507 -0
  55. package/build/input/InputBuffer.mjs.map +7 -0
  56. package/build/input/RoomInput.cjs +306 -0
  57. package/build/input/RoomInput.cjs.map +7 -0
  58. package/build/input/RoomInput.d.ts +91 -0
  59. package/build/input/RoomInput.mjs +287 -0
  60. package/build/input/RoomInput.mjs.map +7 -0
  61. package/build/input/types.cjs +18 -0
  62. package/build/input/types.cjs.map +7 -0
  63. package/build/input/types.d.ts +476 -0
  64. package/build/input/types.mjs +0 -0
  65. package/build/input/types.mjs.map +7 -0
  66. package/build/internal.cjs +61 -0
  67. package/build/internal.cjs.map +7 -0
  68. package/build/internal.d.ts +9 -0
  69. package/build/internal.mjs +29 -0
  70. package/build/internal.mjs.map +7 -0
  71. package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
  72. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
  73. package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
  74. package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
  75. package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
  76. package/build/matchmaker/driver.cjs.map +1 -1
  77. package/build/matchmaker/driver.d.ts +12 -0
  78. package/build/matchmaker/driver.mjs.map +1 -1
  79. package/build/presence/LocalPresence.d.ts +1 -1
  80. package/build/rooms/LobbyRoom.cjs +8 -10
  81. package/build/rooms/LobbyRoom.cjs.map +2 -2
  82. package/build/rooms/LobbyRoom.d.ts +4 -3
  83. package/build/rooms/LobbyRoom.mjs +8 -10
  84. package/build/rooms/LobbyRoom.mjs.map +2 -2
  85. package/build/rooms/QueueRoom.cjs +1 -1
  86. package/build/rooms/QueueRoom.cjs.map +2 -2
  87. package/build/rooms/QueueRoom.mjs +1 -1
  88. package/build/rooms/QueueRoom.mjs.map +2 -2
  89. package/build/rooms/RelayRoom.cjs +12 -16
  90. package/build/rooms/RelayRoom.cjs.map +2 -2
  91. package/build/rooms/RelayRoom.d.ts +32 -11
  92. package/build/rooms/RelayRoom.mjs +10 -16
  93. package/build/rooms/RelayRoom.mjs.map +2 -2
  94. package/build/router/default_routes.cjs +4 -1
  95. package/build/router/default_routes.cjs.map +2 -2
  96. package/build/router/default_routes.mjs +4 -1
  97. package/build/router/default_routes.mjs.map +2 -2
  98. package/build/router/index.cjs +71 -5
  99. package/build/router/index.cjs.map +2 -2
  100. package/build/router/index.d.ts +30 -6
  101. package/build/router/index.mjs +72 -7
  102. package/build/router/index.mjs.map +3 -3
  103. package/build/serializer/NoneSerializer.cjs +2 -2
  104. package/build/serializer/NoneSerializer.cjs.map +2 -2
  105. package/build/serializer/NoneSerializer.d.ts +3 -3
  106. package/build/serializer/NoneSerializer.mjs +2 -2
  107. package/build/serializer/NoneSerializer.mjs.map +2 -2
  108. package/build/serializer/SchemaSerializer.cjs +44 -14
  109. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  110. package/build/serializer/SchemaSerializer.d.ts +3 -3
  111. package/build/serializer/SchemaSerializer.mjs +46 -16
  112. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  113. package/build/serializer/Serializer.cjs.map +1 -1
  114. package/build/serializer/Serializer.d.ts +12 -2
  115. package/build/utils/DevMode.cjs +14 -14
  116. package/build/utils/DevMode.cjs.map +2 -2
  117. package/build/utils/DevMode.d.ts +0 -2
  118. package/build/utils/DevMode.mjs +14 -12
  119. package/build/utils/DevMode.mjs.map +2 -2
  120. package/build/utils/UserSessionIndex.cjs +162 -0
  121. package/build/utils/UserSessionIndex.cjs.map +7 -0
  122. package/build/utils/UserSessionIndex.d.ts +166 -0
  123. package/build/utils/UserSessionIndex.mjs +130 -0
  124. package/build/utils/UserSessionIndex.mjs.map +7 -0
  125. package/build/utils/Utils.cjs +3 -0
  126. package/build/utils/Utils.cjs.map +2 -2
  127. package/build/utils/Utils.d.ts +8 -0
  128. package/build/utils/Utils.mjs +2 -0
  129. package/build/utils/Utils.mjs.map +2 -2
  130. package/package.json +19 -14
  131. package/src/MatchMaker.ts +47 -15
  132. package/src/Protocol.ts +130 -59
  133. package/src/Rewind.ts +572 -0
  134. package/src/Room.ts +649 -235
  135. package/src/RoomMessages.ts +342 -0
  136. package/src/RoomPlugin.ts +563 -0
  137. package/src/Server.ts +82 -15
  138. package/src/Transport.ts +172 -9
  139. package/src/errors/RoomExceptions.ts +18 -4
  140. package/src/index.ts +23 -3
  141. package/src/input/InputBuffer.ts +549 -0
  142. package/src/input/RoomInput.ts +337 -0
  143. package/src/input/types.ts +503 -0
  144. package/src/internal.ts +46 -0
  145. package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
  146. package/src/matchmaker/driver.ts +13 -0
  147. package/src/rooms/LobbyRoom.ts +12 -8
  148. package/src/rooms/QueueRoom.ts +1 -1
  149. package/src/rooms/RelayRoom.ts +9 -15
  150. package/src/router/default_routes.ts +6 -1
  151. package/src/router/index.ts +117 -11
  152. package/src/serializer/NoneSerializer.ts +3 -3
  153. package/src/serializer/SchemaSerializer.ts +110 -19
  154. package/src/serializer/Serializer.ts +13 -2
  155. package/src/utils/DevMode.ts +18 -13
  156. package/src/utils/UserSessionIndex.ts +311 -0
  157. package/src/utils/Utils.ts +9 -0
@@ -6,6 +6,7 @@ export declare class LocalDriver implements MatchMakerDriver {
6
6
  query(conditions: Partial<IRoomCache>, sortOptions?: SortOptions): IRoomCache<any>[];
7
7
  cleanup(processId: string): Promise<void>;
8
8
  findOne(conditions: Partial<IRoomCache>, sortOptions?: SortOptions): Promise<IRoomCache>;
9
+ findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;
9
10
  update(room: IRoomCache, operations: Partial<{
10
11
  $set: Partial<IRoomCache>;
11
12
  $inc: Partial<IRoomCache>;
@@ -28,6 +28,19 @@ var LocalDriver = class {
28
28
  }
29
29
  return query;
30
30
  }
31
+ async findByIds(roomIds) {
32
+ const result = /* @__PURE__ */ new Map();
33
+ if (roomIds.length === 0) {
34
+ return result;
35
+ }
36
+ const wanted = new Set(roomIds);
37
+ for (const room of this.rooms) {
38
+ if (wanted.has(room.roomId)) {
39
+ result.set(room.roomId, room);
40
+ }
41
+ }
42
+ return result;
43
+ }
31
44
  update(room, operations) {
32
45
  if (operations.$set) {
33
46
  for (const field in operations.$set) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/matchmaker/LocalDriver/LocalDriver.ts"],
4
- "sourcesContent": ["import { debugMatchMaking } from '../../Debug.ts';\nimport type { IRoomCache, SortOptions, MatchMakerDriver } from '../driver.ts';\nimport { Query } from './Query.ts';\n\n// re-export\nexport type { IRoomCache, SortOptions, MatchMakerDriver };\n\nexport class LocalDriver implements MatchMakerDriver {\n public rooms: IRoomCache[] = [];\n\n public has(roomId: string) {\n return this.rooms.some((room) => room.roomId === roomId);\n }\n\n public query(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {\n const query = new Query<IRoomCache>(this.rooms, conditions);\n\n if (sortOptions) {\n query.sort(sortOptions);\n }\n\n return query.filter(conditions);\n }\n\n public cleanup(processId: string) {\n const cachedRooms = this.query({ processId });\n debugMatchMaking(\"removing stale rooms by processId %s (%s rooms found)\", processId, cachedRooms.length);\n\n cachedRooms.forEach((room) => this.remove(room.roomId));\n return Promise.resolve();\n }\n\n public findOne(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {\n const query = new Query<IRoomCache>(this.rooms, conditions);\n\n if (sortOptions) {\n query.sort(sortOptions);\n }\n\n return query as unknown as Promise<IRoomCache>;\n }\n\n public update(room: IRoomCache, operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>) {\n if (operations.$set) {\n for (const field in operations.$set) {\n if (operations.$set.hasOwnProperty(field)) {\n room[field] = operations.$set[field];\n }\n }\n }\n\n if (operations.$inc) {\n for (const field in operations.$inc) {\n if (operations.$inc.hasOwnProperty(field)) {\n room[field] += operations.$inc[field];\n }\n }\n }\n\n return true;\n }\n\n public persist(room: IRoomCache, create: boolean = false) {\n // if (this.rooms.indexOf(room) !== -1) {\n // // already in the list\n // return true;\n // }\n\n if (!create) { return false; }\n\n // add to the list\n this.rooms.push(room);\n\n return true;\n }\n\n public remove(roomId: string) {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n if (roomIndex !== -1) {\n this.rooms.splice(roomIndex, 1);\n return true;\n }\n return false;\n }\n\n public clear() {\n this.rooms = [];\n }\n\n public shutdown() {\n }\n}\n"],
5
- "mappings": ";AAAA,SAAS,wBAAwB;AAEjC,SAAS,aAAa;AAKf,IAAM,cAAN,MAA8C;AAAA,EAA9C;AACL,SAAO,QAAsB,CAAC;AAAA;AAAA,EAEvB,IAAI,QAAgB;AACzB,WAAO,KAAK,MAAM,KAAK,CAAC,SAAS,KAAK,WAAW,MAAM;AAAA,EACzD;AAAA,EAEO,MAAM,YAAiC,aAA2B;AACvE,UAAM,QAAQ,IAAI,MAAkB,KAAK,OAAO,UAAU;AAE1D,QAAI,aAAa;AACf,YAAM,KAAK,WAAW;AAAA,IACxB;AAEA,WAAO,MAAM,OAAO,UAAU;AAAA,EAChC;AAAA,EAEO,QAAQ,WAAmB;AAChC,UAAM,cAAc,KAAK,MAAM,EAAE,UAAU,CAAC;AAC5C,qBAAiB,yDAAyD,WAAW,YAAY,MAAM;AAEvG,gBAAY,QAAQ,CAAC,SAAS,KAAK,OAAO,KAAK,MAAM,CAAC;AACtD,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEO,QAAQ,YAAiC,aAA2B;AACzE,UAAM,QAAQ,IAAI,MAAkB,KAAK,OAAO,UAAU;AAE1D,QAAI,aAAa;AACf,YAAM,KAAK,WAAW;AAAA,IACxB;AAEA,WAAO;AAAA,EACT;AAAA,EAEO,OAAO,MAAkB,YAA+E;AAC7G,QAAI,WAAW,MAAM;AACnB,iBAAW,SAAS,WAAW,MAAM;AACnC,YAAI,WAAW,KAAK,eAAe,KAAK,GAAG;AACzC,eAAK,KAAK,IAAI,WAAW,KAAK,KAAK;AAAA,QACrC;AAAA,MACF;AAAA,IACF;AAEA,QAAI,WAAW,MAAM;AACnB,iBAAW,SAAS,WAAW,MAAM;AACnC,YAAI,WAAW,KAAK,eAAe,KAAK,GAAG;AACzC,eAAK,KAAK,KAAK,WAAW,KAAK,KAAK;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEO,QAAQ,MAAkB,SAAkB,OAAO;AAMxD,QAAI,CAAC,QAAQ;AAAE,aAAO;AAAA,IAAO;AAG7B,SAAK,MAAM,KAAK,IAAI;AAEpB,WAAO;AAAA,EACT;AAAA,EAEO,OAAO,QAAgB;AAC5B,UAAM,YAAY,KAAK,MAAM,UAAU,CAAC,SAAS,KAAK,WAAW,MAAM;AACvE,QAAI,cAAc,IAAI;AACpB,WAAK,MAAM,OAAO,WAAW,CAAC;AAC9B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEO,QAAQ;AACb,SAAK,QAAQ,CAAC;AAAA,EAChB;AAAA,EAEO,WAAW;AAAA,EAClB;AACF;",
4
+ "sourcesContent": ["import { debugMatchMaking } from '../../Debug.ts';\nimport type { IRoomCache, SortOptions, MatchMakerDriver } from '../driver.ts';\nimport { Query } from './Query.ts';\n\n// re-export\nexport type { IRoomCache, SortOptions, MatchMakerDriver };\n\nexport class LocalDriver implements MatchMakerDriver {\n public rooms: IRoomCache[] = [];\n\n public has(roomId: string) {\n return this.rooms.some((room) => room.roomId === roomId);\n }\n\n public query(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {\n const query = new Query<IRoomCache>(this.rooms, conditions);\n\n if (sortOptions) {\n query.sort(sortOptions);\n }\n\n return query.filter(conditions);\n }\n\n public cleanup(processId: string) {\n const cachedRooms = this.query({ processId });\n debugMatchMaking(\"removing stale rooms by processId %s (%s rooms found)\", processId, cachedRooms.length);\n\n cachedRooms.forEach((room) => this.remove(room.roomId));\n return Promise.resolve();\n }\n\n public findOne(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {\n const query = new Query<IRoomCache>(this.rooms, conditions);\n\n if (sortOptions) {\n query.sort(sortOptions);\n }\n\n return query as unknown as Promise<IRoomCache>;\n }\n\n public async findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>> {\n const result = new Map<string, IRoomCache>();\n if (roomIds.length === 0) { return result; }\n const wanted = new Set(roomIds);\n for (const room of this.rooms) {\n if (wanted.has(room.roomId)) { result.set(room.roomId, room); }\n }\n return result;\n }\n\n public update(room: IRoomCache, operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>) {\n if (operations.$set) {\n for (const field in operations.$set) {\n if (operations.$set.hasOwnProperty(field)) {\n room[field] = operations.$set[field];\n }\n }\n }\n\n if (operations.$inc) {\n for (const field in operations.$inc) {\n if (operations.$inc.hasOwnProperty(field)) {\n room[field] += operations.$inc[field];\n }\n }\n }\n\n return true;\n }\n\n public persist(room: IRoomCache, create: boolean = false) {\n // if (this.rooms.indexOf(room) !== -1) {\n // // already in the list\n // return true;\n // }\n\n if (!create) { return false; }\n\n // add to the list\n this.rooms.push(room);\n\n return true;\n }\n\n public remove(roomId: string) {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n if (roomIndex !== -1) {\n this.rooms.splice(roomIndex, 1);\n return true;\n }\n return false;\n }\n\n public clear() {\n this.rooms = [];\n }\n\n public shutdown() {\n }\n}\n"],
5
+ "mappings": ";AAAA,SAAS,wBAAwB;AAEjC,SAAS,aAAa;AAKf,IAAM,cAAN,MAA8C;AAAA,EAA9C;AACL,SAAO,QAAsB,CAAC;AAAA;AAAA,EAEvB,IAAI,QAAgB;AACzB,WAAO,KAAK,MAAM,KAAK,CAAC,SAAS,KAAK,WAAW,MAAM;AAAA,EACzD;AAAA,EAEO,MAAM,YAAiC,aAA2B;AACvE,UAAM,QAAQ,IAAI,MAAkB,KAAK,OAAO,UAAU;AAE1D,QAAI,aAAa;AACf,YAAM,KAAK,WAAW;AAAA,IACxB;AAEA,WAAO,MAAM,OAAO,UAAU;AAAA,EAChC;AAAA,EAEO,QAAQ,WAAmB;AAChC,UAAM,cAAc,KAAK,MAAM,EAAE,UAAU,CAAC;AAC5C,qBAAiB,yDAAyD,WAAW,YAAY,MAAM;AAEvG,gBAAY,QAAQ,CAAC,SAAS,KAAK,OAAO,KAAK,MAAM,CAAC;AACtD,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA,EAEO,QAAQ,YAAiC,aAA2B;AACzE,UAAM,QAAQ,IAAI,MAAkB,KAAK,OAAO,UAAU;AAE1D,QAAI,aAAa;AACf,YAAM,KAAK,WAAW;AAAA,IACxB;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAa,UAAU,SAAqD;AAC1E,UAAM,SAAS,oBAAI,IAAwB;AAC3C,QAAI,QAAQ,WAAW,GAAG;AAAE,aAAO;AAAA,IAAQ;AAC3C,UAAM,SAAS,IAAI,IAAI,OAAO;AAC9B,eAAW,QAAQ,KAAK,OAAO;AAC7B,UAAI,OAAO,IAAI,KAAK,MAAM,GAAG;AAAE,eAAO,IAAI,KAAK,QAAQ,IAAI;AAAA,MAAG;AAAA,IAChE;AACA,WAAO;AAAA,EACT;AAAA,EAEO,OAAO,MAAkB,YAA+E;AAC7G,QAAI,WAAW,MAAM;AACnB,iBAAW,SAAS,WAAW,MAAM;AACnC,YAAI,WAAW,KAAK,eAAe,KAAK,GAAG;AACzC,eAAK,KAAK,IAAI,WAAW,KAAK,KAAK;AAAA,QACrC;AAAA,MACF;AAAA,IACF;AAEA,QAAI,WAAW,MAAM;AACnB,iBAAW,SAAS,WAAW,MAAM;AACnC,YAAI,WAAW,KAAK,eAAe,KAAK,GAAG;AACzC,eAAK,KAAK,KAAK,WAAW,KAAK,KAAK;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEO,QAAQ,MAAkB,SAAkB,OAAO;AAMxD,QAAI,CAAC,QAAQ;AAAE,aAAO;AAAA,IAAO;AAG7B,SAAK,MAAM,KAAK,IAAI;AAEpB,WAAO;AAAA,EACT;AAAA,EAEO,OAAO,QAAgB;AAC5B,UAAM,YAAY,KAAK,MAAM,UAAU,CAAC,SAAS,KAAK,WAAW,MAAM;AACvE,QAAI,cAAc,IAAI;AACpB,WAAK,MAAM,OAAO,WAAW,CAAC;AAC9B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEO,QAAQ;AACb,SAAK,QAAQ,CAAC;AAAA,EAChB;AAAA,EAEO,WAAW;AAAA,EAClB;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/matchmaker/driver.ts"],
4
- "sourcesContent": ["import type { Room } from \"@colyseus/core\";\n\n/**\n * Sort options for room queries.\n */\nexport interface SortOptions {\n [fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';\n}\n\n/**\n * Built-in room cache fields that can be used for sorting.\n */\nexport type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';\n\n/**\n * Built-in room cache fields that can be used for filtering.\n */\nexport type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';\n\n/**\n * Extract metadata type from Room type\n */\nexport type ExtractRoomCacheMetadata<RoomType extends Room> = RoomType['~metadata'];\n\n/**\n * Generates a unique lock ID based on filter options.\n */\nexport function getLockId(filterOptions: any) {\n return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join(\"-\");\n}\n\n/**\n * Initialize a room cache which contains CRUD operations for room listings.\n *\n * @internal\n * @param initialValues - Predefined room properties.\n * @returns RoomData - New room cache.\n */\nexport function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {\n return {\n clients: 0,\n maxClients: Infinity,\n locked: false,\n private: false,\n metadata: undefined,\n createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),\n unlisted: false,\n ...initialValues,\n } as IRoomCache;\n}\n\nexport interface IRoomCache<Metadata = any> {\n /**\n * Room name.\n */\n name: string;\n\n /**\n * Unique identifier for the room.\n */\n roomId: string;\n\n /**\n * Process id where the room is running.\n */\n processId: string;\n\n /**\n * Number of clients connected to this room.\n */\n clients: number;\n\n /**\n * Maximum number of clients allowed to join the room.\n */\n maxClients: number;\n\n /**\n * Indicates if the room is locked (i.e. join requests are rejected).\n */\n locked?: boolean;\n\n /**\n * Indicates if the room is private\n * Private rooms can't be joined via `join()` or `joinOrCreate()`.\n */\n private?: boolean;\n\n /**\n * Public address of the server.\n */\n publicAddress?: string;\n\n /**\n * Do not show this room in lobby listing.\n */\n unlisted?: boolean;\n\n /**\n * Metadata associated with the room.\n */\n metadata?: Metadata;\n\n /**\n * When the room was created.\n */\n createdAt?: Date;\n}\n\nexport interface MatchMakerDriver {\n /**\n * Check if a room exists in room cache.\n *\n * @param roomId - The room id.\n *\n * @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.\n */\n has(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Query rooms in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.\n */\n query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<Array<IRoomCache<ExtractRoomCacheMetadata<T>>>> | Array<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Clean up rooms in room cache by process id.\n * @param processId - The process id.\n */\n cleanup?(processId: string): Promise<void>;\n\n /**\n * Query for a room in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns `IRoomCache` - An object contaning filtered room metadata.\n */\n findOne<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Remove a room from room cache.\n *\n * @param roomId - The room id.\n */\n remove(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Update a room in room cache.\n *\n * @param IRoomCache - The room to update.\n * @param operations - The operations to update the room.\n */\n update(\n room: IRoomCache,\n operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>\n ): Promise<boolean> | boolean;\n\n /**\n * Persist a room in room cache.\n *\n * @param room - The room to persist.\n * @param create - If true, create a new record. If false (default), update existing record.\n */\n persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;\n\n /**\n * Empty the room cache. Used for testing purposes only.\n * @internal Do not call this method yourself.\n */\n clear(): void;\n\n /**\n * Boot the room cache medium (if available).\n */\n boot?(): Promise<void>;\n\n /**\n * Dispose the connection of the room cache medium.\n */\n shutdown(): void;\n}\n"],
4
+ "sourcesContent": ["import type { Room } from \"@colyseus/core\";\n\n/**\n * Sort options for room queries.\n */\nexport interface SortOptions {\n [fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';\n}\n\n/**\n * Built-in room cache fields that can be used for sorting.\n */\nexport type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';\n\n/**\n * Built-in room cache fields that can be used for filtering.\n */\nexport type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';\n\n/**\n * Extract metadata type from Room type\n */\nexport type ExtractRoomCacheMetadata<RoomType extends Room> = RoomType['~metadata'];\n\n/**\n * Generates a unique lock ID based on filter options.\n */\nexport function getLockId(filterOptions: any) {\n return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join(\"-\");\n}\n\n/**\n * Initialize a room cache which contains CRUD operations for room listings.\n *\n * @internal\n * @param initialValues - Predefined room properties.\n * @returns RoomData - New room cache.\n */\nexport function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {\n return {\n clients: 0,\n maxClients: Infinity,\n locked: false,\n private: false,\n metadata: undefined,\n createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),\n unlisted: false,\n ...initialValues,\n } as IRoomCache;\n}\n\nexport interface IRoomCache<Metadata = any> {\n /**\n * Room name.\n */\n name: string;\n\n /**\n * Unique identifier for the room.\n */\n roomId: string;\n\n /**\n * Process id where the room is running.\n */\n processId: string;\n\n /**\n * Number of clients connected to this room.\n */\n clients: number;\n\n /**\n * Maximum number of clients allowed to join the room.\n */\n maxClients: number;\n\n /**\n * Indicates if the room is locked (i.e. join requests are rejected).\n */\n locked?: boolean;\n\n /**\n * Indicates if the room is private\n * Private rooms can't be joined via `join()` or `joinOrCreate()`.\n */\n private?: boolean;\n\n /**\n * Public address of the server.\n */\n publicAddress?: string;\n\n /**\n * Do not show this room in lobby listing.\n */\n unlisted?: boolean;\n\n /**\n * Metadata associated with the room.\n */\n metadata?: Metadata;\n\n /**\n * When the room was created.\n */\n createdAt?: Date;\n}\n\nexport interface MatchMakerDriver {\n /**\n * Check if a room exists in room cache.\n *\n * @param roomId - The room id.\n *\n * @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.\n */\n has(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Query rooms in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.\n */\n query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<Array<IRoomCache<ExtractRoomCacheMetadata<T>>>> | Array<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Clean up rooms in room cache by process id.\n * @param processId - The process id.\n */\n cleanup?(processId: string): Promise<void>;\n\n /**\n * Query for a room in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns `IRoomCache` - An object contaning filtered room metadata.\n */\n findOne<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Batch-resolve room caches by roomId in a single backend round\n * trip. Missing roomIds are absent from the returned map. Used by\n * hot paths (per-join uniqueness check, by-user reverse-index\n * lookups) where K serial `findOne` calls would multiply round\n * trips needlessly.\n *\n * @param roomIds - Room ids to look up.\n *\n * @returns Map keyed by roomId of the rooms that were found.\n */\n findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;\n\n /**\n * Remove a room from room cache.\n *\n * @param roomId - The room id.\n */\n remove(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Update a room in room cache.\n *\n * @param IRoomCache - The room to update.\n * @param operations - The operations to update the room.\n */\n update(\n room: IRoomCache,\n operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>\n ): Promise<boolean> | boolean;\n\n /**\n * Persist a room in room cache.\n *\n * @param room - The room to persist.\n * @param create - If true, create a new record. If false (default), update existing record.\n */\n persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;\n\n /**\n * Empty the room cache. Used for testing purposes only.\n * @internal Do not call this method yourself.\n */\n clear(): void;\n\n /**\n * Boot the room cache medium (if available).\n */\n boot?(): Promise<void>;\n\n /**\n * Dispose the connection of the room cache medium.\n */\n shutdown(): void;\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BO,SAAS,UAAU,eAAoB;AAC5C,SAAO,OAAO,KAAK,aAAa,EAAE,IAAI,CAAC,QAAQ,GAAG,GAAG,IAAI,cAAc,GAAG,CAAC,EAAE,EAAE,KAAK,GAAG;AACzF;AASO,SAAS,oBAAoB,gBAAqC,CAAC,GAAe;AACvF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,WAAY,iBAAiB,cAAc,YAAa,IAAI,KAAK,cAAc,SAAS,IAAI,oBAAI,KAAK;AAAA,IACrG,UAAU;AAAA,IACV,GAAG;AAAA,EACL;AACF;",
6
6
  "names": []
7
7
  }
@@ -106,6 +106,18 @@ export interface MatchMakerDriver {
106
106
  * @returns `IRoomCache` - An object contaning filtered room metadata.
107
107
  */
108
108
  findOne<T extends Room = any>(conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>, sortOptions?: SortOptions): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;
109
+ /**
110
+ * Batch-resolve room caches by roomId in a single backend round
111
+ * trip. Missing roomIds are absent from the returned map. Used by
112
+ * hot paths (per-join uniqueness check, by-user reverse-index
113
+ * lookups) where K serial `findOne` calls would multiply round
114
+ * trips needlessly.
115
+ *
116
+ * @param roomIds - Room ids to look up.
117
+ *
118
+ * @returns Map keyed by roomId of the rooms that were found.
119
+ */
120
+ findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;
109
121
  /**
110
122
  * Remove a room from room cache.
111
123
  *
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/matchmaker/driver.ts"],
4
- "sourcesContent": ["import type { Room } from \"@colyseus/core\";\n\n/**\n * Sort options for room queries.\n */\nexport interface SortOptions {\n [fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';\n}\n\n/**\n * Built-in room cache fields that can be used for sorting.\n */\nexport type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';\n\n/**\n * Built-in room cache fields that can be used for filtering.\n */\nexport type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';\n\n/**\n * Extract metadata type from Room type\n */\nexport type ExtractRoomCacheMetadata<RoomType extends Room> = RoomType['~metadata'];\n\n/**\n * Generates a unique lock ID based on filter options.\n */\nexport function getLockId(filterOptions: any) {\n return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join(\"-\");\n}\n\n/**\n * Initialize a room cache which contains CRUD operations for room listings.\n *\n * @internal\n * @param initialValues - Predefined room properties.\n * @returns RoomData - New room cache.\n */\nexport function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {\n return {\n clients: 0,\n maxClients: Infinity,\n locked: false,\n private: false,\n metadata: undefined,\n createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),\n unlisted: false,\n ...initialValues,\n } as IRoomCache;\n}\n\nexport interface IRoomCache<Metadata = any> {\n /**\n * Room name.\n */\n name: string;\n\n /**\n * Unique identifier for the room.\n */\n roomId: string;\n\n /**\n * Process id where the room is running.\n */\n processId: string;\n\n /**\n * Number of clients connected to this room.\n */\n clients: number;\n\n /**\n * Maximum number of clients allowed to join the room.\n */\n maxClients: number;\n\n /**\n * Indicates if the room is locked (i.e. join requests are rejected).\n */\n locked?: boolean;\n\n /**\n * Indicates if the room is private\n * Private rooms can't be joined via `join()` or `joinOrCreate()`.\n */\n private?: boolean;\n\n /**\n * Public address of the server.\n */\n publicAddress?: string;\n\n /**\n * Do not show this room in lobby listing.\n */\n unlisted?: boolean;\n\n /**\n * Metadata associated with the room.\n */\n metadata?: Metadata;\n\n /**\n * When the room was created.\n */\n createdAt?: Date;\n}\n\nexport interface MatchMakerDriver {\n /**\n * Check if a room exists in room cache.\n *\n * @param roomId - The room id.\n *\n * @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.\n */\n has(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Query rooms in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.\n */\n query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<Array<IRoomCache<ExtractRoomCacheMetadata<T>>>> | Array<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Clean up rooms in room cache by process id.\n * @param processId - The process id.\n */\n cleanup?(processId: string): Promise<void>;\n\n /**\n * Query for a room in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns `IRoomCache` - An object contaning filtered room metadata.\n */\n findOne<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Remove a room from room cache.\n *\n * @param roomId - The room id.\n */\n remove(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Update a room in room cache.\n *\n * @param IRoomCache - The room to update.\n * @param operations - The operations to update the room.\n */\n update(\n room: IRoomCache,\n operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>\n ): Promise<boolean> | boolean;\n\n /**\n * Persist a room in room cache.\n *\n * @param room - The room to persist.\n * @param create - If true, create a new record. If false (default), update existing record.\n */\n persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;\n\n /**\n * Empty the room cache. Used for testing purposes only.\n * @internal Do not call this method yourself.\n */\n clear(): void;\n\n /**\n * Boot the room cache medium (if available).\n */\n boot?(): Promise<void>;\n\n /**\n * Dispose the connection of the room cache medium.\n */\n shutdown(): void;\n}\n"],
4
+ "sourcesContent": ["import type { Room } from \"@colyseus/core\";\n\n/**\n * Sort options for room queries.\n */\nexport interface SortOptions {\n [fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';\n}\n\n/**\n * Built-in room cache fields that can be used for sorting.\n */\nexport type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';\n\n/**\n * Built-in room cache fields that can be used for filtering.\n */\nexport type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';\n\n/**\n * Extract metadata type from Room type\n */\nexport type ExtractRoomCacheMetadata<RoomType extends Room> = RoomType['~metadata'];\n\n/**\n * Generates a unique lock ID based on filter options.\n */\nexport function getLockId(filterOptions: any) {\n return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join(\"-\");\n}\n\n/**\n * Initialize a room cache which contains CRUD operations for room listings.\n *\n * @internal\n * @param initialValues - Predefined room properties.\n * @returns RoomData - New room cache.\n */\nexport function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {\n return {\n clients: 0,\n maxClients: Infinity,\n locked: false,\n private: false,\n metadata: undefined,\n createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),\n unlisted: false,\n ...initialValues,\n } as IRoomCache;\n}\n\nexport interface IRoomCache<Metadata = any> {\n /**\n * Room name.\n */\n name: string;\n\n /**\n * Unique identifier for the room.\n */\n roomId: string;\n\n /**\n * Process id where the room is running.\n */\n processId: string;\n\n /**\n * Number of clients connected to this room.\n */\n clients: number;\n\n /**\n * Maximum number of clients allowed to join the room.\n */\n maxClients: number;\n\n /**\n * Indicates if the room is locked (i.e. join requests are rejected).\n */\n locked?: boolean;\n\n /**\n * Indicates if the room is private\n * Private rooms can't be joined via `join()` or `joinOrCreate()`.\n */\n private?: boolean;\n\n /**\n * Public address of the server.\n */\n publicAddress?: string;\n\n /**\n * Do not show this room in lobby listing.\n */\n unlisted?: boolean;\n\n /**\n * Metadata associated with the room.\n */\n metadata?: Metadata;\n\n /**\n * When the room was created.\n */\n createdAt?: Date;\n}\n\nexport interface MatchMakerDriver {\n /**\n * Check if a room exists in room cache.\n *\n * @param roomId - The room id.\n *\n * @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.\n */\n has(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Query rooms in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.\n */\n query<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<Array<IRoomCache<ExtractRoomCacheMetadata<T>>>> | Array<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Clean up rooms in room cache by process id.\n * @param processId - The process id.\n */\n cleanup?(processId: string): Promise<void>;\n\n /**\n * Query for a room in room cache for given conditions.\n *\n * @param conditions - Filtering conditions.\n *\n * @returns `IRoomCache` - An object contaning filtered room metadata.\n */\n findOne<T extends Room = any>(\n conditions: Partial<IRoomCache & ExtractRoomCacheMetadata<T>>,\n sortOptions?: SortOptions\n ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;\n\n /**\n * Batch-resolve room caches by roomId in a single backend round\n * trip. Missing roomIds are absent from the returned map. Used by\n * hot paths (per-join uniqueness check, by-user reverse-index\n * lookups) where K serial `findOne` calls would multiply round\n * trips needlessly.\n *\n * @param roomIds - Room ids to look up.\n *\n * @returns Map keyed by roomId of the rooms that were found.\n */\n findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;\n\n /**\n * Remove a room from room cache.\n *\n * @param roomId - The room id.\n */\n remove(roomId: string): Promise<boolean> | boolean;\n\n /**\n * Update a room in room cache.\n *\n * @param IRoomCache - The room to update.\n * @param operations - The operations to update the room.\n */\n update(\n room: IRoomCache,\n operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>\n ): Promise<boolean> | boolean;\n\n /**\n * Persist a room in room cache.\n *\n * @param room - The room to persist.\n * @param create - If true, create a new record. If false (default), update existing record.\n */\n persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;\n\n /**\n * Empty the room cache. Used for testing purposes only.\n * @internal Do not call this method yourself.\n */\n clear(): void;\n\n /**\n * Boot the room cache medium (if available).\n */\n boot?(): Promise<void>;\n\n /**\n * Dispose the connection of the room cache medium.\n */\n shutdown(): void;\n}\n"],
5
5
  "mappings": ";AA2BO,SAAS,UAAU,eAAoB;AAC5C,SAAO,OAAO,KAAK,aAAa,EAAE,IAAI,CAAC,QAAQ,GAAG,GAAG,IAAI,cAAc,GAAG,CAAC,EAAE,EAAE,KAAK,GAAG;AACzF;AASO,SAAS,oBAAoB,gBAAqC,CAAC,GAAe;AACvF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,WAAY,iBAAiB,cAAc,YAAa,IAAI,KAAK,cAAc,SAAS,IAAI,oBAAI,KAAK;AAAA,IACrG,UAAU;AAAA,IACV,GAAG;AAAA,EACL;AACF;",
6
6
  "names": []
7
7
  }
@@ -28,7 +28,7 @@ export declare class LocalPresence implements Presence {
28
28
  del(key: string): void;
29
29
  sadd(key: string, value: any): void;
30
30
  smembers(key: string): Promise<string[]>;
31
- sismember(key: string, field: string): Promise<1 | 0>;
31
+ sismember(key: string, field: string): Promise<0 | 1>;
32
32
  srem(key: string, value: any): void;
33
33
  scard(key: string): number;
34
34
  sinter(...keys: string[]): Promise<any[]>;
@@ -41,19 +41,17 @@ var LobbyRoom = class extends import_Room.Room {
41
41
  super(...arguments);
42
42
  this.rooms = [];
43
43
  this.clientOptions = {};
44
- this.messages = {
45
- filter: (client, filter) => {
46
- const clientOptions = this.clientOptions[client.sessionId];
47
- if (!clientOptions) {
48
- return;
49
- }
50
- clientOptions.filter = filter;
51
- client.send("rooms", this.filterItemsForClient(clientOptions));
52
- }
53
- };
54
44
  }
55
45
  async onCreate(options) {
56
46
  this["_listing"].unlisted = true;
47
+ this.onMessage("filter", (client, filter) => {
48
+ const clientOptions = this.clientOptions[client.sessionId];
49
+ if (!clientOptions) {
50
+ return;
51
+ }
52
+ clientOptions.filter = filter;
53
+ client.send("rooms", this.filterItemsForClient(clientOptions));
54
+ });
57
55
  this.unsubscribeLobby = await (0, import_Lobby.subscribeLobby)((roomId, data) => {
58
56
  const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
59
57
  const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/rooms/LobbyRoom.ts"],
4
- "sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n name?: string;\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n messages = {\n filter: (client: LobbyClient, filter: FilterInput) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n }\n }\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n clients.forEach((client) => {\n const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);\n const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n name?: string;\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport type LobbyMessages = {\n filter: (client: LobbyClient, filter: FilterInput) => void;\n};\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n declare messages: LobbyMessages;\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.onMessage('filter', (client, filter) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n });\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n clients.forEach((client) => {\n const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);\n const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -15,15 +15,16 @@ export interface FilterInput {
15
15
  export interface LobbyOptions {
16
16
  filter?: FilterInput;
17
17
  }
18
+ export type LobbyMessages = {
19
+ filter: (client: LobbyClient, filter: FilterInput) => void;
20
+ };
18
21
  export declare class LobbyRoom<Metadata = any> extends Room {
19
22
  rooms: IRoomCache<Metadata>[];
20
23
  unsubscribeLobby: () => void;
21
24
  clientOptions: {
22
25
  [sessionId: string]: LobbyOptions;
23
26
  };
24
- messages: {
25
- filter: (client: LobbyClient, filter: FilterInput) => void;
26
- };
27
+ messages: LobbyMessages;
27
28
  onCreate(options: any): Promise<void>;
28
29
  onJoin(client: LobbyClient, options: LobbyOptions): void;
29
30
  onLeave(client: LobbyClient): void;
@@ -7,19 +7,17 @@ var LobbyRoom = class extends Room {
7
7
  super(...arguments);
8
8
  this.rooms = [];
9
9
  this.clientOptions = {};
10
- this.messages = {
11
- filter: (client, filter) => {
12
- const clientOptions = this.clientOptions[client.sessionId];
13
- if (!clientOptions) {
14
- return;
15
- }
16
- clientOptions.filter = filter;
17
- client.send("rooms", this.filterItemsForClient(clientOptions));
18
- }
19
- };
20
10
  }
21
11
  async onCreate(options) {
22
12
  this["_listing"].unlisted = true;
13
+ this.onMessage("filter", (client, filter) => {
14
+ const clientOptions = this.clientOptions[client.sessionId];
15
+ if (!clientOptions) {
16
+ return;
17
+ }
18
+ clientOptions.filter = filter;
19
+ client.send("rooms", this.filterItemsForClient(clientOptions));
20
+ });
23
21
  this.unsubscribeLobby = await subscribeLobby((roomId, data) => {
24
22
  const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
25
23
  const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/rooms/LobbyRoom.ts"],
4
- "sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n name?: string;\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n messages = {\n filter: (client: LobbyClient, filter: FilterInput) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n }\n }\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n clients.forEach((client) => {\n const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);\n const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as matchMaker from '../MatchMaker.ts';\nimport type { IRoomCache } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport type { Client } from '../Transport.ts';\nimport { subscribeLobby } from '../matchmaker/Lobby.ts';\nimport { Room } from '../Room.ts';\n\n// TODO: use Schema state & filters on version 1.0.0\n\n// class DummyLobbyState extends Schema { // tslint:disable-line\n// @type(\"number\") public _: number;\n// }\n\n//\n// Strongly-typed client messages for LobbyRoom\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype LobbyClient = Client<{\n messages: {\n rooms: IRoomCache[];\n '+': [roomId: string, room: IRoomCache];\n '-': string;\n }\n}>;\n\nexport interface FilterInput {\n name?: string;\n metadata?: any;\n}\n\nexport interface LobbyOptions {\n filter?: FilterInput;\n}\n\nexport type LobbyMessages = {\n filter: (client: LobbyClient, filter: FilterInput) => void;\n};\n\nexport class LobbyRoom<Metadata = any> extends Room {\n public rooms: IRoomCache<Metadata>[] = [];\n public unsubscribeLobby: () => void;\n\n public clientOptions: { [sessionId: string]: LobbyOptions } = {};\n\n declare messages: LobbyMessages;\n\n public async onCreate(options: any) {\n // prevent LobbyRoom to notify itself\n this['_listing'].unlisted = true;\n\n this.onMessage('filter', (client, filter) => {\n const clientOptions = this.clientOptions[client.sessionId];\n if (!clientOptions) { return; }\n\n clientOptions.filter = filter;\n client.send('rooms', this.filterItemsForClient(clientOptions));\n });\n\n this.unsubscribeLobby = await subscribeLobby((roomId, data) => {\n const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);\n const clients = this.clients.filter((client) => this.clientOptions[client.sessionId]);\n\n if (!data) {\n // remove room listing data\n if (roomIndex !== -1) {\n const previousData = this.rooms[roomIndex];\n\n this.rooms.splice(roomIndex, 1);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter)) {\n client.send('-', roomId);\n }\n });\n }\n\n } else if (roomIndex === -1) {\n // append room listing data\n this.rooms.push(data);\n\n clients.forEach((client) => {\n if (this.filterItemForClient(data, this.clientOptions[client.sessionId].filter)) {\n client.send('+', [roomId, data]);\n }\n });\n\n } else {\n const previousData = this.rooms[roomIndex];\n\n // replace room listing data\n this.rooms[roomIndex] = data;\n\n clients.forEach((client) => {\n const hadData = this.filterItemForClient(previousData, this.clientOptions[client.sessionId].filter);\n const hasData = this.filterItemForClient(data, this.clientOptions[client.sessionId].filter);\n\n if (hadData && !hasData) {\n client.send('-', roomId);\n\n } else if (hasData) {\n client.send('+', [roomId, data]);\n }\n });\n }\n });\n\n this.rooms = await matchMaker.query({ private: false, unlisted: false });\n }\n\n public onJoin(client: LobbyClient, options: LobbyOptions) {\n this.clientOptions[client.sessionId] = (\n !Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object\n options\n ) || {};\n client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));\n }\n\n public onLeave(client: LobbyClient) {\n delete this.clientOptions[client.sessionId];\n }\n\n public onDispose() {\n if (this.unsubscribeLobby) {\n this.unsubscribeLobby();\n }\n }\n\n protected filterItemsForClient(options: LobbyOptions): IRoomCache<Metadata>[] {\n const filter = options.filter;\n\n return (filter)\n ? this.rooms.filter((room) => this.filterItemForClient(room, filter))\n : this.rooms;\n }\n\n protected filterItemForClient(room: IRoomCache, filter?: LobbyOptions['filter']) {\n if (!filter) {\n return true;\n }\n\n let isAllowed = true;\n\n if (filter.name !== room.name) {\n isAllowed = false;\n }\n\n if (filter.metadata) {\n for (const field in filter.metadata) {\n if (room.metadata[field] !== filter.metadata[field]) {\n isAllowed = false;\n break;\n }\n }\n }\n\n return isAllowed;\n }\n\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -98,7 +98,7 @@ var QueueRoom = class extends import_Room.Room {
98
98
  throw new import_ServerError.ServerError(import_shared_types.ErrorCode.APPLICATION_ERROR, "QueueRoom: 'matchRoomName' option is required.");
99
99
  }
100
100
  (0, import_Debug.debugMatchMaking)("QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);
101
- this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
101
+ this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);
102
102
  }
103
103
  onJoin(client, options, auth) {
104
104
  this.addToQueue(client, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/rooms/QueueRoom.ts"],
4
- "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -64,7 +64,7 @@ var QueueRoom = class extends Room {
64
64
  throw new ServerError(ErrorCode.APPLICATION_ERROR, "QueueRoom: 'matchRoomName' option is required.");
65
65
  }
66
66
  debugMatchMaking("QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);
67
- this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
67
+ this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);
68
68
  }
69
69
  onJoin(client, options, auth) {
70
70
  this.addToQueue(client, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/rooms/QueueRoom.ts"],
4
- "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
5
- "mappings": ";AAAA,SAAS,YAAY;AAGrB,YAAY,gBAAgB;AAC5B,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAC5B,SAAS,WAAW,iBAAiB;AAqHrC,IAAM,eAAe,uBAAO,eAAe;AAC3C,IAAM,kBAAkB,CAAC,QAAyB,eAAgC;AAChF,QAAM,OAAO,KAAK,IAAI,OAAO,OAAO,WAAW,WAAW;AAC1D,QAAM,YAAa,OAAO,WAAW;AAErC,SAAQ,OAAO,MAAM,aAAa;AACpC;AAEO,IAAM,YAAN,cAAwB,KAAK;AAAA,EAA7B;AAAA;AACL,sBAAa;AAEb,iCAAiC;AAEjC,4BAAmB;AACnB,uCAAuC;AAKvC;AAAA;AAAA;AAAA,6BAAoB;AAKpB;AAAA;AAAA;AAAA,kBAA4B,CAAC;AAC7B,8BAAwC,CAAC;AAIzC,SAAU,UAAU;AACpB,SAAU,eAAe,CAAC,UAAsC,sBAAW,KAAK,eAAe,CAAC,CAAC;AAEjG,oBAAW;AAAA,MACT,SAAS,CAAC,QAAgB,MAAe;AACvC,cAAM,YAAY,OAAO;AAEzB,YAAI,aAAa,UAAU,SAAS,OAAQ,UAAU,MAAM,cAAe,UAAU;AACnF,oBAAU,YAAY;AACtB,oBAAU,MAAM;AAChB,iBAAO,MAAM,UAAU,cAAc;AAAA,QACvC;AAAA,MACF;AAAA,IACF;AAAA;AAAA,EAEA,SAAS,SAAuB;AAC9B,QAAI,OAAO,QAAQ,qBAAsB,UAAU;AACjD,WAAK,mBAAmB,QAAQ;AAAA,IAClC;AAEA,QAAI,OAAO,QAAQ,eAAgB,UAAU;AAC3C,WAAK,aAAa,QAAQ;AAAA,IAC5B;AAEA,QAAI,OAAO,QAAQ,gBAAiB,UAAU;AAC5C,WAAK,cAAc,QAAQ;AAAA,IAC7B;AAEA,QAAI,OAAO,QAAQ,0BAA2B,aAAa;AACzD,WAAK,wBAAwB,QAAQ;AAAA,IACvC;AAEA,QAAI,OAAO,QAAQ,YAAa,YAAY;AAC1C,WAAK,UAAU,QAAQ;AAAA,IACzB;AAEA,QAAI,OAAO,QAAQ,iBAAkB,YAAY;AAC/C,WAAK,eAAe,QAAQ;AAAA,IAC9B;AAEA,QAAI,QAAQ,eAAe;AACzB,WAAK,gBAAgB,QAAQ;AAAA,IAE/B,OAAO;AACL,YAAM,IAAI,YAAY,UAAU,mBAAmB,gDAAgD;AAAA,IACrG;AAEA,qBAAiB,+HAA+H,KAAK,YAAY,KAAK,kBAAkB,KAAK,aAAa,KAAK,uBAAuB,KAAK,aAAa;AAKxP,SAAK,sBAAsB,MAAM,KAAK,oBAAoB,GAAG,KAAK,iBAAiB;AAAA,EACrF;AAAA,EAEA,OAAO,QAAqB,SAAc,MAAgB;AACxD,SAAK,WAAW,QAAQ;AAAA,MACtB,MAAM,QAAQ;AAAA,MACd,QAAQ,QAAQ;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,QAAqB,WAA4B;AAC1D,QAAI,UAAU,iBAAiB,QAAW;AACxC,gBAAU,eAAe;AAAA,IAC3B;AACA,WAAO,WAAW;AAGlB,WAAO,KAAK,WAAW,CAAC;AAAA,EAC1B;AAAA,EAEA,mBAAmB;AACjB,UAAM,QAAyB,EAAE,SAAS,CAAC,GAAG,aAAa,EAAE;AAC7D,SAAK,OAAO,KAAK,KAAK;AACtB,WAAO;AAAA,EACT;AAAA,EAEA,sBAAsB;AAEpB,SAAK,OAAO,SAAS;AACrB,SAAK,mBAAmB,SAAS;AAEjC,UAAM,gBAAiB,KAAK,QACzB,OAAO,CAAC,WAAW;AAIlB,aACE,OAAO,YACP,OAAO,SAAS,OAAO,UAAU;AAAA,IAErC,CAAC,EACA,KAAK,CAAC,GAAG,MAAM;AAId,aAAO,EAAE,SAAS,OAAO,EAAE,SAAS;AAAA,IACtC,CAAC;AAKH,QAAI,OAAO,KAAK,gBAAiB,UAAU;AACzC,WAAK,kBAAkB,aAAa;AAAA,IAEtC,OAAO;AACL,WAAK,oBAAoB,aAAa;AAAA,IACxC;AAEA,SAAK,2BAA2B;AAChC,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EAEA,kBAAkB,eAAwD;AACxE,UAAM,YAA2D,CAAC;AAElE,kBAAc,QAAQ,CAAC,WAAW;AAChC,YAAM,SAAS,OAAO,SAAS,UAAU;AAGzC,UAAI,CAAC,UAAU,MAAM,GAAG;AACtB,kBAAU,MAAM,IAAI,EAAE,QAAgB,SAAS,CAAC,GAAG,aAAa,EAAG;AAAA,MACrE;AAEA,gBAAU,MAAM,EAAE,eAAe,OAAO,SAAS;AACjD,gBAAU,MAAM,EAAE,QAAQ,KAAK,MAAM;AAAA,IACvC,CAAC;AAGD,QAAI,QAAQ,OAAO,OAAO,SAAS,EAAE,IAAI,CAAC,SAAS;AACjD,WAAK,eAAe,KAAK,QAAQ;AACjC,aAAO;AAAA,IACT,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM;AAEhB,aAAO,EAAE,cAAc,EAAE;AAAA,IAC3B,CAAC;AAGD,OAAG;AACD,UAAI,eAAgC,KAAK,iBAAiB;AAC1D,cAAQ,MAAM,OAAO,CAAC,SAAS;AAE7B,cAAM,YAAY,KAAK,cAAc,KAAK,QAAQ;AAKlD,uBAAe,KAAK,oBAAoB,KAAK,QAAQ,OAAO,GAAG,KAAK,WAAW,GAAG,cAAc,SAAS;AAEzG,YAAI,KAAK,QAAQ,UAAU,KAAK,aAAa;AAE3C,iBAAO;AAAA,QACT;AAGA,aAAK,QAAQ,QAAQ,CAAC,WAAW,OAAO,SAAS,cAAc;AAE/D,eAAO;AAAA,MACT,CAAC;AAAA,IACH,SAAS,MAAM,UAAU;AAAA,EAC3B;AAAA,EAEA,oBACE,eACA,eAAgC,KAAK,iBAAiB,GACtD,YAAoB,GACpB;AACA,aAAS,IAAI,GAAG,IAAI,cAAc,QAAQ,IAAI,GAAG,KAAK;AACpD,YAAM,SAAS,cAAc,CAAC;AAC9B,YAAM,WAAW,OAAO;AACxB,YAAM,eAAe,SAAS;AAE9B,UAAI,aAAa,cAAc,GAAG;AAChC,YACE,CAAC,KAAK,QAAQ,UAAU,YAAY,KACpC,CAAC,SAAS,cACV;AACA,yBAAe,KAAK,iBAAiB;AACrC,sBAAY;AAAA,QACd;AAAA,MACF;AAEA,eAAS,QAAQ;AACjB,mBAAa,QAAQ,KAAK,MAAM;AAEhC,mBAAa,SAAS;AACtB,mBAAa,cAAc,YAAY,aAAa,QAAQ;AAG5D,UAAI,aAAa,QAAQ,WAAW,KAAK,YAAY;AACnD,qBAAa,QAAQ;AACrB,uBAAe,KAAK,iBAAiB;AACrC,oBAAY;AACZ;AAAA,MACF;AAEA,UAAI,gBAAgB,KAAK,oBAAoB,KAAK,uBAAuB;AAIvE,YAAI,KAAK,mBAAmB,QAAQ,YAAY,MAAM,IAAI;AACxD,eAAK,mBAAmB,KAAK,YAAY;AAAA,QAC3C;AAAA,MAEF,WACE,KAAK,gCAAgC,UACrC,gBAAgB,KAAK,6BACrB;AAIA,iBAAS,eAAe;AAAA,MAC1B;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,6BAA6B;AAI3B,SAAK,mBAAmB,QAAQ,CAAC,UAAU;AACzC,YAAM,QAAQ,MAAM,QAAQ,MAAM,CAAC,MAAM;AAEvC,eAAO,EAAE,UAAU,eAAe;AAAA,MAEpC,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,qBAAqB;AACnB,SAAK,OAAO,QAAQ,OAAO,UAAU;AACnC,UAAI,MAAM,OAAO;AACf,cAAM,YAAY;AAElB,YAAI;AAIF,gBAAM,OAAO,MAAM,KAAK,aAAa,KAAK,MAAM,KAAK;AAMrD,gBAAiB;AAAA,YACf;AAAA,YACA,MAAM,QAAQ,IAAI,CAAC,YAAY;AAAA,cAC7B,WAAW,OAAO;AAAA,cAClB,SAAS,OAAO,SAAS;AAAA,cACzB,MAAM,OAAO;AAAA,YACf,EAAE;AAAA,UACJ;AAKA,gBAAM,QAAQ,QAAQ,CAAC,QAAQ,MAAM;AACnC,mBAAO,KAAK,QAAmB,gCAAqB,MAAM,OAAO,SAAS,CAAC;AAAA,UAC7E,CAAC;AAAA,QAEH,SAAS,GAAQ;AAKf,gBAAM,QAAQ,QAAQ,YAAU,OAAO,MAAM,MAAM,EAAE,OAAO,CAAC;AAAA,QAC/D;AAAA,MAEF,OAAO;AAIL,cAAM,QAAQ,QAAQ,CAAC,WAAW;AAIhC,gBAAM,mBAAmB,MAAM,QAAQ;AACvC,cAAI,OAAO,SAAS,yBAAyB,kBAAkB;AAC7D,mBAAO,SAAS,uBAAuB;AACvC,mBAAO,KAAK,WAAW,gBAAgB;AAAA,UACzC;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AAEF;",
4
+ "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -20,27 +20,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // packages/core/src/rooms/RelayRoom.ts
21
21
  var RelayRoom_exports = {};
22
22
  __export(RelayRoom_exports, {
23
- RelayRoom: () => RelayRoom
23
+ Player: () => Player,
24
+ RelayRoom: () => RelayRoom,
25
+ State: () => State
24
26
  });
25
27
  module.exports = __toCommonJS(RelayRoom_exports);
26
28
  var import_shared_types = require("@colyseus/shared-types");
27
29
  var import_schema = require("@colyseus/schema");
28
30
  var import_Room = require("../Room.cjs");
29
- var Player = class extends import_schema.Schema {
30
- };
31
- (0, import_schema.defineTypes)(Player, {
32
- connected: "boolean",
33
- name: "string",
34
- sessionId: "string"
31
+ var Player = (0, import_schema.schema)({
32
+ connected: import_schema.t.boolean(),
33
+ name: import_schema.t.string(),
34
+ sessionId: import_schema.t.string()
35
35
  });
36
- var State = class extends import_schema.Schema {
37
- constructor() {
38
- super(...arguments);
39
- this.players = new import_schema.MapSchema();
40
- }
41
- };
42
- (0, import_schema.defineTypes)(State, {
43
- players: { map: Player }
36
+ var State = (0, import_schema.schema)({
37
+ players: import_schema.t.map(Player)
44
38
  });
45
39
  var RelayRoom = class extends import_Room.Room {
46
40
  constructor() {
@@ -89,5 +83,7 @@ var RelayRoom = class extends import_Room.Room {
89
83
  };
90
84
  // Annotate the CommonJS export names for ESM import in node:
91
85
  0 && (module.exports = {
92
- RelayRoom
86
+ Player,
87
+ RelayRoom,
88
+ State
93
89
  });