@babylonjs-toolkit/next 9.30.0 → 9.34.0

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 (191) hide show
  1. package/lib/contentdownloadoutcome.d.ts +2 -0
  2. package/lib/contentdownloadoutcome.d.ts.map +1 -0
  3. package/lib/contentdownloadoutcome.js +1 -0
  4. package/lib/contentgraphicsoutcome.d.ts +2 -0
  5. package/lib/contentgraphicsoutcome.d.ts.map +1 -0
  6. package/lib/contentgraphicsoutcome.js +1 -0
  7. package/lib/contentgraphicsstatus.d.ts +2 -0
  8. package/lib/contentgraphicsstatus.d.ts.map +1 -0
  9. package/lib/contentgraphicsstatus.js +1 -0
  10. package/lib/contentinstaller.d.ts +245 -0
  11. package/lib/contentinstaller.d.ts.map +1 -0
  12. package/lib/contentinstaller.js +1929 -0
  13. package/lib/contentinstalleravailability.d.ts +2 -0
  14. package/lib/contentinstalleravailability.d.ts.map +1 -0
  15. package/lib/contentinstalleravailability.js +1 -0
  16. package/lib/contentinstallererrorkind.d.ts +2 -0
  17. package/lib/contentinstallererrorkind.d.ts.map +1 -0
  18. package/lib/contentinstallererrorkind.js +1 -0
  19. package/lib/contentinstallphase.d.ts +2 -0
  20. package/lib/contentinstallphase.d.ts.map +1 -0
  21. package/lib/contentinstallphase.js +1 -0
  22. package/lib/contentinstallpolicy.d.ts +2 -0
  23. package/lib/contentinstallpolicy.d.ts.map +1 -0
  24. package/lib/contentinstallpolicy.js +1 -0
  25. package/lib/contentupdatepolicy.d.ts +2 -0
  26. package/lib/contentupdatepolicy.d.ts.map +1 -0
  27. package/lib/contentupdatepolicy.js +1 -0
  28. package/lib/iafterspawned.d.ts +4 -0
  29. package/lib/iafterspawned.d.ts.map +1 -0
  30. package/lib/iafterspawned.js +1 -0
  31. package/lib/iaftertick.d.ts +4 -0
  32. package/lib/iaftertick.d.ts.map +1 -0
  33. package/lib/iaftertick.js +1 -0
  34. package/lib/iafterupdate.d.ts +4 -0
  35. package/lib/iafterupdate.d.ts.map +1 -0
  36. package/lib/iafterupdate.js +1 -0
  37. package/lib/ibeforetick.d.ts +4 -0
  38. package/lib/ibeforetick.d.ts.map +1 -0
  39. package/lib/ibeforetick.js +1 -0
  40. package/lib/ibeforeupdate.d.ts +4 -0
  41. package/lib/ibeforeupdate.d.ts.map +1 -0
  42. package/lib/ibeforeupdate.js +1 -0
  43. package/lib/icontentavailabilityblock.d.ts +6 -0
  44. package/lib/icontentavailabilityblock.d.ts.map +1 -0
  45. package/lib/icontentavailabilityblock.js +1 -0
  46. package/lib/icontentchannelmessage.d.ts +24 -0
  47. package/lib/icontentchannelmessage.d.ts.map +1 -0
  48. package/lib/icontentchannelmessage.js +1 -0
  49. package/lib/icontentdownloadjob.d.ts +7 -0
  50. package/lib/icontentdownloadjob.d.ts.map +1 -0
  51. package/lib/icontentdownloadjob.js +1 -0
  52. package/lib/icontentfileref.d.ts +9 -0
  53. package/lib/icontentfileref.d.ts.map +1 -0
  54. package/lib/icontentfileref.js +1 -0
  55. package/lib/icontentforwardedrequest.d.ts +9 -0
  56. package/lib/icontentforwardedrequest.d.ts.map +1 -0
  57. package/lib/icontentforwardedrequest.js +1 -0
  58. package/lib/icontentgraphicssession.d.ts +8 -0
  59. package/lib/icontentgraphicssession.d.ts.map +1 -0
  60. package/lib/icontentgraphicssession.js +1 -0
  61. package/lib/icontentgroupref.d.ts +6 -0
  62. package/lib/icontentgroupref.d.ts.map +1 -0
  63. package/lib/icontentgroupref.js +1 -0
  64. package/lib/icontentgroupstatus.d.ts +14 -0
  65. package/lib/icontentgroupstatus.d.ts.map +1 -0
  66. package/lib/icontentgroupstatus.js +1 -0
  67. package/lib/icontentinflightdownload.d.ts +8 -0
  68. package/lib/icontentinflightdownload.d.ts.map +1 -0
  69. package/lib/icontentinflightdownload.js +1 -0
  70. package/lib/icontentinstallcall.d.ts +6 -0
  71. package/lib/icontentinstallcall.d.ts.map +1 -0
  72. package/lib/icontentinstallcall.js +1 -0
  73. package/lib/icontentinstallererror.d.ts +7 -0
  74. package/lib/icontentinstallererror.d.ts.map +1 -0
  75. package/lib/icontentinstallererror.js +1 -0
  76. package/lib/icontentinstalleroptions.d.ts +20 -0
  77. package/lib/icontentinstalleroptions.d.ts.map +1 -0
  78. package/lib/icontentinstalleroptions.js +1 -0
  79. package/lib/icontentinstallerstate.d.ts +30 -0
  80. package/lib/icontentinstallerstate.d.ts.map +1 -0
  81. package/lib/icontentinstallerstate.js +1 -0
  82. package/lib/icontentinstalloptions.d.ts +5 -0
  83. package/lib/icontentinstalloptions.d.ts.map +1 -0
  84. package/lib/icontentinstalloptions.js +1 -0
  85. package/lib/icontentinstallprogress.d.ts +21 -0
  86. package/lib/icontentinstallprogress.d.ts.map +1 -0
  87. package/lib/icontentinstallprogress.js +1 -0
  88. package/lib/icontentmanifest.d.ts +10 -0
  89. package/lib/icontentmanifest.d.ts.map +1 -0
  90. package/lib/icontentmanifest.js +1 -0
  91. package/lib/icontentmanifestfile.d.ts +8 -0
  92. package/lib/icontentmanifestfile.d.ts.map +1 -0
  93. package/lib/icontentmanifestfile.js +1 -0
  94. package/lib/icontentmanifestgroup.d.ts +5 -0
  95. package/lib/icontentmanifestgroup.d.ts.map +1 -0
  96. package/lib/icontentmanifestgroup.js +1 -0
  97. package/lib/icontentpreparegraphicsoptions.d.ts +5 -0
  98. package/lib/icontentpreparegraphicsoptions.d.ts.map +1 -0
  99. package/lib/icontentpreparegraphicsoptions.js +1 -0
  100. package/lib/icontentprogresssession.d.ts +10 -0
  101. package/lib/icontentprogresssession.d.ts.map +1 -0
  102. package/lib/icontentprogresssession.js +1 -0
  103. package/lib/icontentretrywait.d.ts +6 -0
  104. package/lib/icontentretrywait.d.ts.map +1 -0
  105. package/lib/icontentretrywait.js +1 -0
  106. package/lib/icontentrootoptions.d.ts +6 -0
  107. package/lib/icontentrootoptions.d.ts.map +1 -0
  108. package/lib/icontentrootoptions.js +1 -0
  109. package/lib/icontentrootruntime.d.ts +8 -0
  110. package/lib/icontentrootruntime.d.ts.map +1 -0
  111. package/lib/icontentrootruntime.js +1 -0
  112. package/lib/icontentrootstate.d.ts +29 -0
  113. package/lib/icontentrootstate.d.ts.map +1 -0
  114. package/lib/icontentrootstate.js +1 -0
  115. package/lib/icontentstorageestimate.d.ts +8 -0
  116. package/lib/icontentstorageestimate.d.ts.map +1 -0
  117. package/lib/icontentstorageestimate.js +1 -0
  118. package/lib/icontentstoreresult.d.ts +8 -0
  119. package/lib/icontentstoreresult.d.ts.map +1 -0
  120. package/lib/icontentstoreresult.js +1 -0
  121. package/lib/icontentstorewaiter.d.ts +6 -0
  122. package/lib/icontentstorewaiter.d.ts.map +1 -0
  123. package/lib/icontentstorewaiter.js +1 -0
  124. package/lib/icontentupdateinfo.d.ts +8 -0
  125. package/lib/icontentupdateinfo.d.ts.map +1 -0
  126. package/lib/icontentupdateinfo.js +1 -0
  127. package/lib/idespawned.d.ts +5 -0
  128. package/lib/idespawned.d.ts.map +1 -0
  129. package/lib/idespawned.js +1 -0
  130. package/lib/iinputauthoritylost.d.ts +4 -0
  131. package/lib/iinputauthoritylost.d.ts.map +1 -0
  132. package/lib/iinputauthoritylost.js +1 -0
  133. package/lib/index.d.ts +141 -29
  134. package/lib/index.d.ts.map +1 -1
  135. package/lib/index.js +141 -29
  136. package/lib/iplayerjoined.d.ts +5 -0
  137. package/lib/iplayerjoined.d.ts.map +1 -0
  138. package/lib/iplayerjoined.js +1 -0
  139. package/lib/iplayerleft.d.ts +5 -0
  140. package/lib/iplayerleft.d.ts.map +1 -0
  141. package/lib/iplayerleft.js +1 -0
  142. package/lib/ispawned.d.ts +4 -0
  143. package/lib/ispawned.d.ts.map +1 -0
  144. package/lib/ispawned.js +1 -0
  145. package/lib/istateauthoritychanged.d.ts +4 -0
  146. package/lib/istateauthoritychanged.d.ts.map +1 -0
  147. package/lib/istateauthoritychanged.js +1 -0
  148. package/lib/loopbackdelivery.d.ts +6 -0
  149. package/lib/loopbackdelivery.d.ts.map +1 -0
  150. package/lib/loopbackdelivery.js +1 -0
  151. package/lib/loopbackhub.d.ts +206 -0
  152. package/lib/loopbackhub.d.ts.map +1 -0
  153. package/lib/loopbackhub.js +986 -0
  154. package/lib/loopbackhuboptions.d.ts +8 -0
  155. package/lib/loopbackhuboptions.d.ts.map +1 -0
  156. package/lib/loopbackhuboptions.js +1 -0
  157. package/lib/network_reconnect_grace_seconds.d.ts +2 -0
  158. package/lib/network_reconnect_grace_seconds.d.ts.map +1 -0
  159. package/lib/network_reconnect_grace_seconds.js +1 -0
  160. package/lib/networkstats.d.ts +51 -0
  161. package/lib/networkstats.d.ts.map +1 -0
  162. package/lib/networkstats.js +204 -0
  163. package/lib/networkstatssample.d.ts +7 -0
  164. package/lib/networkstatssample.d.ts.map +1 -0
  165. package/lib/networkstatssample.js +1 -0
  166. package/lib/networkstatssliderrow.d.ts +9 -0
  167. package/lib/networkstatssliderrow.d.ts.map +1 -0
  168. package/lib/networkstatssliderrow.js +1 -0
  169. package/lib/project.d.ts +2907 -133
  170. package/lib/project.d.ts.map +1 -1
  171. package/lib/project.js +14960 -3764
  172. package/lib/scenemanager.d.ts +4434 -51
  173. package/lib/scenemanager.d.ts.map +1 -1
  174. package/lib/scenemanager.js +18725 -1147
  175. package/lib/shadercodecachememorystorage.d.ts +10 -0
  176. package/lib/shadercodecachememorystorage.d.ts.map +1 -0
  177. package/lib/shadercodecachememorystorage.js +32 -0
  178. package/lib/workers/bt-content-worker.js +1180 -0
  179. package/lib/zigzagdecode.d.ts +2 -0
  180. package/lib/zigzagdecode.d.ts.map +1 -0
  181. package/lib/zigzagdecode.js +3 -0
  182. package/lib/zigzagencode.d.ts +2 -0
  183. package/lib/zigzagencode.d.ts.map +1 -0
  184. package/lib/zigzagencode.js +3 -0
  185. package/package.json +18 -13
  186. package/lib/entitycontroller.d.ts +0 -11
  187. package/lib/entitycontroller.d.ts.map +0 -1
  188. package/lib/entitycontroller.js +0 -41
  189. package/lib/sgshadowdepth.d.ts +0 -19
  190. package/lib/sgshadowdepth.d.ts.map +0 -1
  191. package/lib/sgshadowdepth.js +0 -214
