@usions/sdk 2.0.1 → 2.1.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.
- package/README.md +5 -5
- package/package.json +16 -5
- package/src/browser.js +1392 -1139
- package/src/index.js +4 -49
- package/src/modules/back-button.js +26 -0
- package/src/modules/bot.js +52 -0
- package/src/modules/chat.js +35 -0
- package/src/modules/core.js +207 -0
- package/src/modules/file-storage.js +46 -0
- package/src/modules/game-core.js +149 -0
- package/src/modules/game-direct.js +219 -0
- package/src/modules/game-methods.js +334 -0
- package/src/modules/game-proxy.js +149 -0
- package/src/modules/game-socket.js +195 -0
- package/src/modules/index.js +54 -0
- package/src/modules/misc.js +123 -0
- package/src/modules/results.js +34 -0
- package/src/modules/session.js +62 -0
- package/src/modules/storage.js +64 -0
- package/src/modules/ui.js +111 -0
- package/src/modules/user.js +61 -0
- package/src/modules/wallet.js +114 -0
- package/types/index.d.ts +62 -2
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Game Direct — WebSocket direct connection to game server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add direct WebSocket connection methods to game module
|
|
7
|
+
* @param {object} game - The game module object
|
|
8
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
9
|
+
*/
|
|
10
|
+
export function applyGameDirect(game, Usion) {
|
|
11
|
+
/**
|
|
12
|
+
* Connect directly to creator-controlled WebSocket server.
|
|
13
|
+
* Uses backend-issued short-lived room token.
|
|
14
|
+
* @returns {Promise}
|
|
15
|
+
*/
|
|
16
|
+
game.connectDirect = function(config) {
|
|
17
|
+
var self = this;
|
|
18
|
+
config = config || {};
|
|
19
|
+
|
|
20
|
+
if (self.directMode && self.directSocket && self.connected) {
|
|
21
|
+
return Promise.resolve();
|
|
22
|
+
}
|
|
23
|
+
if (self._connecting && self._connectPromise) {
|
|
24
|
+
return self._connectPromise;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
self._connecting = true;
|
|
28
|
+
self.directMode = true;
|
|
29
|
+
self._connectPromise = self._fetchDirectAccess(config)
|
|
30
|
+
.then(function(access) {
|
|
31
|
+
self.directConfig = access;
|
|
32
|
+
return self._initDirectSocket(access);
|
|
33
|
+
})
|
|
34
|
+
.then(function() {
|
|
35
|
+
self.connected = true;
|
|
36
|
+
self._connecting = false;
|
|
37
|
+
Usion.log('Direct game socket connected');
|
|
38
|
+
})
|
|
39
|
+
.catch(function(err) {
|
|
40
|
+
self._connecting = false;
|
|
41
|
+
self.connected = false;
|
|
42
|
+
self.directMode = false;
|
|
43
|
+
if (self._eventHandlers.connectionError) {
|
|
44
|
+
self._eventHandlers.connectionError(err);
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
});
|
|
48
|
+
return self._connectPromise;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
game._fetchDirectAccess = function(config) {
|
|
52
|
+
var roomId = config.roomId || this.roomId || Usion.config.roomId;
|
|
53
|
+
var serviceId = config.serviceId || Usion.config.serviceId;
|
|
54
|
+
var apiUrl = config.apiUrl || Usion.config.apiUrl || '';
|
|
55
|
+
var token = config.token || Usion.user.getToken();
|
|
56
|
+
|
|
57
|
+
if (!roomId) return Promise.reject(new Error('No room ID provided'));
|
|
58
|
+
if (!serviceId) return Promise.reject(new Error('No service ID provided'));
|
|
59
|
+
|
|
60
|
+
this.roomId = roomId;
|
|
61
|
+
this.playerId = Usion.user.getId();
|
|
62
|
+
|
|
63
|
+
// When embedded (iframe/WebView), proxy through parent app to avoid
|
|
64
|
+
// CORS / Private Network Access issues
|
|
65
|
+
if (Usion._isEmbedded) {
|
|
66
|
+
return Usion._request('GAME_ACCESS_REQUEST', {
|
|
67
|
+
room_id: roomId,
|
|
68
|
+
service_id: serviceId,
|
|
69
|
+
protocol_version: (config.protocolVersion || Usion.config.protocolVersion || Usion.config.protocol_version || '2')
|
|
70
|
+
}, 10000);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!apiUrl) return Promise.reject(new Error('No API URL provided'));
|
|
74
|
+
if (!token) return Promise.reject(new Error('No auth token available'));
|
|
75
|
+
|
|
76
|
+
var cleanApiUrl = String(apiUrl).replace(/\/$/, '');
|
|
77
|
+
var endpoint = cleanApiUrl + '/games/rooms/' + encodeURIComponent(roomId) + '/access';
|
|
78
|
+
return fetch(endpoint, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
'Authorization': 'Bearer ' + token
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
service_id: serviceId,
|
|
86
|
+
client_type: 'iframe',
|
|
87
|
+
protocol_version: (config.protocolVersion || Usion.config.protocolVersion || Usion.config.protocol_version || '2')
|
|
88
|
+
})
|
|
89
|
+
}).then(function(res) {
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
return res.text().then(function(text) {
|
|
92
|
+
throw new Error(text || ('Direct access failed: HTTP ' + res.status));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return res.json();
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
game._initDirectSocket = function(access) {
|
|
100
|
+
var self = this;
|
|
101
|
+
return new Promise(function(resolve, reject) {
|
|
102
|
+
if (!access || !access.ws_url || !access.access_token) {
|
|
103
|
+
reject(new Error('Invalid direct access payload'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var wsUrl = access.ws_url;
|
|
108
|
+
var separator = wsUrl.indexOf('?') === -1 ? '?' : '&';
|
|
109
|
+
var urlWithToken = wsUrl + separator + 'token=' + encodeURIComponent(access.access_token);
|
|
110
|
+
var ws = new WebSocket(urlWithToken);
|
|
111
|
+
self.directSocket = ws;
|
|
112
|
+
|
|
113
|
+
var opened = false;
|
|
114
|
+
var joinSent = false;
|
|
115
|
+
var timeout = setTimeout(function() {
|
|
116
|
+
if (!opened) {
|
|
117
|
+
try { ws.close(); } catch (e) {}
|
|
118
|
+
reject(new Error('Direct WebSocket connection timeout'));
|
|
119
|
+
}
|
|
120
|
+
}, 10000);
|
|
121
|
+
|
|
122
|
+
ws.onopen = function() {
|
|
123
|
+
opened = true;
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
if (!joinSent) {
|
|
126
|
+
joinSent = true;
|
|
127
|
+
self._sendDirect('join', {});
|
|
128
|
+
}
|
|
129
|
+
// Start heartbeat for direct mode
|
|
130
|
+
if (self._heartbeatInterval) clearInterval(self._heartbeatInterval);
|
|
131
|
+
self._heartbeatInterval = setInterval(function() {
|
|
132
|
+
if (self.directSocket && self.directSocket.readyState === WebSocket.OPEN) {
|
|
133
|
+
self._sendDirect('heartbeat', {});
|
|
134
|
+
}
|
|
135
|
+
}, 25000);
|
|
136
|
+
resolve();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
ws.onerror = function() {
|
|
140
|
+
if (!opened) {
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
reject(new Error('Direct WebSocket connection error'));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
ws.onclose = function(evt) {
|
|
147
|
+
self.connected = false;
|
|
148
|
+
self._joined = false;
|
|
149
|
+
self._joinPromise = null;
|
|
150
|
+
if (self._heartbeatInterval) {
|
|
151
|
+
clearInterval(self._heartbeatInterval);
|
|
152
|
+
self._heartbeatInterval = null;
|
|
153
|
+
}
|
|
154
|
+
if (self._eventHandlers.disconnect) {
|
|
155
|
+
self._eventHandlers.disconnect(evt && evt.reason ? evt.reason : 'direct socket closed');
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
ws.onmessage = function(evt) {
|
|
160
|
+
self._handleDirectMessage(evt && evt.data);
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
game._sendDirect = function(type, payload) {
|
|
166
|
+
if (!this.directSocket || this.directSocket.readyState !== WebSocket.OPEN) return;
|
|
167
|
+
this._directSeq = this._directSeq + 1;
|
|
168
|
+
this.directSocket.send(JSON.stringify({
|
|
169
|
+
type: type,
|
|
170
|
+
room_id: this.roomId,
|
|
171
|
+
ts: Date.now(),
|
|
172
|
+
seq: this._directSeq,
|
|
173
|
+
session_id: (this.directConfig && this.directConfig.session_id) ? this.directConfig.session_id : null,
|
|
174
|
+
protocol_version: (this.directConfig && this.directConfig.protocol_version) ? this.directConfig.protocol_version : '2',
|
|
175
|
+
payload: payload || {}
|
|
176
|
+
}));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
game._handleDirectMessage = function(raw) {
|
|
180
|
+
var data;
|
|
181
|
+
try {
|
|
182
|
+
data = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!data || !data.type) return;
|
|
187
|
+
var payload = data.payload || {};
|
|
188
|
+
|
|
189
|
+
if (data.type === 'joined') {
|
|
190
|
+
this._joined = true;
|
|
191
|
+
if (this._eventHandlers.joined) this._eventHandlers.joined(payload);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (data.type === 'player_joined') {
|
|
195
|
+
if (this._eventHandlers.playerJoined) this._eventHandlers.playerJoined(payload);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (data.type === 'player_left') {
|
|
199
|
+
if (this._eventHandlers.playerLeft) this._eventHandlers.playerLeft(payload);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (data.type === 'state_snapshot' || data.type === 'state_delta') {
|
|
203
|
+
if (this._eventHandlers.realtime) this._eventHandlers.realtime(payload);
|
|
204
|
+
if (this._eventHandlers.stateUpdate) this._eventHandlers.stateUpdate(payload);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (data.type === 'pong') {
|
|
208
|
+
if (this._eventHandlers.sync) this._eventHandlers.sync(payload);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (data.type === 'match_end') {
|
|
212
|
+
if (this._eventHandlers.finished) this._eventHandlers.finished(payload);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (data.type === 'error' && this._eventHandlers.error) {
|
|
216
|
+
this._eventHandlers.error(payload);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Game Methods — join, leave, action, realtime, sync, etc.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add game action methods to game module
|
|
7
|
+
* @param {object} game - The game module object
|
|
8
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
9
|
+
*/
|
|
10
|
+
export function applyGameMethods(game, Usion) {
|
|
11
|
+
/**
|
|
12
|
+
* Join a game room
|
|
13
|
+
* @param {string} roomId - Game room ID (optional, uses config)
|
|
14
|
+
* @returns {Promise} Resolves with join data
|
|
15
|
+
*/
|
|
16
|
+
game.join = function(roomId) {
|
|
17
|
+
const self = this;
|
|
18
|
+
roomId = roomId || Usion.config.roomId;
|
|
19
|
+
|
|
20
|
+
// If already joined this room, return cached promise/data
|
|
21
|
+
if (self._joined && self.roomId === roomId && self._joinPromise) {
|
|
22
|
+
return self._joinPromise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
self.roomId = roomId;
|
|
26
|
+
self.playerId = Usion.user.getId();
|
|
27
|
+
|
|
28
|
+
if (self.directMode) {
|
|
29
|
+
self._joined = true;
|
|
30
|
+
self._joinPromise = Promise.resolve({
|
|
31
|
+
room_id: roomId,
|
|
32
|
+
player_id: self.playerId
|
|
33
|
+
});
|
|
34
|
+
return self._joinPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Proxy mode: send join request to parent
|
|
38
|
+
if (self._useProxy) {
|
|
39
|
+
self._joinPromise = new Promise(function(resolve, reject) {
|
|
40
|
+
self._proxyJoinResolve = resolve;
|
|
41
|
+
self._proxyJoinReject = reject;
|
|
42
|
+
Usion._post({ type: 'GAME_JOIN', room_id: roomId });
|
|
43
|
+
setTimeout(function() {
|
|
44
|
+
if (!self._joined && self._proxyJoinReject) {
|
|
45
|
+
self._proxyJoinReject = null;
|
|
46
|
+
reject(new Error('Join timeout'));
|
|
47
|
+
}
|
|
48
|
+
}, 15000);
|
|
49
|
+
});
|
|
50
|
+
return self._joinPromise;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
self._joinPromise = new Promise(function(resolve, reject) {
|
|
54
|
+
if (!self.socket || !self.connected) {
|
|
55
|
+
reject(new Error('Not connected'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!roomId) {
|
|
60
|
+
reject(new Error('No room ID provided'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
self.socket.emit('game:join', { room_id: roomId }, function(response) {
|
|
65
|
+
if (response.error) {
|
|
66
|
+
self._joined = false;
|
|
67
|
+
reject(new Error(response.message || response.error));
|
|
68
|
+
} else {
|
|
69
|
+
self._joined = true;
|
|
70
|
+
if (response.sequence !== undefined) {
|
|
71
|
+
self._lastSequence = response.sequence;
|
|
72
|
+
}
|
|
73
|
+
resolve(response);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return self._joinPromise;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Leave the current game room
|
|
83
|
+
*/
|
|
84
|
+
game.leave = function() {
|
|
85
|
+
const self = this;
|
|
86
|
+
|
|
87
|
+
if (self.directMode) {
|
|
88
|
+
if (self.roomId) self._sendDirect('leave', {});
|
|
89
|
+
self.roomId = null;
|
|
90
|
+
self._lastSequence = 0;
|
|
91
|
+
self._joined = false;
|
|
92
|
+
self._joinPromise = null;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (self._useProxy) {
|
|
97
|
+
if (self.roomId) Usion._post({ type: 'GAME_LEAVE', room_id: self.roomId });
|
|
98
|
+
self.roomId = null;
|
|
99
|
+
self._lastSequence = 0;
|
|
100
|
+
self._joined = false;
|
|
101
|
+
self._joinPromise = null;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (self.socket && self.connected && self.roomId) {
|
|
106
|
+
self.socket.emit('game:leave', { room_id: self.roomId });
|
|
107
|
+
self.roomId = null;
|
|
108
|
+
self._lastSequence = 0;
|
|
109
|
+
self._joined = false;
|
|
110
|
+
self._joinPromise = null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Send a game action
|
|
116
|
+
* @param {string} actionType - Type of action (e.g., 'move')
|
|
117
|
+
* @param {object} actionData - Action data
|
|
118
|
+
* @returns {Promise} Resolves when action is processed
|
|
119
|
+
*/
|
|
120
|
+
game.action = function(actionType, actionData) {
|
|
121
|
+
const self = this;
|
|
122
|
+
|
|
123
|
+
if (self.directMode) {
|
|
124
|
+
self._sendDirect(actionType || 'action', actionData || {});
|
|
125
|
+
return Promise.resolve({ success: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (self._useProxy) {
|
|
129
|
+
Usion._post({ type: 'GAME_ACTION', room_id: self.roomId, action_type: actionType, action_data: actionData });
|
|
130
|
+
return Promise.resolve({ success: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new Promise(function(resolve, reject) {
|
|
134
|
+
if (!self.socket || !self.connected) {
|
|
135
|
+
reject(new Error('Not connected'));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
self.socket.emit('game:action', {
|
|
140
|
+
room_id: self.roomId,
|
|
141
|
+
action_type: actionType,
|
|
142
|
+
action_data: actionData
|
|
143
|
+
}, function(response) {
|
|
144
|
+
if (response.error) {
|
|
145
|
+
reject(new Error(response.message || response.error));
|
|
146
|
+
} else {
|
|
147
|
+
if (response.sequence !== undefined) {
|
|
148
|
+
self._lastSequence = response.sequence;
|
|
149
|
+
}
|
|
150
|
+
resolve(response);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Send a real-time game update (fire-and-forget).
|
|
158
|
+
* @param {string} actionType - Type of action
|
|
159
|
+
* @param {object} actionData - Action data
|
|
160
|
+
*/
|
|
161
|
+
game.realtime = function(actionType, actionData) {
|
|
162
|
+
const self = this;
|
|
163
|
+
|
|
164
|
+
if (self.directMode) {
|
|
165
|
+
self._sendDirect('input', {
|
|
166
|
+
action_type: actionType,
|
|
167
|
+
action_data: actionData || {}
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (self._useProxy) {
|
|
173
|
+
Usion._post({ type: 'GAME_REALTIME', room_id: self.roomId, action_type: actionType, action_data: actionData });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!self.socket || !self.connected) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
self.socket.emit('game:realtime', {
|
|
182
|
+
room_id: self.roomId,
|
|
183
|
+
action_type: actionType,
|
|
184
|
+
action_data: actionData
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Request game state sync (for reconnection)
|
|
190
|
+
* @param {number} lastSequence - Last known sequence number
|
|
191
|
+
*/
|
|
192
|
+
game.requestSync = function(lastSequence) {
|
|
193
|
+
const self = this;
|
|
194
|
+
lastSequence = lastSequence !== undefined ? lastSequence : self._lastSequence;
|
|
195
|
+
|
|
196
|
+
if (self.directMode) {
|
|
197
|
+
self._sendDirect('ping', { last_sequence: lastSequence || 0 });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (self._useProxy && self.roomId) {
|
|
202
|
+
Usion._post({ type: 'GAME_SYNC_REQUEST', room_id: self.roomId, last_sequence: lastSequence || 0 });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (self.socket && self.connected && self.roomId) {
|
|
207
|
+
self.socket.emit('game:sync_request', {
|
|
208
|
+
room_id: self.roomId,
|
|
209
|
+
last_sequence: lastSequence || 0
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Request a rematch
|
|
216
|
+
*/
|
|
217
|
+
game.requestRematch = function() {
|
|
218
|
+
const self = this;
|
|
219
|
+
|
|
220
|
+
if (self.directMode) {
|
|
221
|
+
self._sendDirect('rematch', {});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (self._useProxy && self.roomId) {
|
|
226
|
+
Usion._post({ type: 'GAME_REMATCH', room_id: self.roomId });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (self.socket && self.connected && self.roomId) {
|
|
231
|
+
self.socket.emit('game:rematch', { room_id: self.roomId });
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Forfeit the current game
|
|
237
|
+
* @returns {Promise}
|
|
238
|
+
*/
|
|
239
|
+
game.forfeit = function() {
|
|
240
|
+
const self = this;
|
|
241
|
+
|
|
242
|
+
if (self.directMode) {
|
|
243
|
+
self._sendDirect('forfeit', {});
|
|
244
|
+
return Promise.resolve({ success: true });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (self._useProxy) {
|
|
248
|
+
Usion._post({ type: 'GAME_FORFEIT', room_id: self.roomId });
|
|
249
|
+
return Promise.resolve({ success: true });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return new Promise(function(resolve, reject) {
|
|
253
|
+
if (!self.socket || !self.connected) {
|
|
254
|
+
reject(new Error('Not connected'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
self.socket.emit('game:forfeit', { room_id: self.roomId }, function(response) {
|
|
259
|
+
if (response.error) {
|
|
260
|
+
reject(new Error(response.message || response.error));
|
|
261
|
+
} else {
|
|
262
|
+
resolve(response);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Disconnect from the game socket
|
|
270
|
+
*/
|
|
271
|
+
game.disconnect = function() {
|
|
272
|
+
const self = this;
|
|
273
|
+
|
|
274
|
+
// Always clear heartbeat
|
|
275
|
+
if (self._heartbeatInterval) {
|
|
276
|
+
clearInterval(self._heartbeatInterval);
|
|
277
|
+
self._heartbeatInterval = null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (self.directMode) {
|
|
281
|
+
if (self.directSocket) {
|
|
282
|
+
try { self.directSocket.close(); } catch (e) {}
|
|
283
|
+
}
|
|
284
|
+
self.directSocket = null;
|
|
285
|
+
self.connected = false;
|
|
286
|
+
self.roomId = null;
|
|
287
|
+
self._lastSequence = 0;
|
|
288
|
+
self._connecting = false;
|
|
289
|
+
self._connectPromise = null;
|
|
290
|
+
self._joined = false;
|
|
291
|
+
self._joinPromise = null;
|
|
292
|
+
self.directMode = false;
|
|
293
|
+
self.directConfig = null;
|
|
294
|
+
self._directSeq = 0;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (self._useProxy) {
|
|
299
|
+
Usion._post({ type: 'GAME_DISCONNECT' });
|
|
300
|
+
self.connected = false;
|
|
301
|
+
self.roomId = null;
|
|
302
|
+
self._lastSequence = 0;
|
|
303
|
+
self._connecting = false;
|
|
304
|
+
self._connectPromise = null;
|
|
305
|
+
self._joined = false;
|
|
306
|
+
self._joinPromise = null;
|
|
307
|
+
self._useProxy = false;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (self.socket) {
|
|
312
|
+
self.socket.disconnect();
|
|
313
|
+
self.socket = null;
|
|
314
|
+
self.connected = false;
|
|
315
|
+
self.roomId = null;
|
|
316
|
+
self._lastSequence = 0;
|
|
317
|
+
self._connecting = false;
|
|
318
|
+
self._connectPromise = null;
|
|
319
|
+
self._joined = false;
|
|
320
|
+
self._joinPromise = null;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get connection status
|
|
326
|
+
* @returns {boolean}
|
|
327
|
+
*/
|
|
328
|
+
game.isConnected = function() {
|
|
329
|
+
if (this.directMode) {
|
|
330
|
+
return this.connected && this.directSocket && this.directSocket.readyState === WebSocket.OPEN;
|
|
331
|
+
}
|
|
332
|
+
return this.connected && this.socket && this.socket.connected;
|
|
333
|
+
};
|
|
334
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usion SDK Game Proxy — postMessage relay through parent app
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add proxy connection methods to game module
|
|
7
|
+
* @param {object} game - The game module object
|
|
8
|
+
* @param {object} Usion - Reference to the main Usion object
|
|
9
|
+
*/
|
|
10
|
+
export function applyGameProxy(game, Usion) {
|
|
11
|
+
/**
|
|
12
|
+
* Connect via parent app proxy (postMessage relay).
|
|
13
|
+
* Used when mixed content prevents direct socket connection.
|
|
14
|
+
* @private
|
|
15
|
+
*/
|
|
16
|
+
game._connectViaProxy = function() {
|
|
17
|
+
var self = this;
|
|
18
|
+
|
|
19
|
+
if (self._useProxy && self.connected) {
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
self._useProxy = true;
|
|
24
|
+
self._connecting = true;
|
|
25
|
+
self._setupProxyListener();
|
|
26
|
+
|
|
27
|
+
self._connectPromise = new Promise(function(resolve, reject) {
|
|
28
|
+
// Listen for GAME_CONNECTED from parent
|
|
29
|
+
self._proxyConnectResolve = function() {
|
|
30
|
+
self.connected = true;
|
|
31
|
+
self._connecting = false;
|
|
32
|
+
Usion.log('Game socket connected via parent proxy');
|
|
33
|
+
resolve();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Send connect request to parent
|
|
37
|
+
Usion._post({ type: 'GAME_CONNECT' });
|
|
38
|
+
|
|
39
|
+
// Timeout after 10s
|
|
40
|
+
setTimeout(function() {
|
|
41
|
+
if (!self.connected) {
|
|
42
|
+
self._connecting = false;
|
|
43
|
+
reject(new Error('Proxy connection timeout'));
|
|
44
|
+
}
|
|
45
|
+
}, 10000);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return self._connectPromise;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set up message listener for proxy game events from parent.
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
game._setupProxyListener = function() {
|
|
56
|
+
var self = this;
|
|
57
|
+
if (self._proxyListenerSetup) return;
|
|
58
|
+
self._proxyListenerSetup = true;
|
|
59
|
+
|
|
60
|
+
window.addEventListener('message', function(event) {
|
|
61
|
+
var data;
|
|
62
|
+
try {
|
|
63
|
+
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!data || typeof data !== 'object' || !self._useProxy) return;
|
|
68
|
+
|
|
69
|
+
switch (data.type) {
|
|
70
|
+
case 'GAME_CONNECTED':
|
|
71
|
+
if (self._proxyConnectResolve) {
|
|
72
|
+
self._proxyConnectResolve();
|
|
73
|
+
self._proxyConnectResolve = null;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'GAME_CONNECT_ERROR':
|
|
78
|
+
self.connected = false;
|
|
79
|
+
self._connecting = false;
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case 'GAME_JOINED':
|
|
83
|
+
self._joined = true;
|
|
84
|
+
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
85
|
+
if (self._proxyJoinResolve) {
|
|
86
|
+
self._proxyJoinResolve(data);
|
|
87
|
+
self._proxyJoinResolve = null;
|
|
88
|
+
}
|
|
89
|
+
if (self._eventHandlers.joined) self._eventHandlers.joined(data);
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case 'GAME_JOIN_ERROR':
|
|
93
|
+
self._joined = false;
|
|
94
|
+
if (self._proxyJoinReject) {
|
|
95
|
+
self._proxyJoinReject(new Error(data.message || 'Join failed'));
|
|
96
|
+
self._proxyJoinReject = null;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case 'GAME_PLAYER_JOINED':
|
|
101
|
+
if (self._eventHandlers.playerJoined) self._eventHandlers.playerJoined(data);
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case 'GAME_PLAYER_LEFT':
|
|
105
|
+
if (self._eventHandlers.playerLeft) self._eventHandlers.playerLeft(data);
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'GAME_STATE':
|
|
109
|
+
if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
110
|
+
if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'GAME_ACTION_DATA':
|
|
114
|
+
if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
115
|
+
if (self._eventHandlers.action) self._eventHandlers.action(data);
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'GAME_REALTIME_DATA':
|
|
119
|
+
if (self._eventHandlers.realtime) self._eventHandlers.realtime(data);
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case 'GAME_FINISHED':
|
|
123
|
+
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
124
|
+
if (self._eventHandlers.finished) self._eventHandlers.finished(data);
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case 'GAME_ERROR':
|
|
128
|
+
Usion.log('Game error via proxy: ' + (data.message || data.code));
|
|
129
|
+
if (self._eventHandlers.error) self._eventHandlers.error(data);
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'GAME_RESTARTED':
|
|
133
|
+
self._lastSequence = 0;
|
|
134
|
+
if (self._eventHandlers.restarted) self._eventHandlers.restarted(data);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'GAME_REMATCH_REQUEST':
|
|
138
|
+
if (self._eventHandlers.rematchRequest) self._eventHandlers.rematchRequest(data);
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'GAME_SYNC':
|
|
142
|
+
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
143
|
+
if (self._eventHandlers.sync) self._eventHandlers.sync(data);
|
|
144
|
+
if (self._eventHandlers.stateUpdate) self._eventHandlers.stateUpdate(data);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
}
|