@dcl/sdk 7.22.6-25007982108.commit-83012ab → 7.22.6-25321038582.commit-63ddb3f
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.
- package/atom.d.ts +19 -0
- package/atom.js +83 -0
- package/future.d.ts +8 -0
- package/future.js +26 -0
- package/index.js +1 -5
- package/internal/transports/rendererTransport.d.ts +0 -4
- package/internal/transports/rendererTransport.js +1 -8
- package/network/binary-message-bus.d.ts +6 -3
- package/network/binary-message-bus.js +9 -5
- package/network/chunking.d.ts +5 -0
- package/network/chunking.js +38 -0
- package/network/entities.js +11 -3
- package/network/events/implementation.d.ts +93 -0
- package/network/events/implementation.js +221 -0
- package/network/events/index.d.ts +42 -0
- package/network/events/index.js +43 -0
- package/network/events/protocol.d.ts +27 -0
- package/network/events/protocol.js +66 -0
- package/network/events/registry.d.ts +8 -0
- package/network/events/registry.js +3 -0
- package/network/index.d.ts +8 -2
- package/network/index.js +16 -3
- package/network/message-bus-sync.d.ts +14 -1
- package/network/message-bus-sync.js +161 -103
- package/network/server/index.d.ts +14 -0
- package/network/server/index.js +219 -0
- package/network/server/utils.d.ts +18 -0
- package/network/server/utils.js +135 -0
- package/network/state.js +8 -7
- package/package.json +6 -6
- package/platform/index.d.ts +5 -0
- package/platform/index.js +29 -0
- package/players/index.d.ts +0 -1
- package/players/index.js +1 -2
- package/server/env-var.d.ts +15 -0
- package/server/env-var.js +31 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +3 -0
- package/server/storage/constants.d.ts +23 -0
- package/server/storage/constants.js +2 -0
- package/server/storage/index.d.ts +22 -0
- package/server/storage/index.js +29 -0
- package/server/storage/player.d.ts +43 -0
- package/server/storage/player.js +92 -0
- package/server/storage/scene.d.ts +38 -0
- package/server/storage/scene.js +90 -0
- package/server/storage-url.d.ts +10 -0
- package/server/storage-url.js +29 -0
- package/server/utils.d.ts +35 -0
- package/server/utils.js +56 -0
- package/src/atom.ts +98 -0
- package/src/future.ts +38 -0
- package/src/index.ts +1 -5
- package/src/internal/transports/rendererTransport.ts +0 -13
- package/src/network/binary-message-bus.ts +9 -4
- package/src/network/chunking.ts +45 -0
- package/src/network/entities.ts +10 -2
- package/src/network/events/implementation.ts +271 -0
- package/src/network/events/index.ts +48 -0
- package/src/network/events/protocol.ts +94 -0
- package/src/network/events/registry.ts +18 -0
- package/src/network/index.ts +40 -3
- package/src/network/message-bus-sync.ts +174 -110
- package/src/network/server/index.ts +301 -0
- package/src/network/server/utils.ts +189 -0
- package/src/network/state.ts +9 -5
- package/src/platform/index.ts +35 -0
- package/src/players/index.ts +0 -2
- package/src/server/env-var.ts +36 -0
- package/src/server/index.ts +2 -0
- package/src/server/storage/constants.ts +22 -0
- package/src/server/storage/index.ts +44 -0
- package/src/server/storage/player.ts +156 -0
- package/src/server/storage/scene.ts +149 -0
- package/src/server/storage-url.ts +34 -0
- package/src/server/utils.ts +73 -0
- package/src/testing/runtime.ts +3 -3
- package/testing/runtime.js +4 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ReadWriteByteBuffer } from '@dcl/ecs/dist/serialization/ByteBuffer'
|
|
2
|
+
import { Schemas } from '@dcl/ecs'
|
|
3
|
+
import { EventSchemas, EventTypes, EventSchemaRegistry } from './registry'
|
|
4
|
+
|
|
5
|
+
// Event envelope that wraps all events with metadata
|
|
6
|
+
const EventEnvelope = Schemas.Map({
|
|
7
|
+
eventType: Schemas.String,
|
|
8
|
+
timestamp: Schemas.Int64
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Encode an event into a binary buffer
|
|
13
|
+
* @param eventType - The type of event from the registry
|
|
14
|
+
* @param data - The event data matching the schema
|
|
15
|
+
* @param registry - Optional custom registry (defaults to EventSchemas)
|
|
16
|
+
* @returns Binary buffer containing the encoded event
|
|
17
|
+
*/
|
|
18
|
+
export function encodeEvent<T extends EventSchemaRegistry = typeof EventSchemas, K extends keyof T = keyof T>(
|
|
19
|
+
eventType: K,
|
|
20
|
+
data: EventTypes<T>[K],
|
|
21
|
+
registry: T = EventSchemas as T
|
|
22
|
+
): Uint8Array {
|
|
23
|
+
const buffer = new ReadWriteByteBuffer()
|
|
24
|
+
|
|
25
|
+
// Write envelope with event type and timestamp
|
|
26
|
+
EventEnvelope.serialize(
|
|
27
|
+
{
|
|
28
|
+
eventType: eventType as string,
|
|
29
|
+
timestamp: Date.now()
|
|
30
|
+
},
|
|
31
|
+
buffer
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// Get the schema for this event type
|
|
35
|
+
const schema = registry[eventType]
|
|
36
|
+
if (!schema) {
|
|
37
|
+
throw new Error(`Unknown event type: ${String(eventType)}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Write the typed payload
|
|
41
|
+
schema.serialize(data, buffer)
|
|
42
|
+
|
|
43
|
+
return buffer.toBinary()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decode a binary buffer into an event
|
|
48
|
+
* @param data - Binary buffer containing the encoded event
|
|
49
|
+
* @param registry - Optional custom registry (defaults to EventSchemas)
|
|
50
|
+
* @returns Decoded event with type, payload, and timestamp
|
|
51
|
+
*/
|
|
52
|
+
export function decodeEvent<T extends EventSchemaRegistry = typeof EventSchemas>(
|
|
53
|
+
data: Uint8Array,
|
|
54
|
+
registry: T = EventSchemas as T
|
|
55
|
+
): {
|
|
56
|
+
eventType: keyof T
|
|
57
|
+
payload: EventTypes<T>[keyof T]
|
|
58
|
+
timestamp: number
|
|
59
|
+
} {
|
|
60
|
+
const buffer = new ReadWriteByteBuffer()
|
|
61
|
+
buffer.writeBuffer(data, false)
|
|
62
|
+
|
|
63
|
+
// Read envelope
|
|
64
|
+
const envelope = EventEnvelope.deserialize(buffer)
|
|
65
|
+
const eventType = envelope.eventType as keyof T
|
|
66
|
+
|
|
67
|
+
// Get the schema for this event type
|
|
68
|
+
const schema = registry[eventType]
|
|
69
|
+
if (!schema) {
|
|
70
|
+
throw new Error(`Unknown event type: ${String(eventType)}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Read the typed payload
|
|
74
|
+
const payload = schema.deserialize(buffer)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
eventType,
|
|
78
|
+
payload,
|
|
79
|
+
timestamp: envelope.timestamp
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate if an event type exists in the registry
|
|
85
|
+
* @param eventType - The event type to check
|
|
86
|
+
* @param registry - Optional custom registry (defaults to EventSchemas)
|
|
87
|
+
* @returns True if the event type exists
|
|
88
|
+
*/
|
|
89
|
+
export function isValidEventType<T extends EventSchemaRegistry = typeof EventSchemas>(
|
|
90
|
+
eventType: string,
|
|
91
|
+
registry: T = EventSchemas as T
|
|
92
|
+
): eventType is Extract<keyof T, string> {
|
|
93
|
+
return eventType in registry
|
|
94
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ISchema } from '@dcl/ecs'
|
|
2
|
+
|
|
3
|
+
// Base type for event schema registry
|
|
4
|
+
export type EventSchemaRegistry = Record<string, ISchema>
|
|
5
|
+
|
|
6
|
+
// Type extraction from schemas
|
|
7
|
+
export type EventTypes<T extends EventSchemaRegistry = EventSchemaRegistry> = {
|
|
8
|
+
[K in keyof T]: T[K] extends ISchema<infer U> ? U : never
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Global interface that users can augment with their own events
|
|
12
|
+
export type RegisteredEvents = EventSchemaRegistry
|
|
13
|
+
|
|
14
|
+
// Default empty registry
|
|
15
|
+
export const EventSchemas = {} as RegisteredEvents
|
|
16
|
+
|
|
17
|
+
// Helper to ensure user events conform to the registry type
|
|
18
|
+
export type ValidateEventRegistry<T extends EventSchemaRegistry> = T
|
package/src/network/index.ts
CHANGED
|
@@ -2,9 +2,46 @@ import { sendBinary } from '~system/CommunicationsController'
|
|
|
2
2
|
import { engine } from '@dcl/ecs'
|
|
3
3
|
import { addSyncTransport } from './message-bus-sync'
|
|
4
4
|
import { getUserData } from '~system/UserIdentity'
|
|
5
|
+
import { isServer as isServerApi } from '~system/EngineApi'
|
|
6
|
+
import { Atom } from '../atom'
|
|
7
|
+
|
|
8
|
+
// Create isServer atom for consistent state
|
|
9
|
+
const isServerAtom = Atom<boolean>(false)
|
|
10
|
+
void isServerApi({}).then((response) => {
|
|
11
|
+
isServerAtom.swap(!!response.isServer)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// Helper function to check if running on server
|
|
15
|
+
export function isServer(): boolean {
|
|
16
|
+
return isServerAtom.getOrNull() ?? false
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
// initialize sync transport for sdk engine
|
|
7
|
-
const {
|
|
8
|
-
|
|
20
|
+
const {
|
|
21
|
+
getChildren,
|
|
22
|
+
syncEntity,
|
|
23
|
+
parentEntity,
|
|
24
|
+
getParent,
|
|
25
|
+
myProfile,
|
|
26
|
+
removeParent,
|
|
27
|
+
getFirstChild,
|
|
28
|
+
isStateSyncronized,
|
|
29
|
+
binaryMessageBus,
|
|
30
|
+
eventBus
|
|
31
|
+
} = addSyncTransport(engine, sendBinary, getUserData, isServerApi, 'network')
|
|
32
|
+
|
|
33
|
+
// Re-export the room messaging system
|
|
34
|
+
export { registerMessages, getRoom } from './events'
|
|
9
35
|
|
|
10
|
-
export {
|
|
36
|
+
export {
|
|
37
|
+
getFirstChild,
|
|
38
|
+
getChildren,
|
|
39
|
+
syncEntity,
|
|
40
|
+
parentEntity,
|
|
41
|
+
getParent,
|
|
42
|
+
myProfile,
|
|
43
|
+
removeParent,
|
|
44
|
+
isStateSyncronized,
|
|
45
|
+
binaryMessageBus,
|
|
46
|
+
eventBus
|
|
47
|
+
}
|
|
@@ -1,27 +1,53 @@
|
|
|
1
|
-
import { IEngine, Transport, RealmInfo
|
|
1
|
+
import { IEngine, Transport, RealmInfo } from '@dcl/ecs'
|
|
2
2
|
import { type SendBinaryRequest, type SendBinaryResponse } from '~system/CommunicationsController'
|
|
3
3
|
|
|
4
4
|
import { syncFilter } from './filter'
|
|
5
5
|
import { engineToCrdt } from './state'
|
|
6
|
-
import { BinaryMessageBus, CommsMessage
|
|
6
|
+
import { BinaryMessageBus, CommsMessage } from './binary-message-bus'
|
|
7
7
|
import { fetchProfile } from './utils'
|
|
8
8
|
import { entityUtils } from './entities'
|
|
9
|
+
import { createServerValidator } from './server'
|
|
9
10
|
import { GetUserDataRequest, GetUserDataResponse } from '~system/UserIdentity'
|
|
10
11
|
import { definePlayerHelper } from '../players'
|
|
11
12
|
import { serializeCrdtMessages } from '../internal/transports/logger'
|
|
13
|
+
import { IsServerRequest, IsServerResponse } from '~system/EngineApi'
|
|
14
|
+
import { Atom } from '../atom'
|
|
15
|
+
import { setGlobalRoom, Room } from './events/implementation'
|
|
12
16
|
|
|
13
17
|
export type IProfile = { networkId: number; userId: string }
|
|
14
18
|
// user that we asked for the inital crdt state
|
|
19
|
+
export const AUTH_SERVER_PEER_ID = 'authoritative-server'
|
|
20
|
+
export const DEBUG_NETWORK_MESSAGES = () => (globalThis as any).DEBUG_NETWORK_MESSAGES ?? false
|
|
21
|
+
|
|
22
|
+
// Test environment detection without 'as any'
|
|
23
|
+
const isTestEnvironment = (): boolean => {
|
|
24
|
+
try {
|
|
25
|
+
if (typeof globalThis === 'undefined') return false
|
|
26
|
+
const globalWithProcess = globalThis as unknown as { process?: { env?: { NODE_ENV?: string } } }
|
|
27
|
+
return globalWithProcess.process?.env?.NODE_ENV === 'test'
|
|
28
|
+
} catch {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export function addSyncTransport(
|
|
16
34
|
engine: IEngine,
|
|
17
35
|
sendBinary: (msg: SendBinaryRequest) => Promise<SendBinaryResponse>,
|
|
18
|
-
getUserData: (value: GetUserDataRequest) => Promise<GetUserDataResponse
|
|
36
|
+
getUserData: (value: GetUserDataRequest) => Promise<GetUserDataResponse>,
|
|
37
|
+
isServerFn: (request: IsServerRequest) => Promise<IsServerResponse>,
|
|
38
|
+
name: string
|
|
19
39
|
) {
|
|
20
|
-
const DEBUG_NETWORK_MESSAGES = () => (globalThis as any).DEBUG_NETWORK_MESSAGES ?? false
|
|
21
40
|
// Profile Info
|
|
22
41
|
const myProfile: IProfile = {} as IProfile
|
|
23
42
|
fetchProfile(myProfile!, getUserData)
|
|
24
43
|
|
|
44
|
+
const isServerAtom = Atom<boolean>()
|
|
45
|
+
const isRoomReadyAtom = Atom<boolean>(false)
|
|
46
|
+
|
|
47
|
+
void isServerFn({}).then(($: IsServerResponse) => {
|
|
48
|
+
return isServerAtom.swap(!!$.isServer)
|
|
49
|
+
})
|
|
50
|
+
|
|
25
51
|
// Entity utils
|
|
26
52
|
const entityDefinitions = entityUtils(engine, myProfile)
|
|
27
53
|
|
|
@@ -40,108 +66,190 @@ export function addSyncTransport(
|
|
|
40
66
|
const players = definePlayerHelper(engine)
|
|
41
67
|
|
|
42
68
|
let stateIsSyncronized = false
|
|
43
|
-
let transportInitialzed = false
|
|
44
69
|
|
|
70
|
+
/**
|
|
71
|
+
* We need to wait till 2 ticks that is when the engine is ready to send new messages.
|
|
72
|
+
* The first tick is for the client engine processing the CRDT messages,
|
|
73
|
+
* and the second one are the messages created by the main() function.
|
|
74
|
+
* So to avoid sending those messages, that all the clients have, through the network we put this validation here.
|
|
75
|
+
*/
|
|
76
|
+
let tick = 0
|
|
77
|
+
const TRANSPORT_INITIALIZED_NUMBER = isTestEnvironment() ? 0 : 2
|
|
45
78
|
// Add Sync Transport
|
|
46
79
|
const transport: Transport = {
|
|
47
80
|
filter: syncFilter(engine),
|
|
48
81
|
send: async (messages) => {
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
if (tick <= TRANSPORT_INITIALIZED_NUMBER) tick++
|
|
83
|
+
for (const message of tick > TRANSPORT_INITIALIZED_NUMBER ? [messages].flat() : []) {
|
|
84
|
+
if (message.byteLength) {
|
|
51
85
|
DEBUG_NETWORK_MESSAGES() &&
|
|
52
86
|
console.log(...Array.from(serializeCrdtMessages('[NetworkMessage sent]:', message, engine)))
|
|
53
|
-
|
|
87
|
+
|
|
88
|
+
// Convert regular messages to network messages for broadcasting with chunking
|
|
89
|
+
for (const chunk of serverValidator.convertRegularToNetworkMessage(message)) {
|
|
90
|
+
binaryMessageBus.emit(CommsMessage.CRDT, chunk)
|
|
91
|
+
}
|
|
54
92
|
}
|
|
55
93
|
}
|
|
56
94
|
const peerMessages = getMessagesToSend()
|
|
57
|
-
let totalSize = 0
|
|
58
|
-
for (const message of peerMessages) {
|
|
59
|
-
for (const data of message.data) {
|
|
60
|
-
totalSize += data.byteLength
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (totalSize) {
|
|
64
|
-
DEBUG_NETWORK_MESSAGES() && console.log('Sending network messages: ', totalSize / 1024, 'KB')
|
|
65
|
-
}
|
|
66
95
|
const response = await sendBinary({ data: [], peerData: peerMessages })
|
|
67
96
|
binaryMessageBus.__processMessages(response.data)
|
|
68
|
-
transportInitialzed = true
|
|
69
97
|
},
|
|
70
|
-
type:
|
|
98
|
+
type: name
|
|
71
99
|
}
|
|
100
|
+
|
|
101
|
+
// Server validation setup
|
|
102
|
+
const serverValidator = createServerValidator({
|
|
103
|
+
engine,
|
|
104
|
+
binaryMessageBus
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Initialize Event Bus with registered schemas
|
|
108
|
+
const eventBus = new Room(engine, binaryMessageBus, isServerAtom, isRoomReadyAtom)
|
|
109
|
+
|
|
110
|
+
// Set global eventBus instance
|
|
111
|
+
setGlobalRoom(eventBus)
|
|
112
|
+
|
|
72
113
|
engine.addTransport(transport)
|
|
73
114
|
// End add sync transport
|
|
74
115
|
|
|
75
116
|
// Receive & Process CRDT_STATE
|
|
76
|
-
binaryMessageBus.on(CommsMessage.
|
|
77
|
-
|
|
78
|
-
|
|
117
|
+
binaryMessageBus.on(CommsMessage.REQ_CRDT_STATE, async (data, sender) => {
|
|
118
|
+
DEBUG_NETWORK_MESSAGES() && console.log('[REQ_CRDT_STATE]', sender, Date.now())
|
|
119
|
+
const chunks = engineToCrdt(engine)
|
|
120
|
+
if (chunks.length === 0) {
|
|
121
|
+
DEBUG_NETWORK_MESSAGES() && console.log('[Emiting empty state:]', sender, Date.now())
|
|
122
|
+
binaryMessageBus.emit(CommsMessage.RES_CRDT_STATE, new Uint8Array(), [sender])
|
|
123
|
+
} else {
|
|
124
|
+
for (const chunk of chunks) {
|
|
125
|
+
DEBUG_NETWORK_MESSAGES() && console.log('[Emiting:]', sender, Date.now())
|
|
126
|
+
binaryMessageBus.emit(CommsMessage.RES_CRDT_STATE, chunk, [sender])
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
binaryMessageBus.on(CommsMessage.RES_CRDT_STATE, async (data, sender) => {
|
|
131
|
+
requestingState = false
|
|
132
|
+
elapsedTimeSinceRequest = 0
|
|
133
|
+
if (isServerAtom.getOrNull() || sender !== AUTH_SERVER_PEER_ID) return
|
|
79
134
|
DEBUG_NETWORK_MESSAGES() && console.log('[Processing CRDT State]', data.byteLength / 1024, 'KB')
|
|
80
|
-
|
|
135
|
+
if (data.byteLength > 0) {
|
|
136
|
+
transport.onmessage!(serverValidator.processClientMessages(data, sender))
|
|
137
|
+
}
|
|
81
138
|
stateIsSyncronized = true
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// Answer to REQ_CRDT_STATE
|
|
85
|
-
binaryMessageBus.on(CommsMessage.REQ_CRDT_STATE, async (_, userId) => {
|
|
86
|
-
DEBUG_NETWORK_MESSAGES() && console.log(`Sending CRDT State to: ${userId}`)
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
140
|
+
// IMPORTANT: Only mark room as ready AFTER state is synchronized
|
|
141
|
+
// This ensures comms is truly connected and working
|
|
142
|
+
const realmInfo = RealmInfo.getOrNull(engine.RootEntity)
|
|
143
|
+
if (realmInfo) {
|
|
144
|
+
DEBUG_NETWORK_MESSAGES() && console.log('[isRoomReady] Marking room as ready after state sync')
|
|
145
|
+
isRoomReadyAtom.swap(true)
|
|
90
146
|
}
|
|
91
147
|
})
|
|
92
148
|
|
|
93
|
-
//
|
|
94
|
-
binaryMessageBus.on(CommsMessage.CRDT, (value) => {
|
|
149
|
+
// received message from the network
|
|
150
|
+
binaryMessageBus.on(CommsMessage.CRDT, (value, sender) => {
|
|
151
|
+
const isServer = isServerAtom.getOrNull()
|
|
95
152
|
DEBUG_NETWORK_MESSAGES() &&
|
|
96
|
-
console.log(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
153
|
+
console.log(
|
|
154
|
+
transport.type,
|
|
155
|
+
...Array.from(serializeCrdtMessages('[NetworkMessage received]:', value, engine)),
|
|
156
|
+
isServer
|
|
157
|
+
)
|
|
158
|
+
if (isServer) {
|
|
159
|
+
transport.onmessage!(serverValidator.processServerMessages(value, sender))
|
|
160
|
+
} else if (sender === AUTH_SERVER_PEER_ID) {
|
|
161
|
+
// Process network messages from server and convert to regular messages
|
|
162
|
+
transport.onmessage!(serverValidator.processClientMessages(value, sender))
|
|
107
163
|
}
|
|
164
|
+
})
|
|
108
165
|
|
|
109
|
-
|
|
166
|
+
// received authoritative message from server - force apply to fix invalid local state
|
|
167
|
+
binaryMessageBus.on(CommsMessage.CRDT_AUTHORITATIVE, (value, sender) => {
|
|
168
|
+
// Only accept authoritative messages from authoritative server
|
|
169
|
+
if (sender !== AUTH_SERVER_PEER_ID) return
|
|
110
170
|
|
|
111
|
-
//
|
|
112
|
-
|
|
171
|
+
// DEBUG_NETWORK_MESSAGES() &&
|
|
172
|
+
console.log('[AUTHORITATIVE] Received authoritative message from server:', value.byteLength, 'bytes')
|
|
113
173
|
|
|
114
|
-
|
|
174
|
+
// Process authoritative messages by forcing them through normal CRDT processing
|
|
175
|
+
// but with a timestamp that's guaranteed to be accepted
|
|
176
|
+
const authoritativeBuffer = serverValidator.processClientMessages(value, sender, true)
|
|
177
|
+
if (authoritativeBuffer.byteLength > 0) {
|
|
178
|
+
// Apply authoritative message through normal transport, but the server's messages
|
|
179
|
+
// should be processed as authoritative with special timestamp handling
|
|
180
|
+
transport.onmessage!(authoritativeBuffer)
|
|
115
181
|
|
|
116
|
-
|
|
117
|
-
if (players.length > 1 && retryCount <= 2) {
|
|
118
|
-
DEBUG_NETWORK_MESSAGES() &&
|
|
119
|
-
console.log(`Requesting state again ${retryCount} (no response). Players connected: ${players.length - 1}`)
|
|
120
|
-
void requestState(retryCount + 1)
|
|
121
|
-
} else {
|
|
122
|
-
DEBUG_NETWORK_MESSAGES() && console.log('No active players. State syncronized')
|
|
123
|
-
stateIsSyncronized = true
|
|
124
|
-
}
|
|
182
|
+
DEBUG_NETWORK_MESSAGES() && console.log('[AUTHORITATIVE] Applied server authoritative message to local state')
|
|
125
183
|
}
|
|
126
|
-
}
|
|
184
|
+
})
|
|
127
185
|
|
|
128
186
|
players.onEnterScene((player) => {
|
|
129
187
|
DEBUG_NETWORK_MESSAGES() && console.log('[onEnterScene]', player.userId)
|
|
188
|
+
if (!isServerAtom.getOrNull() && myProfile.userId === player.userId) {
|
|
189
|
+
requestState()
|
|
190
|
+
}
|
|
130
191
|
})
|
|
131
192
|
|
|
132
193
|
// Asks for the REQ_CRDT_STATE when its connected to comms
|
|
133
194
|
RealmInfo.onChange(engine.RootEntity, (value) => {
|
|
195
|
+
const isServer = isServerAtom.getOrNull()
|
|
196
|
+
|
|
134
197
|
if (!value?.isConnectedSceneRoom) {
|
|
135
|
-
|
|
136
|
-
|
|
198
|
+
// Only react when actually transitioning from ready to not ready
|
|
199
|
+
if (isRoomReadyAtom.getOrNull() === true) {
|
|
200
|
+
DEBUG_NETWORK_MESSAGES() && console.log('Disconnected from comms')
|
|
201
|
+
isRoomReadyAtom.swap(false)
|
|
202
|
+
if (!isServer) {
|
|
203
|
+
stateIsSyncronized = false
|
|
204
|
+
}
|
|
205
|
+
}
|
|
137
206
|
}
|
|
138
207
|
|
|
139
208
|
if (value?.isConnectedSceneRoom) {
|
|
140
|
-
|
|
209
|
+
requestState()
|
|
210
|
+
|
|
211
|
+
// For servers, mark as ready immediately when connected
|
|
212
|
+
// (servers don't need to sync state from anyone)
|
|
213
|
+
if (isServer && isRoomReadyAtom.getOrNull() === false) {
|
|
214
|
+
DEBUG_NETWORK_MESSAGES() && console.log('[isRoomReady] Server marking room as ready')
|
|
215
|
+
isRoomReadyAtom.swap(true)
|
|
216
|
+
}
|
|
217
|
+
// For clients, room will be marked ready after receiving CRDT state (above)
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
let requestingState = false
|
|
222
|
+
let elapsedTimeSinceRequest = 0
|
|
223
|
+
const STATE_REQUEST_RETRY_INTERVAL = 2.0 // seconds
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Why we have to request the state if we have a server that can send us the state when we joined?
|
|
227
|
+
* The thing is that when the server detects a new JOIN_PARTICIPANT on livekit room, it sends automatically the state to that peer.
|
|
228
|
+
* But in unity, it takes more time, so that message is not being delivered to the client.
|
|
229
|
+
* So instead, when we are finally connected to the room, we request the state, and then the server answers with the state :)
|
|
230
|
+
*
|
|
231
|
+
* If no response is received within 2 seconds, the request is automatically retried.
|
|
232
|
+
*/
|
|
233
|
+
function requestState() {
|
|
234
|
+
if (isServerAtom.getOrNull()) return
|
|
235
|
+
if (RealmInfo.getOrNull(engine.RootEntity)?.isConnectedSceneRoom && !requestingState) {
|
|
236
|
+
requestingState = true
|
|
237
|
+
elapsedTimeSinceRequest = 0
|
|
238
|
+
DEBUG_NETWORK_MESSAGES() && console.log('Requesting state...')
|
|
239
|
+
binaryMessageBus.emit(CommsMessage.REQ_CRDT_STATE, new Uint8Array())
|
|
141
240
|
}
|
|
241
|
+
}
|
|
142
242
|
|
|
143
|
-
|
|
144
|
-
|
|
243
|
+
// System to retry state request if no response is received within the retry interval
|
|
244
|
+
engine.addSystem((dt: number) => {
|
|
245
|
+
if (requestingState && !stateIsSyncronized) {
|
|
246
|
+
elapsedTimeSinceRequest += dt
|
|
247
|
+
if (elapsedTimeSinceRequest >= STATE_REQUEST_RETRY_INTERVAL) {
|
|
248
|
+
DEBUG_NETWORK_MESSAGES() && console.log('State request timed out, retrying...')
|
|
249
|
+
elapsedTimeSinceRequest = 0
|
|
250
|
+
requestingState = false
|
|
251
|
+
requestState()
|
|
252
|
+
}
|
|
145
253
|
}
|
|
146
254
|
})
|
|
147
255
|
|
|
@@ -153,56 +261,12 @@ export function addSyncTransport(
|
|
|
153
261
|
return stateIsSyncronized
|
|
154
262
|
}
|
|
155
263
|
|
|
156
|
-
function sleep(ms: number) {
|
|
157
|
-
return new Promise<void>((resolve) => {
|
|
158
|
-
let timer = 0
|
|
159
|
-
function sleepSystem(dt: number) {
|
|
160
|
-
timer += dt
|
|
161
|
-
if (timer * 1000 >= ms) {
|
|
162
|
-
engine.removeSystem(sleepSystem)
|
|
163
|
-
resolve()
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
engine.addSystem(sleepSystem)
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
|
|
170
264
|
return {
|
|
171
265
|
...entityDefinitions,
|
|
172
266
|
myProfile,
|
|
173
|
-
isStateSyncronized
|
|
267
|
+
isStateSyncronized,
|
|
268
|
+
binaryMessageBus,
|
|
269
|
+
eventBus,
|
|
270
|
+
isRoomReadyAtom
|
|
174
271
|
}
|
|
175
272
|
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Messages Protocol Encoding
|
|
179
|
-
*
|
|
180
|
-
* CRDT: Plain Uint8Array
|
|
181
|
-
*
|
|
182
|
-
* CRDT_STATE_RES { sender: string, data: Uint8Array}
|
|
183
|
-
*/
|
|
184
|
-
function decodeCRDTState(data: Uint8Array) {
|
|
185
|
-
let offset = 0
|
|
186
|
-
const r = new Uint8Array(data)
|
|
187
|
-
const view = new DataView(r.buffer)
|
|
188
|
-
const senderLength = view.getUint8(offset)
|
|
189
|
-
offset += 1
|
|
190
|
-
const sender = decodeString(data.subarray(1, senderLength + 1))
|
|
191
|
-
offset += senderLength
|
|
192
|
-
const state = r.subarray(offset)
|
|
193
|
-
|
|
194
|
-
return { sender, data: state }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function encodeCRDTState(address: string, data: Uint8Array) {
|
|
198
|
-
// address to uint8array
|
|
199
|
-
const addressBuffer = encodeString(address)
|
|
200
|
-
const addressOffset = 1
|
|
201
|
-
const messageLength = addressOffset + addressBuffer.byteLength + data.byteLength
|
|
202
|
-
|
|
203
|
-
const serializedMessage = new Uint8Array(messageLength)
|
|
204
|
-
serializedMessage.set(new Uint8Array([addressBuffer.byteLength]), 0)
|
|
205
|
-
serializedMessage.set(addressBuffer, 1)
|
|
206
|
-
serializedMessage.set(data, addressBuffer.byteLength + 1)
|
|
207
|
-
return serializedMessage
|
|
208
|
-
}
|