@@ -0,0 +1,986 @@
1
+ import { NetworkRoute, NetworkConditionsSimulator, RpcChannel, NetworkProtocol, NetDisconnectReason, NetworkError, FUSION_CONTROL_WELCOME, ShutdownReason, GameMode, NETWORK_DEFAULT_TICK_RATE, NETWORK_START_SCENE_LOAD_ID, NetworkFrameKind, NetworkDespawnReason, NetworkObjectBlockFlags, NetworkIdUtility, NetworkObjectOptions, FUSION_CONTROL_SHUTDOWN, FUSION_CONTROL_CONNECT_RESULT, FUSION_CONTROL_SESSION, FUSION_CONTROL_AREA, FUSION_CONTROL_PLAYER_LEFT, FUSION_CONTROL_HOST_MIGRATION, FUSION_CONTROL_CONNECT_REQUEST, FUSION_CONTROL_PLAYER_JOINED, NETWORK_SHARED_ID_PLAYER_STRIDE, FUSION_CONTROL_MASTER_CLIENT } from './scenemanager';
2
+ import { NETWORK_RECONNECT_GRACE_SECONDS } from './network_reconnect_grace_seconds';
3
+ export class LoopbackHub {
4
+ constructor(options = {}) {
5
+ this.transports = new Map();
6
+ this.pendingDeliveries = [];
7
+ this.linkDeliveryTimes = new Map();
8
+ this.clockMs = 0;
9
+ this.deliverySequence = 0;
10
+ this.transportCount = 0;
11
+ this.sessionUpdateTimer = null;
12
+ this.publishedSession = null;
13
+ this.sessionLocked = false;
14
+ this.conditions = new NetworkConditionsSimulator(options.seed != null ? options.seed : 1);
15
+ this.conditions.latencyMs = options.latencyMs || 0;
16
+ this.conditions.jitterMs = options.jitterMs || 0;
17
+ this.conditions.lossPercent = options.lossPercent || 0;
18
+ this.realTime = options.realTime === true;
19
+ this.realTimeOriginMs = performance.now();
20
+ this.sessionLogic = new FusionSessionLogic(this);
21
+ if (this.realTime)
22
+ this.sessionUpdateTimer = setInterval(() => this.sessionLogic.update(this.nowMilliseconds()), LoopbackHub.SESSION_UPDATE_INTERVAL_MS);
23
+ }
24
+ get logic() {
25
+ return this.sessionLogic;
26
+ }
27
+ get sessionMetadata() {
28
+ return this.publishedSession;
29
+ }
30
+ get isLocked() {
31
+ return this.sessionLocked;
32
+ }
33
+ createTransport() {
34
+ this.transportCount += 1;
35
+ const clientKey = LoopbackHub.CLIENT_KEY_PREFIX + this.transportCount;
36
+ const transport = new LoopbackTransport(this, clientKey);
37
+ this.transports.set(clientKey, transport);
38
+ return transport;
39
+ }
40
+ advance(milliseconds) {
41
+ const targetMs = this.clockMs + milliseconds;
42
+ this.deliverDue(targetMs);
43
+ this.clockMs = targetMs;
44
+ this.sessionLogic.update(this.clockMs);
45
+ }
46
+ dispose() {
47
+ if (this.sessionUpdateTimer != null)
48
+ clearInterval(this.sessionUpdateTimer);
49
+ this.sessionUpdateTimer = null;
50
+ }
51
+ nowMilliseconds() {
52
+ return this.realTime ? performance.now() - this.realTimeOriginMs : this.clockMs;
53
+ }
54
+ sendFrame(clientKey, sourcePlayerId, frame) {
55
+ const copy = frame.slice();
56
+ this.enqueue(LoopbackHub.linkFromRoom(clientKey), () => {
57
+ const transport = this.transports.get(clientKey);
58
+ if (transport != null)
59
+ transport.deliverFrame(sourcePlayerId, copy);
60
+ });
61
+ }
62
+ sendControl(clientKey, name, payload) {
63
+ const copy = JSON.parse(JSON.stringify(payload));
64
+ this.enqueue(LoopbackHub.linkFromRoom(clientKey), () => {
65
+ const transport = this.transports.get(clientKey);
66
+ if (transport != null)
67
+ transport.deliverControl(name, copy);
68
+ });
69
+ }
70
+ setSessionMetadata(session) {
71
+ this.publishedSession = session;
72
+ }
73
+ lockSession(locked) {
74
+ this.sessionLocked = locked;
75
+ }
76
+ joinClient(clientKey, request) {
77
+ return this.sessionLogic.join(clientKey, request);
78
+ }
79
+ sendFromClient(clientKey, route, targetPlayerId, frame, channel) {
80
+ if (this.conditions.shouldDrop(channel))
81
+ return;
82
+ const envelope = NetworkProtocol.encodeEnvelope(route, targetPlayerId, frame);
83
+ this.enqueue(LoopbackHub.linkToRoom(clientKey), () => this.sessionLogic.receiveFrame(clientKey, envelope, channel));
84
+ }
85
+ sendControlFromClient(clientKey, name, payload) {
86
+ const copy = JSON.parse(JSON.stringify(payload));
87
+ this.enqueue(LoopbackHub.linkToRoom(clientKey), () => this.sessionLogic.receiveControl(clientKey, name, copy));
88
+ }
89
+ leaveClient(clientKey) {
90
+ this.enqueue(LoopbackHub.linkToRoom(clientKey), () => {
91
+ this.sessionLogic.leave(clientKey);
92
+ this.transports.delete(clientKey);
93
+ });
94
+ }
95
+ dropClient(clientKey) {
96
+ this.transports.delete(clientKey);
97
+ this.sessionLogic.drop(clientKey);
98
+ }
99
+ static linkToRoom(clientKey) {
100
+ return clientKey + LoopbackHub.LINK_SEPARATOR + LoopbackHub.ROOM_LINK_NAME;
101
+ }
102
+ static linkFromRoom(clientKey) {
103
+ return LoopbackHub.ROOM_LINK_NAME + LoopbackHub.LINK_SEPARATOR + clientKey;
104
+ }
105
+ enqueue(link, deliver) {
106
+ const previousTimeMs = this.linkDeliveryTimes.get(link) || 0;
107
+ const timeMs = Math.max(this.nowMilliseconds() + this.conditions.sampleDelayMs(), previousTimeMs);
108
+ this.linkDeliveryTimes.set(link, timeMs);
109
+ if (this.realTime) {
110
+ setTimeout(deliver, timeMs - this.nowMilliseconds());
111
+ return;
112
+ }
113
+ this.deliverySequence += 1;
114
+ const delivery = { timeMs: timeMs, sequence: this.deliverySequence, deliver: deliver };
115
+ let insertAt = this.pendingDeliveries.length;
116
+ while (insertAt > 0 && this.pendingDeliveries[insertAt - 1].timeMs > timeMs)
117
+ insertAt--;
118
+ this.pendingDeliveries.splice(insertAt, 0, delivery);
119
+ }
120
+ deliverDue(targetMs) {
121
+ while (this.pendingDeliveries.length > 0 && this.pendingDeliveries[0].timeMs <= targetMs) {
122
+ const delivery = this.pendingDeliveries.shift();
123
+ this.clockMs = Math.max(this.clockMs, delivery.timeMs);
124
+ delivery.deliver();
125
+ }
126
+ }
127
+ }
128
+ LoopbackHub.SESSION_UPDATE_INTERVAL_MS = 250;
129
+ LoopbackHub.CLIENT_KEY_PREFIX = "loopback-";
130
+ LoopbackHub.ROOM_LINK_NAME = "room";
131
+ LoopbackHub.LINK_SEPARATOR = ">";
132
+ export class LoopbackTransport {
133
+ constructor(hub, clientKey) {
134
+ this.handlers = null;
135
+ this.connected = false;
136
+ this.transportName = "Loopback";
137
+ this.hub = hub;
138
+ this.clientKey = clientKey;
139
+ }
140
+ get isConnected() {
141
+ return this.connected;
142
+ }
143
+ get conditions() {
144
+ return this.hub.conditions;
145
+ }
146
+ connect(request) {
147
+ try {
148
+ const welcome = this.hub.joinClient(this.clientKey, request);
149
+ this.connected = true;
150
+ return Promise.resolve(welcome);
151
+ }
152
+ catch (error) {
153
+ return Promise.reject(error);
154
+ }
155
+ }
156
+ sendFrame(route, targetPlayerId, frame, channel) {
157
+ if (!this.connected)
158
+ return;
159
+ this.hub.sendFromClient(this.clientKey, route, targetPlayerId, frame, channel);
160
+ }
161
+ sendControl(name, payload) {
162
+ if (!this.connected)
163
+ return;
164
+ this.hub.sendControlFromClient(this.clientKey, name, payload);
165
+ }
166
+ setHandlers(handlers) {
167
+ this.handlers = handlers;
168
+ }
169
+ disconnect() {
170
+ if (this.connected)
171
+ this.hub.leaveClient(this.clientKey);
172
+ this.connected = false;
173
+ return Promise.resolve();
174
+ }
175
+ simulateDrop() {
176
+ if (!this.connected)
177
+ return;
178
+ this.connected = false;
179
+ this.hub.dropClient(this.clientKey);
180
+ if (this.handlers != null)
181
+ this.handlers.onDisconnected(NetDisconnectReason.Timeout);
182
+ }
183
+ deliverFrame(sourcePlayerId, frame) {
184
+ if (this.connected && this.handlers != null)
185
+ this.handlers.onFrame(sourcePlayerId, frame);
186
+ }
187
+ deliverControl(name, payload) {
188
+ if (name === FUSION_CONTROL_WELCOME)
189
+ return;
190
+ if (this.connected && this.handlers != null)
191
+ this.handlers.onControl(name, payload);
192
+ }
193
+ }
194
+ export class FusionSessionLogic {
195
+ constructor(outputs) {
196
+ this.clients = new Map();
197
+ this.sharedObjects = new Map();
198
+ this.sessionMode = null;
199
+ this.session = null;
200
+ this.sessionTickRate = NETWORK_DEFAULT_TICK_RATE;
201
+ this.sessionPrefabTableHash = 0;
202
+ this.lastPlayerId = 0;
203
+ this.currentHostPlayerId = 0;
204
+ this.currentMasterClientPlayerId = 0;
205
+ this.dedicatedEndpoint = null;
206
+ this.latestHostSnapshot = null;
207
+ this.latestSceneInfoFrame = null;
208
+ this.latestSceneInfo = null;
209
+ this.migration = null;
210
+ this.migrationHeldFrames = [];
211
+ this.outputs = outputs;
212
+ }
213
+ get gameMode() {
214
+ return this.sessionMode;
215
+ }
216
+ get players() {
217
+ const players = [];
218
+ this.clients.forEach((record) => {
219
+ players.push({ clientKey: record.clientKey, playerId: record.playerId, connected: record.connected, isHost: record.isHost });
220
+ });
221
+ return players;
222
+ }
223
+ get hostPlayerId() {
224
+ return this.currentHostPlayerId;
225
+ }
226
+ get masterClientPlayerId() {
227
+ return this.currentMasterClientPlayerId;
228
+ }
229
+ get hostSnapshot() {
230
+ return this.latestHostSnapshot;
231
+ }
232
+ get isMigratingHost() {
233
+ return this.migration != null;
234
+ }
235
+ join(clientKey, request) {
236
+ if (this.sessionMode == null)
237
+ this.createSession(request);
238
+ else
239
+ this.checkCompatible(request);
240
+ const migrationRecord = (request.hostMigration === true) ? this.findMigrationRejoin(request.reconnectPlayerId) : null;
241
+ const resumesMigration = migrationRecord != null;
242
+ const heldRecord = resumesMigration ? migrationRecord : this.findHeldPlayer(request.reconnectPlayerId);
243
+ if (heldRecord == null)
244
+ this.checkCapacity();
245
+ const record = (heldRecord != null) ? this.resumeClient(heldRecord, clientKey) : this.addClient(clientKey);
246
+ record.authIdentity = request.authIdentity ?? null;
247
+ const needsApproval = heldRecord == null && this.needsApproval(record);
248
+ record.admitted = !needsApproval;
249
+ const previousMasterClient = this.currentMasterClientPlayerId;
250
+ this.currentMasterClientPlayerId = this.computeMasterClient();
251
+ const welcome = this.buildWelcome(record);
252
+ welcome.isPendingApproval = needsApproval;
253
+ this.outputs.sendControl(clientKey, FUSION_CONTROL_WELCOME, welcome);
254
+ if (needsApproval)
255
+ this.requestApproval(record, request.connectionTokenBase64);
256
+ else if (heldRecord == null || resumesMigration)
257
+ this.announceJoin(record);
258
+ if (resumesMigration)
259
+ record.awaitingMigrationRejoin = false;
260
+ if (resumesMigration && record.isHost && this.migration != null)
261
+ this.completeHostMigration(record);
262
+ if (this.currentMasterClientPlayerId !== previousMasterClient)
263
+ this.broadcastMasterClient(clientKey);
264
+ if (record.admitted)
265
+ this.replaySharedState(record);
266
+ this.publishSession();
267
+ return welcome;
268
+ }
269
+ receiveFrame(clientKey, payload, channel = RpcChannel.Reliable) {
270
+ const record = this.clients.get(clientKey);
271
+ if (record == null || !record.connected)
272
+ return;
273
+ if (!record.admitted) {
274
+ record.heldPayloads.push({ payload: payload.slice(), channel: channel });
275
+ return;
276
+ }
277
+ const envelope = NetworkProtocol.decodeEnvelope(payload);
278
+ this.routeFrame(record, envelope, channel);
279
+ }
280
+ receiveControl(clientKey, name, payload) {
281
+ const record = this.clients.get(clientKey);
282
+ if (record == null || !record.connected)
283
+ return;
284
+ if (name === FUSION_CONTROL_CONNECT_RESULT && record.isHost) {
285
+ this.resolveConnectRequest(payload);
286
+ return;
287
+ }
288
+ if (name === FUSION_CONTROL_SESSION && this.maySetSession(record))
289
+ this.updateSession(payload);
290
+ if (name === FUSION_CONTROL_AREA && this.sessionMode === GameMode.Shared)
291
+ this.updatePlayerArea(record, payload);
292
+ }
293
+ update(nowMilliseconds) {
294
+ const expiredKeys = [];
295
+ this.clients.forEach((record) => {
296
+ if (!record.connected && record.dropDeadlineMs <= nowMilliseconds)
297
+ expiredKeys.push(record.clientKey);
298
+ });
299
+ for (const clientKey of expiredKeys)
300
+ this.leave(clientKey);
301
+ }
302
+ sendFromServer(route, targetPlayerId, frame, channel = RpcChannel.Reliable) {
303
+ if (route === NetworkRoute.ToPlayer) {
304
+ const target = this.findAdmittedPlayer(targetPlayerId);
305
+ if (target != null)
306
+ this.outputs.sendFrame(target.clientKey, FusionSessionLogic.SESSION_PLAYER_ID, frame, channel);
307
+ return;
308
+ }
309
+ this.forEachAdmitted(null, (record) => this.outputs.sendFrame(record.clientKey, FusionSessionLogic.SESSION_PLAYER_ID, frame, channel));
310
+ }
311
+ sendControlFromServer(name, payload, targetPlayerId) {
312
+ if (name === FUSION_CONTROL_CONNECT_RESULT) {
313
+ this.resolveConnectRequest(payload);
314
+ return;
315
+ }
316
+ if (targetPlayerId !== FusionSessionLogic.SESSION_PLAYER_ID) {
317
+ const target = this.findAdmittedPlayer(targetPlayerId);
318
+ if (target != null)
319
+ this.outputs.sendControl(target.clientKey, name, payload);
320
+ return;
321
+ }
322
+ this.forEachAdmitted(null, (record) => this.outputs.sendControl(record.clientKey, name, payload));
323
+ }
324
+ drop(clientKey) {
325
+ const record = this.clients.get(clientKey);
326
+ if (record == null || !record.connected)
327
+ return;
328
+ this.holdForReconnect(record);
329
+ if (record.isHost && this.migration == null && this.hasOtherConnectedPlayer(record)) {
330
+ this.startHostMigration(record);
331
+ return;
332
+ }
333
+ this.refreshMasterClient();
334
+ this.publishSession();
335
+ }
336
+ leave(clientKey) {
337
+ const record = this.clients.get(clientKey);
338
+ if (record == null)
339
+ return;
340
+ if (this.migration != null && record.connected && record.admitted) {
341
+ this.holdForReconnect(record);
342
+ this.publishSession();
343
+ return;
344
+ }
345
+ this.clients.delete(clientKey);
346
+ if (record.isHost) {
347
+ if (this.hasOtherConnectedPlayer(record))
348
+ this.startHostMigration(record);
349
+ else
350
+ this.endSession(ShutdownReason.DisconnectedByPluginLogic);
351
+ return;
352
+ }
353
+ if (record.admitted) {
354
+ this.broadcastControl(FUSION_CONTROL_PLAYER_LEFT, { playerId: record.playerId }, null);
355
+ this.refreshMasterClient();
356
+ this.applySharedLeaveRules(record.playerId);
357
+ this.sharedObjects.forEach((object) => object.receivers.delete(record.playerId));
358
+ }
359
+ if (this.clients.size === 0 && this.dedicatedEndpoint == null) {
360
+ this.resetSession();
361
+ return;
362
+ }
363
+ this.publishSession();
364
+ }
365
+ holdForReconnect(record) {
366
+ record.connected = false;
367
+ record.dropDeadlineMs = this.outputs.nowMilliseconds() + NETWORK_RECONNECT_GRACE_SECONDS * FusionSessionLogic.MILLISECONDS_PER_SECOND;
368
+ }
369
+ startHostMigration(oldHost) {
370
+ this.clients.delete(oldHost.clientKey);
371
+ const elected = this.findLowestConnectedPlayer();
372
+ if (elected == null) {
373
+ this.endSession(ShutdownReason.DisconnectedByPluginLogic);
374
+ return;
375
+ }
376
+ elected.isHost = true;
377
+ this.currentHostPlayerId = elected.playerId;
378
+ this.migration = { newHostPlayerId: elected.playerId, previousHostPlayerId: oldHost.playerId };
379
+ this.migrationHeldFrames.length = 0;
380
+ this.forEachAdmitted(null, (record) => {
381
+ record.awaitingMigrationRejoin = true;
382
+ });
383
+ const snapshot = (this.latestHostSnapshot != null) ? this.latestHostSnapshot : new Uint8Array(0);
384
+ const payload = { newHostPlayerId: elected.playerId, snapshotBase64: NetworkProtocol.bytesToBase64(snapshot), previousHostPlayerId: oldHost.playerId };
385
+ this.forEachAdmitted(null, (record) => this.outputs.sendControl(record.clientKey, FUSION_CONTROL_HOST_MIGRATION, payload));
386
+ this.publishSession();
387
+ }
388
+ completeHostMigration(host) {
389
+ this.migration = null;
390
+ const heldFrames = this.migrationHeldFrames.splice(0, this.migrationHeldFrames.length);
391
+ for (const held of heldFrames)
392
+ this.outputs.sendFrame(host.clientKey, held.sourcePlayerId, held.frame);
393
+ }
394
+ findMigrationRejoin(playerId) {
395
+ const record = this.findPlayer(playerId);
396
+ return (record != null && record.awaitingMigrationRejoin) ? record : null;
397
+ }
398
+ findLowestConnectedPlayer() {
399
+ let lowest = null;
400
+ this.forEachAdmitted(null, (record) => {
401
+ if (lowest == null || record.playerId < lowest.playerId)
402
+ lowest = record;
403
+ });
404
+ return lowest;
405
+ }
406
+ attachDedicatedServer(serverEndpoint, request = null) {
407
+ this.dedicatedEndpoint = serverEndpoint;
408
+ this.sessionMode = GameMode.Server;
409
+ this.currentHostPlayerId = FusionSessionLogic.SESSION_PLAYER_ID;
410
+ if (request != null) {
411
+ const playerCount = (request.playerCount > 0) ? request.playerCount : FusionSessionLogic.DEFAULT_PLAYER_COUNT;
412
+ this.session = this.createSessionInfo(request.sessionName, playerCount, request.isOpen !== false, request.isVisible !== false, request.sessionProperties || {});
413
+ this.sessionTickRate = (request.tickRate > 0) ? request.tickRate : NETWORK_DEFAULT_TICK_RATE;
414
+ this.sessionPrefabTableHash = request.prefabTableHash;
415
+ }
416
+ else if (this.session == null) {
417
+ this.session = this.createSessionInfo("", FusionSessionLogic.DEFAULT_PLAYER_COUNT, true, true, {});
418
+ }
419
+ this.publishSession();
420
+ return {
421
+ playerId: FusionSessionLogic.SESSION_PLAYER_ID,
422
+ gameMode: GameMode.Server,
423
+ hostPlayerId: FusionSessionLogic.SESSION_PLAYER_ID,
424
+ masterClientPlayerId: 0,
425
+ serverTimeMs: this.outputs.nowMilliseconds(),
426
+ session: this.sessionSnapshot(),
427
+ tickRate: this.sessionTickRate,
428
+ prefabTableHash: this.sessionPrefabTableHash,
429
+ isDedicated: true,
430
+ playerIds: []
431
+ };
432
+ }
433
+ createSession(request) {
434
+ const requestedMode = request.gameMode;
435
+ if (requestedMode === GameMode.Client)
436
+ throw FusionSessionLogic.error(ShutdownReason.GameNotFound);
437
+ const isHostSession = requestedMode === GameMode.Host || requestedMode === GameMode.AutoHostOrClient;
438
+ if (!isHostSession && requestedMode !== GameMode.Shared)
439
+ throw FusionSessionLogic.error(ShutdownReason.InvalidArguments);
440
+ this.sessionMode = isHostSession ? GameMode.Host : GameMode.Shared;
441
+ const playerCount = (request.playerCount > 0) ? request.playerCount : FusionSessionLogic.DEFAULT_PLAYER_COUNT;
442
+ this.session = this.createSessionInfo(request.sessionName, playerCount, request.isOpen !== false, request.isVisible !== false, request.sessionProperties || {});
443
+ this.sessionTickRate = (request.tickRate > 0) ? request.tickRate : NETWORK_DEFAULT_TICK_RATE;
444
+ this.sessionPrefabTableHash = request.prefabTableHash;
445
+ }
446
+ createSessionInfo(name, maxPlayers, isOpen, isVisible, properties) {
447
+ return { name: name, isValid: true, isOpen: isOpen, isVisible: isVisible, playerCount: 0, maxPlayers: maxPlayers, properties: Object.assign({}, properties), region: FusionSessionLogic.REGION };
448
+ }
449
+ checkCompatible(request) {
450
+ const requestedMode = request.gameMode;
451
+ const isHostedSession = this.sessionMode === GameMode.Host || this.sessionMode === GameMode.Server;
452
+ const joinsAsClient = requestedMode === GameMode.Client || requestedMode === GameMode.AutoHostOrClient;
453
+ const resumesAsHost = this.migration != null && request.hostMigration === true && requestedMode === GameMode.Host && request.reconnectPlayerId === this.migration.newHostPlayerId;
454
+ const modeFits = isHostedSession ? (joinsAsClient || resumesAsHost) : requestedMode === GameMode.Shared;
455
+ if (!modeFits)
456
+ throw FusionSessionLogic.error(ShutdownReason.IncompatibleConfiguration);
457
+ if (request.prefabTableHash !== this.sessionPrefabTableHash)
458
+ throw FusionSessionLogic.error(ShutdownReason.IncompatibleConfiguration);
459
+ }
460
+ checkCapacity() {
461
+ if (!this.session.isOpen)
462
+ throw FusionSessionLogic.error(ShutdownReason.GameClosed);
463
+ if (this.clients.size >= this.session.maxPlayers)
464
+ throw FusionSessionLogic.error(ShutdownReason.GameIsFull);
465
+ }
466
+ addClient(clientKey) {
467
+ this.lastPlayerId += 1;
468
+ const isHost = this.sessionMode === GameMode.Host && this.currentHostPlayerId === 0;
469
+ if (isHost)
470
+ this.currentHostPlayerId = this.lastPlayerId;
471
+ const record = { clientKey: clientKey, playerId: this.lastPlayerId, connected: true, isHost: isHost, admitted: false, heldPayloads: [], dropDeadlineMs: 0, area: null, awaitingMigrationRejoin: false, authIdentity: null };
472
+ this.clients.set(clientKey, record);
473
+ return record;
474
+ }
475
+ findHeldPlayer(playerId) {
476
+ if (playerId == null || playerId <= 0)
477
+ return null;
478
+ for (const record of this.clients.values()) {
479
+ if (record.playerId === playerId && !record.connected)
480
+ return record;
481
+ }
482
+ return null;
483
+ }
484
+ resumeClient(record, clientKey) {
485
+ this.clients.delete(record.clientKey);
486
+ record.clientKey = clientKey;
487
+ record.connected = true;
488
+ record.dropDeadlineMs = 0;
489
+ this.clients.set(clientKey, record);
490
+ return record;
491
+ }
492
+ needsApproval(record) {
493
+ const isHostedSession = this.sessionMode === GameMode.Host || this.sessionMode === GameMode.Server;
494
+ return isHostedSession && !record.isHost;
495
+ }
496
+ buildWelcome(record) {
497
+ const playerIds = [];
498
+ this.clients.forEach((client) => {
499
+ if (client.connected && (client.admitted || client === record))
500
+ playerIds.push(client.playerId);
501
+ });
502
+ playerIds.sort((left, right) => left - right);
503
+ return {
504
+ playerId: record.playerId,
505
+ gameMode: this.welcomeGameMode(record),
506
+ hostPlayerId: this.currentHostPlayerId,
507
+ masterClientPlayerId: this.currentMasterClientPlayerId,
508
+ serverTimeMs: this.outputs.nowMilliseconds(),
509
+ session: this.sessionSnapshot(),
510
+ tickRate: this.sessionTickRate,
511
+ prefabTableHash: this.sessionPrefabTableHash,
512
+ isDedicated: this.dedicatedEndpoint != null,
513
+ playerIds: playerIds
514
+ };
515
+ }
516
+ welcomeGameMode(record) {
517
+ if (record.isHost)
518
+ return GameMode.Host;
519
+ return (this.sessionMode === GameMode.Shared) ? GameMode.Shared : GameMode.Client;
520
+ }
521
+ requestApproval(record, tokenBase64) {
522
+ const payload = { playerId: record.playerId, tokenBase64: tokenBase64 || "", authIdentity: record.authIdentity };
523
+ if (this.dedicatedEndpoint != null) {
524
+ this.dedicatedEndpoint.receiveControl(FUSION_CONTROL_CONNECT_REQUEST, payload);
525
+ return;
526
+ }
527
+ const host = this.findPlayer(this.currentHostPlayerId);
528
+ if (host != null && host.connected)
529
+ this.outputs.sendControl(host.clientKey, FUSION_CONTROL_CONNECT_REQUEST, payload);
530
+ }
531
+ resolveConnectRequest(result) {
532
+ const record = this.findPlayer(result.playerId);
533
+ if (record == null || record.admitted)
534
+ return;
535
+ if (!result.accepted) {
536
+ this.outputs.sendControl(record.clientKey, FUSION_CONTROL_SHUTDOWN, { reason: ShutdownReason.ConnectionRefused });
537
+ this.leave(record.clientKey);
538
+ return;
539
+ }
540
+ record.admitted = true;
541
+ this.outputs.sendControl(record.clientKey, FUSION_CONTROL_CONNECT_RESULT, { playerId: record.playerId, accepted: true });
542
+ this.announceJoin(record);
543
+ const heldPayloads = record.heldPayloads.splice(0, record.heldPayloads.length);
544
+ for (const held of heldPayloads)
545
+ this.receiveFrame(record.clientKey, held.payload, held.channel);
546
+ this.publishSession();
547
+ }
548
+ announceJoin(record) {
549
+ this.broadcastControl(FUSION_CONTROL_PLAYER_JOINED, { playerId: record.playerId }, record.clientKey);
550
+ }
551
+ routeFrame(sender, envelope, channel = RpcChannel.Reliable) {
552
+ switch (envelope.route) {
553
+ case NetworkRoute.ToHost:
554
+ this.sendToHost(sender, envelope.frame, channel);
555
+ return;
556
+ case NetworkRoute.ToPlayer:
557
+ this.sendToPlayer(sender, envelope.targetPlayerId, envelope.frame, channel);
558
+ return;
559
+ case NetworkRoute.ToAllClients:
560
+ this.forEachAdmitted(null, (record) => {
561
+ if (!record.isHost)
562
+ this.outputs.sendFrame(record.clientKey, sender.playerId, envelope.frame, channel);
563
+ });
564
+ return;
565
+ case NetworkRoute.ToAllOthers:
566
+ this.sendToAllOthers(sender.playerId, sender.clientKey, envelope.frame, channel);
567
+ return;
568
+ case NetworkRoute.ToRoomAuthority:
569
+ this.handleRoomFrame(sender, envelope.frame);
570
+ return;
571
+ default:
572
+ return;
573
+ }
574
+ }
575
+ sendToHost(sender, frame, channel = RpcChannel.Reliable) {
576
+ if (this.dedicatedEndpoint != null) {
577
+ this.dedicatedEndpoint.receiveFrame(sender.playerId, frame);
578
+ return;
579
+ }
580
+ const host = this.findAdmittedPlayer(this.currentHostPlayerId);
581
+ if (host != null) {
582
+ this.outputs.sendFrame(host.clientKey, sender.playerId, frame, channel);
583
+ return;
584
+ }
585
+ if (this.migration != null)
586
+ this.holdForNewHost(sender.playerId, frame);
587
+ }
588
+ holdForNewHost(sourcePlayerId, frame) {
589
+ if (NetworkProtocol.frameKind(frame) === NetworkFrameKind.Ping)
590
+ return;
591
+ this.migrationHeldFrames.push({ sourcePlayerId: sourcePlayerId, frame: frame.slice() });
592
+ if (this.migrationHeldFrames.length > FusionSessionLogic.MAX_MIGRATION_HELD_FRAMES)
593
+ this.migrationHeldFrames.shift();
594
+ }
595
+ sendToPlayer(sender, targetPlayerId, frame, channel = RpcChannel.Reliable) {
596
+ if (targetPlayerId === FusionSessionLogic.SESSION_PLAYER_ID && this.dedicatedEndpoint != null) {
597
+ this.dedicatedEndpoint.receiveFrame(sender.playerId, frame);
598
+ return;
599
+ }
600
+ const target = this.findAdmittedPlayer(targetPlayerId);
601
+ if (target != null)
602
+ this.outputs.sendFrame(target.clientKey, sender.playerId, frame, channel);
603
+ }
604
+ sendToAllOthers(sourcePlayerId, exceptKey, frame, channel = RpcChannel.Reliable) {
605
+ this.forEachAdmitted(exceptKey, (record) => this.outputs.sendFrame(record.clientKey, sourcePlayerId, frame, channel));
606
+ if (this.dedicatedEndpoint != null)
607
+ this.dedicatedEndpoint.receiveFrame(sourcePlayerId, frame);
608
+ }
609
+ handleRoomFrame(sender, frame) {
610
+ switch (NetworkProtocol.frameKind(frame)) {
611
+ case NetworkFrameKind.Ping:
612
+ this.answerPing(sender, frame);
613
+ return;
614
+ case NetworkFrameKind.SharedState:
615
+ if (this.sessionMode === GameMode.Shared)
616
+ this.acceptSharedState(sender, frame);
617
+ return;
618
+ case NetworkFrameKind.AuthorityRequest:
619
+ if (this.sessionMode === GameMode.Shared)
620
+ this.handleAuthorityRequest(sender, frame);
621
+ return;
622
+ case NetworkFrameKind.Despawn:
623
+ if (this.sessionMode === GameMode.Shared)
624
+ this.handleSharedDespawn(sender, frame);
625
+ return;
626
+ case NetworkFrameKind.HostSnapshot:
627
+ if (sender.isHost)
628
+ this.latestHostSnapshot = frame.slice();
629
+ return;
630
+ case NetworkFrameKind.SceneInfo:
631
+ if (this.sessionMode === GameMode.Shared && sender.playerId === this.currentMasterClientPlayerId)
632
+ this.acceptSceneInfo(sender, frame);
633
+ return;
634
+ default:
635
+ return;
636
+ }
637
+ }
638
+ answerPing(sender, frame) {
639
+ const clientTimeMs = NetworkProtocol.decodePing(frame);
640
+ const pong = NetworkProtocol.encodePong({ clientTimeMs: clientTimeMs, serverTimeMs: this.outputs.nowMilliseconds(), serverTick: 0 });
641
+ this.outputs.sendFrame(sender.clientKey, FusionSessionLogic.SESSION_PLAYER_ID, pong);
642
+ }
643
+ acceptSharedState(sender, frame) {
644
+ const decoded = NetworkProtocol.decodeSharedState(frame);
645
+ const acceptedBlocks = decoded.blocks.filter((block) => this.storeSharedBlock(sender.playerId, block, decoded));
646
+ if (acceptedBlocks.length === 0)
647
+ return;
648
+ const hasInterestBlocks = acceptedBlocks.some((block) => this.sharedObjects.get(block.header.networkId).position != null);
649
+ if (hasInterestBlocks) {
650
+ this.forEachAdmitted(sender.clientKey, (receiver) => this.forwardSharedBlocks(sender, receiver, acceptedBlocks, decoded));
651
+ return;
652
+ }
653
+ const forwarded = (acceptedBlocks.length === decoded.blocks.length) ? frame : NetworkProtocol.encodeSharedState({ senderTick: decoded.senderTick, senderServerTimeMs: decoded.senderServerTimeMs, sceneLoadId: decoded.sceneLoadId, blocks: acceptedBlocks });
654
+ this.sendToAllOthers(sender.playerId, sender.clientKey, forwarded);
655
+ }
656
+ forwardSharedBlocks(sender, receiver, blocks, decoded) {
657
+ const receiverBlocks = [];
658
+ for (const block of blocks) {
659
+ const object = this.sharedObjects.get(block.header.networkId);
660
+ if (object.position == null) {
661
+ receiverBlocks.push(block);
662
+ continue;
663
+ }
664
+ const forwardedBlock = this.filterInterestBlock(receiver, block.header.networkId, object, block);
665
+ if (forwardedBlock != null)
666
+ receiverBlocks.push(forwardedBlock);
667
+ }
668
+ if (receiverBlocks.length === 0)
669
+ return;
670
+ const frame = NetworkProtocol.encodeSharedState({ senderTick: decoded.senderTick, senderServerTimeMs: decoded.senderServerTimeMs, sceneLoadId: decoded.sceneLoadId, blocks: receiverBlocks });
671
+ this.outputs.sendFrame(receiver.clientKey, sender.playerId, frame);
672
+ }
673
+ filterInterestBlock(receiver, objectId, object, block) {
674
+ const relevant = FusionSessionLogic.areaHolds(receiver.area, object.position);
675
+ const hasObject = object.receivers.has(receiver.playerId);
676
+ if (relevant && hasObject)
677
+ return block;
678
+ if (relevant) {
679
+ object.receivers.add(receiver.playerId);
680
+ return FusionSessionLogic.storedSpawnBlock(object);
681
+ }
682
+ if (hasObject)
683
+ this.sendOutOfInterest(receiver, objectId, object);
684
+ return null;
685
+ }
686
+ updatePlayerArea(record, area) {
687
+ record.area = area;
688
+ const enteringBlocks = [];
689
+ let latestTick = 0;
690
+ let latestTimeMs = 0;
691
+ this.sharedObjects.forEach((object, objectId) => {
692
+ if (object.position == null || object.authorityPlayerId === record.playerId)
693
+ return;
694
+ const relevant = FusionSessionLogic.areaHolds(area, object.position);
695
+ const hasObject = object.receivers.has(record.playerId);
696
+ if (relevant && !hasObject) {
697
+ object.receivers.add(record.playerId);
698
+ enteringBlocks.push(FusionSessionLogic.storedSpawnBlock(object));
699
+ latestTick = Math.max(latestTick, object.senderTick);
700
+ latestTimeMs = Math.max(latestTimeMs, object.senderServerTimeMs);
701
+ }
702
+ else if (!relevant && hasObject) {
703
+ this.sendOutOfInterest(record, objectId, object);
704
+ }
705
+ });
706
+ if (enteringBlocks.length === 0)
707
+ return;
708
+ const frame = NetworkProtocol.encodeSharedState({ senderTick: latestTick, senderServerTimeMs: latestTimeMs, sceneLoadId: this.currentSceneLoadId(), blocks: enteringBlocks });
709
+ this.outputs.sendFrame(record.clientKey, FusionSessionLogic.SESSION_PLAYER_ID, frame);
710
+ }
711
+ sendOutOfInterest(receiver, objectId, object) {
712
+ object.receivers.delete(receiver.playerId);
713
+ const despawn = NetworkProtocol.encodeDespawn({ objectId: objectId, reason: NetworkDespawnReason.OutOfInterest });
714
+ this.outputs.sendFrame(receiver.clientKey, FusionSessionLogic.SESSION_PLAYER_ID, despawn);
715
+ }
716
+ currentSceneLoadId() {
717
+ return (this.latestSceneInfo != null) ? this.latestSceneInfo.loadId : NETWORK_START_SCENE_LOAD_ID;
718
+ }
719
+ storeSharedBlock(senderPlayerId, block, frame) {
720
+ const header = block.header;
721
+ const existing = this.sharedObjects.get(header.networkId);
722
+ if (existing == null)
723
+ return this.storeSharedSpawn(senderPlayerId, block, frame);
724
+ if (existing.authorityPlayerId !== senderPlayerId)
725
+ return false;
726
+ existing.deltaBytes = block.deltaBytes;
727
+ existing.senderTick = frame.senderTick;
728
+ existing.senderServerTimeMs = frame.senderServerTimeMs;
729
+ if (header.position != null)
730
+ existing.position = header.position.slice();
731
+ const changesInputAuthority = (header.flags & (NetworkObjectBlockFlags.Spawn | NetworkObjectBlockFlags.InputAuthorityChanged)) !== 0;
732
+ if (changesInputAuthority)
733
+ existing.spawnHeader.inputAuthority = header.inputAuthority;
734
+ return true;
735
+ }
736
+ storeSharedSpawn(senderPlayerId, block, frame) {
737
+ const header = block.header;
738
+ if ((header.flags & NetworkObjectBlockFlags.Spawn) === 0)
739
+ return false;
740
+ if (!this.mayCreateObject(senderPlayerId, header.networkId))
741
+ return false;
742
+ const spawnHeader = Object.assign({}, header, { flags: NetworkObjectBlockFlags.Spawn | NetworkObjectBlockFlags.Full, position: undefined });
743
+ const authorityPlayerId = (header.stateAuthority > 0) ? header.stateAuthority : senderPlayerId;
744
+ const position = (header.position != null) ? header.position.slice() : null;
745
+ this.sharedObjects.set(header.networkId, { spawnHeader: spawnHeader, deltaBytes: block.deltaBytes, authorityPlayerId: authorityPlayerId, senderTick: frame.senderTick, senderServerTimeMs: frame.senderServerTimeMs, position: position, receivers: new Set() });
746
+ return true;
747
+ }
748
+ mayCreateObject(playerId, networkId) {
749
+ if (NetworkIdUtility.isSceneObject(networkId))
750
+ return playerId === this.currentMasterClientPlayerId;
751
+ const rangeStart = NetworkIdUtility.forSharedSpawn(playerId, 0);
752
+ return networkId >= rangeStart && networkId < rangeStart + NETWORK_SHARED_ID_PLAYER_STRIDE;
753
+ }
754
+ handleAuthorityRequest(sender, frame) {
755
+ const request = NetworkProtocol.decodeAuthorityRequest(frame);
756
+ const object = this.sharedObjects.get(request.objectId);
757
+ if (object == null)
758
+ return;
759
+ if (request.release) {
760
+ if (object.authorityPlayerId === sender.playerId)
761
+ this.setSharedAuthority(request.objectId, object, 0);
762
+ return;
763
+ }
764
+ if (this.mayTakeAuthority(object, sender.playerId))
765
+ this.setSharedAuthority(request.objectId, object, sender.playerId);
766
+ }
767
+ mayTakeAuthority(object, playerId) {
768
+ if (object.authorityPlayerId === 0 || object.authorityPlayerId === playerId)
769
+ return true;
770
+ if (this.findPlayer(object.authorityPlayerId) == null)
771
+ return true;
772
+ return (object.spawnHeader.options & NetworkObjectOptions.AllowStateAuthorityOverride) !== 0;
773
+ }
774
+ setSharedAuthority(objectId, object, playerId) {
775
+ object.authorityPlayerId = playerId;
776
+ const result = NetworkProtocol.encodeAuthorityResult({ objectId: objectId, playerId: playerId });
777
+ this.sendToAllOthers(FusionSessionLogic.SESSION_PLAYER_ID, null, result);
778
+ }
779
+ handleSharedDespawn(sender, frame) {
780
+ const despawn = NetworkProtocol.decodeDespawn(frame);
781
+ const object = this.sharedObjects.get(despawn.objectId);
782
+ if (object == null || object.authorityPlayerId !== sender.playerId)
783
+ return;
784
+ this.sharedObjects.delete(despawn.objectId);
785
+ this.sendToAllOthers(sender.playerId, sender.clientKey, frame);
786
+ }
787
+ applySharedLeaveRules(playerId) {
788
+ const heldIds = [];
789
+ this.sharedObjects.forEach((object, objectId) => {
790
+ if (object.authorityPlayerId === playerId)
791
+ heldIds.push(objectId);
792
+ });
793
+ for (const objectId of heldIds) {
794
+ const object = this.sharedObjects.get(objectId);
795
+ const options = object.spawnHeader.options;
796
+ if ((options & NetworkObjectOptions.DestroyWhenStateAuthorityLeaves) !== 0) {
797
+ this.sharedObjects.delete(objectId);
798
+ const despawn = NetworkProtocol.encodeDespawn({ objectId: objectId, reason: NetworkDespawnReason.AuthorityLeft });
799
+ this.sendToAllOthers(FusionSessionLogic.SESSION_PLAYER_ID, null, despawn);
800
+ }
801
+ else if ((options & NetworkObjectOptions.MasterClientObject) !== 0) {
802
+ this.setSharedAuthority(objectId, object, this.currentMasterClientPlayerId);
803
+ }
804
+ else {
805
+ this.setSharedAuthority(objectId, object, 0);
806
+ }
807
+ }
808
+ }
809
+ acceptSceneInfo(sender, frame) {
810
+ const sceneInfo = NetworkProtocol.decodeSceneInfo(frame);
811
+ if (this.latestSceneInfo != null && sceneInfo.loadId <= this.latestSceneInfo.loadId)
812
+ return;
813
+ this.dropReplacedSceneObjects(this.latestSceneInfo, sceneInfo);
814
+ this.latestSceneInfo = sceneInfo;
815
+ this.latestSceneInfoFrame = frame.slice();
816
+ this.sendToAllOthers(sender.playerId, sender.clientKey, frame);
817
+ }
818
+ dropReplacedSceneObjects(previous, next) {
819
+ const nextBySlot = new Map();
820
+ for (const scene of next.scenes)
821
+ nextBySlot.set(scene.slot, FusionSessionLogic.sceneKey(scene));
822
+ const previousScenes = (previous != null) ? previous.scenes : next.scenes.filter((scene) => scene.loadId === NETWORK_START_SCENE_LOAD_ID);
823
+ const previousBySlot = new Map();
824
+ for (const scene of previousScenes)
825
+ previousBySlot.set(scene.slot, FusionSessionLogic.sceneKey(scene));
826
+ const keepsUnnamedStartScene = previous == null && !nextBySlot.has(0);
827
+ for (const objectId of Array.from(this.sharedObjects.keys())) {
828
+ if (!NetworkIdUtility.isSceneObject(objectId))
829
+ continue;
830
+ const slot = NetworkIdUtility.getSceneSlot(objectId);
831
+ if (slot === 0 && keepsUnnamedStartScene)
832
+ continue;
833
+ if (!previousBySlot.has(slot) || previousBySlot.get(slot) !== nextBySlot.get(slot))
834
+ this.sharedObjects.delete(objectId);
835
+ }
836
+ }
837
+ replaySharedState(record) {
838
+ if (this.sessionMode !== GameMode.Shared)
839
+ return;
840
+ if (this.latestSceneInfoFrame != null)
841
+ this.outputs.sendFrame(record.clientKey, this.currentMasterClientPlayerId, this.latestSceneInfoFrame);
842
+ if (this.sharedObjects.size === 0)
843
+ return;
844
+ const blocks = [];
845
+ let latestTick = 0;
846
+ let latestTimeMs = 0;
847
+ this.sharedObjects.forEach((object) => {
848
+ if (object.position != null && object.authorityPlayerId !== record.playerId) {
849
+ if (!FusionSessionLogic.areaHolds(record.area, object.position))
850
+ return;
851
+ object.receivers.add(record.playerId);
852
+ }
853
+ blocks.push(FusionSessionLogic.storedSpawnBlock(object));
854
+ latestTick = Math.max(latestTick, object.senderTick);
855
+ latestTimeMs = Math.max(latestTimeMs, object.senderServerTimeMs);
856
+ });
857
+ if (blocks.length === 0)
858
+ return;
859
+ const sceneLoadId = this.currentSceneLoadId();
860
+ const frame = NetworkProtocol.encodeSharedState({ senderTick: latestTick, senderServerTimeMs: latestTimeMs, sceneLoadId: sceneLoadId, blocks: blocks });
861
+ this.outputs.sendFrame(record.clientKey, FusionSessionLogic.SESSION_PLAYER_ID, frame);
862
+ }
863
+ computeMasterClient() {
864
+ if (this.sessionMode !== GameMode.Shared)
865
+ return 0;
866
+ let masterClient = 0;
867
+ this.forEachAdmitted(null, (record) => {
868
+ if (masterClient === 0 || record.playerId < masterClient)
869
+ masterClient = record.playerId;
870
+ });
871
+ return masterClient;
872
+ }
873
+ refreshMasterClient() {
874
+ const masterClient = this.computeMasterClient();
875
+ if (masterClient === this.currentMasterClientPlayerId)
876
+ return;
877
+ this.currentMasterClientPlayerId = masterClient;
878
+ this.broadcastMasterClient(null);
879
+ }
880
+ broadcastMasterClient(exceptKey) {
881
+ this.broadcastControl(FUSION_CONTROL_MASTER_CLIENT, { playerId: this.currentMasterClientPlayerId }, exceptKey);
882
+ }
883
+ maySetSession(record) {
884
+ return record.isHost || (this.sessionMode === GameMode.Shared && record.playerId === this.currentMasterClientPlayerId);
885
+ }
886
+ updateSession(update) {
887
+ if (this.session == null)
888
+ return;
889
+ if (update.isOpen != null)
890
+ this.session.isOpen = update.isOpen;
891
+ if (update.isVisible != null)
892
+ this.session.isVisible = update.isVisible;
893
+ if (update.properties != null)
894
+ this.session.properties = Object.assign({}, update.properties);
895
+ this.broadcastControl(FUSION_CONTROL_SESSION, this.sessionSnapshot(), null);
896
+ this.publishSession();
897
+ }
898
+ endSession(reason) {
899
+ this.clients.forEach((record) => {
900
+ if (record.connected)
901
+ this.outputs.sendControl(record.clientKey, FUSION_CONTROL_SHUTDOWN, { reason: reason });
902
+ });
903
+ this.clients.clear();
904
+ this.resetSession();
905
+ }
906
+ resetSession() {
907
+ this.sharedObjects.clear();
908
+ this.sessionMode = null;
909
+ this.session = null;
910
+ this.currentHostPlayerId = 0;
911
+ this.currentMasterClientPlayerId = 0;
912
+ this.latestHostSnapshot = null;
913
+ this.latestSceneInfoFrame = null;
914
+ this.latestSceneInfo = null;
915
+ this.migration = null;
916
+ this.migrationHeldFrames.length = 0;
917
+ this.outputs.lockSession(true);
918
+ }
919
+ publishSession() {
920
+ if (this.session == null)
921
+ return;
922
+ const snapshot = this.sessionSnapshot();
923
+ this.outputs.setSessionMetadata(snapshot);
924
+ this.outputs.lockSession(!snapshot.isOpen || this.clients.size >= snapshot.maxPlayers);
925
+ }
926
+ sessionSnapshot() {
927
+ let connectedCount = 0;
928
+ this.clients.forEach((record) => {
929
+ if (record.connected)
930
+ connectedCount += 1;
931
+ });
932
+ return Object.assign({}, this.session, { playerCount: connectedCount, properties: Object.assign({}, this.session.properties) });
933
+ }
934
+ broadcastControl(name, payload, exceptKey) {
935
+ this.forEachAdmitted(exceptKey, (record) => this.outputs.sendControl(record.clientKey, name, payload));
936
+ if (this.dedicatedEndpoint != null)
937
+ this.dedicatedEndpoint.receiveControl(name, payload);
938
+ }
939
+ forEachAdmitted(exceptKey, action) {
940
+ for (const record of Array.from(this.clients.values())) {
941
+ if (record.connected && record.admitted && record.clientKey !== exceptKey)
942
+ action(record);
943
+ }
944
+ }
945
+ hasOtherConnectedPlayer(record) {
946
+ for (const other of this.clients.values()) {
947
+ if (other !== record && other.connected)
948
+ return true;
949
+ }
950
+ return false;
951
+ }
952
+ findPlayer(playerId) {
953
+ for (const record of this.clients.values()) {
954
+ if (record.playerId === playerId)
955
+ return record;
956
+ }
957
+ return null;
958
+ }
959
+ findAdmittedPlayer(playerId) {
960
+ const record = this.findPlayer(playerId);
961
+ return (record != null && record.connected && record.admitted) ? record : null;
962
+ }
963
+ static storedSpawnBlock(object) {
964
+ const header = Object.assign({}, object.spawnHeader, { stateAuthority: object.authorityPlayerId });
965
+ return { header: header, deltaBytes: object.deltaBytes };
966
+ }
967
+ static areaHolds(area, position) {
968
+ if (area == null || area.position == null)
969
+ return false;
970
+ const offsetX = position[0] - area.position[0];
971
+ const offsetY = position[1] - area.position[1];
972
+ const offsetZ = position[2] - area.position[2];
973
+ return Math.sqrt(offsetX * offsetX + offsetY * offsetY + offsetZ * offsetZ) <= area.radius;
974
+ }
975
+ static sceneKey(scene) {
976
+ return scene.index + "|" + scene.path + "|" + scene.loadId;
977
+ }
978
+ static error(reason) {
979
+ return new NetworkError(ShutdownReason[reason]);
980
+ }
981
+ }
982
+ FusionSessionLogic.MILLISECONDS_PER_SECOND = 1000;
983
+ FusionSessionLogic.DEFAULT_PLAYER_COUNT = 8;
984
+ FusionSessionLogic.SESSION_PLAYER_ID = 0;
985
+ FusionSessionLogic.MAX_MIGRATION_HELD_FRAMES = 1024;
986
+ FusionSessionLogic.REGION = "local";