@usions/sdk 2.11.0 → 2.12.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 +36 -0
- package/package.json +15 -7
- package/src/browser.js +472 -122
- package/src/modules/core.js +1 -1
- package/src/modules/errors.js +75 -0
- package/src/modules/game-core.js +105 -19
- package/src/modules/game-direct.js +12 -16
- package/src/modules/game-methods.js +161 -21
- package/src/modules/game-proxy.js +46 -17
- package/src/modules/game-socket.js +43 -47
- package/src/modules/index.js +4 -0
- package/src/modules/misc.js +20 -0
- package/src/modules/wallet.js +7 -1
- package/types/index.d.ts +162 -20
package/src/browser.js
CHANGED
|
@@ -69,7 +69,7 @@ var Usion = (function () {
|
|
|
69
69
|
* Core Usion object with init, _post, _request
|
|
70
70
|
*/
|
|
71
71
|
const core = {
|
|
72
|
-
version: '2.
|
|
72
|
+
version: '2.12.0', // injected from package.json at build
|
|
73
73
|
config: {},
|
|
74
74
|
_initialized: false,
|
|
75
75
|
_initCallback: null,
|
|
@@ -510,7 +510,13 @@ var Usion = (function () {
|
|
|
510
510
|
* @param {function} callback - Called with new balance
|
|
511
511
|
*/
|
|
512
512
|
onBalanceChange: function(callback) {
|
|
513
|
-
|
|
513
|
+
const self = this;
|
|
514
|
+
self._balanceChangeHandler = callback;
|
|
515
|
+
return function() {
|
|
516
|
+
if (self._balanceChangeHandler === callback) {
|
|
517
|
+
self._balanceChangeHandler = null;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
514
520
|
}
|
|
515
521
|
};
|
|
516
522
|
}
|
|
@@ -855,6 +861,26 @@ var Usion = (function () {
|
|
|
855
861
|
return this.wallet.requestPayment(amount, reason, data);
|
|
856
862
|
},
|
|
857
863
|
|
|
864
|
+
/**
|
|
865
|
+
* Live SDK diagnostics snapshot: version, transport, connection and
|
|
866
|
+
* sequence state. Surfaced automatically in the platform debug overlay
|
|
867
|
+
* (game.debug attaches it as _diag) and useful in bug reports.
|
|
868
|
+
*/
|
|
869
|
+
diagnostics: function() {
|
|
870
|
+
const game = this.game || {};
|
|
871
|
+
return {
|
|
872
|
+
version: this.version,
|
|
873
|
+
transport: game.directMode ? 'direct' : (game._useProxy ? 'proxy' : (game.socket ? 'socket' : 'none')),
|
|
874
|
+
connected: !!game.connected,
|
|
875
|
+
joined: !!game._joined,
|
|
876
|
+
roomId: game.roomId || null,
|
|
877
|
+
playerId: game.playerId || null,
|
|
878
|
+
lastSequence: game._lastSequence || 0,
|
|
879
|
+
lastActionApplied: game._lastActionApplied || 0,
|
|
880
|
+
rejoining: !!game._rejoinPromise,
|
|
881
|
+
};
|
|
882
|
+
},
|
|
883
|
+
|
|
858
884
|
/**
|
|
859
885
|
* Submit result and signal completion
|
|
860
886
|
* @param {object} data - Result data to send to parent
|
|
@@ -1060,9 +1086,7 @@ var Usion = (function () {
|
|
|
1060
1086
|
self._connecting = false;
|
|
1061
1087
|
self.connected = false;
|
|
1062
1088
|
self.directMode = false;
|
|
1063
|
-
|
|
1064
|
-
self._eventHandlers.connectionError(err);
|
|
1065
|
-
}
|
|
1089
|
+
self._dispatch('connectionError', err);
|
|
1066
1090
|
throw err;
|
|
1067
1091
|
});
|
|
1068
1092
|
return self._connectPromise;
|
|
@@ -1171,9 +1195,7 @@ var Usion = (function () {
|
|
|
1171
1195
|
clearInterval(self._heartbeatInterval);
|
|
1172
1196
|
self._heartbeatInterval = null;
|
|
1173
1197
|
}
|
|
1174
|
-
|
|
1175
|
-
self._eventHandlers.disconnect(evt && evt.reason ? evt.reason : 'direct socket closed');
|
|
1176
|
-
}
|
|
1198
|
+
self._dispatch('disconnect', evt && evt.reason ? evt.reason : 'direct socket closed');
|
|
1177
1199
|
// Seamless resume: if the drop wasn't an intentional disconnect()
|
|
1178
1200
|
// (which clears directMode), transparently re-establish + re-join +
|
|
1179
1201
|
// resync from the last sequence.
|
|
@@ -1207,7 +1229,7 @@ var Usion = (function () {
|
|
|
1207
1229
|
self.connected = true;
|
|
1208
1230
|
self._reconnecting = false;
|
|
1209
1231
|
self._reconnectAttempt = 0;
|
|
1210
|
-
|
|
1232
|
+
self._dispatch('reconnect', attempt);
|
|
1211
1233
|
if (self.roomId) self.requestSync(self._lastSequence || 0); // resync / resume
|
|
1212
1234
|
})
|
|
1213
1235
|
.catch(function() {
|
|
@@ -1245,20 +1267,20 @@ var Usion = (function () {
|
|
|
1245
1267
|
|
|
1246
1268
|
if (data.type === 'joined') {
|
|
1247
1269
|
this._joined = true;
|
|
1248
|
-
|
|
1270
|
+
this._dispatch('joined', payload);
|
|
1249
1271
|
return;
|
|
1250
1272
|
}
|
|
1251
1273
|
if (data.type === 'player_joined') {
|
|
1252
|
-
|
|
1274
|
+
this._dispatch('playerJoined', payload);
|
|
1253
1275
|
return;
|
|
1254
1276
|
}
|
|
1255
1277
|
if (data.type === 'player_left') {
|
|
1256
|
-
|
|
1278
|
+
this._dispatch('playerLeft', payload);
|
|
1257
1279
|
return;
|
|
1258
1280
|
}
|
|
1259
1281
|
if (data.type === 'state_snapshot' || data.type === 'state_delta') {
|
|
1260
|
-
|
|
1261
|
-
|
|
1282
|
+
this._dispatch('realtime', payload);
|
|
1283
|
+
this._dispatch('stateUpdate', payload);
|
|
1262
1284
|
return;
|
|
1263
1285
|
}
|
|
1264
1286
|
if (data.type === 'pong') {
|
|
@@ -1267,15 +1289,15 @@ var Usion = (function () {
|
|
|
1267
1289
|
const waiter = this._pongWaiters.shift();
|
|
1268
1290
|
if (waiter) waiter();
|
|
1269
1291
|
}
|
|
1270
|
-
|
|
1292
|
+
this._dispatch('sync', payload);
|
|
1271
1293
|
return;
|
|
1272
1294
|
}
|
|
1273
1295
|
if (data.type === 'match_end') {
|
|
1274
|
-
|
|
1296
|
+
this._dispatch('finished', payload);
|
|
1275
1297
|
return;
|
|
1276
1298
|
}
|
|
1277
|
-
if (data.type === 'error'
|
|
1278
|
-
this.
|
|
1299
|
+
if (data.type === 'error') {
|
|
1300
|
+
this._dispatch('error', payload);
|
|
1279
1301
|
}
|
|
1280
1302
|
};
|
|
1281
1303
|
}
|
|
@@ -1339,17 +1361,27 @@ var Usion = (function () {
|
|
|
1339
1361
|
}
|
|
1340
1362
|
}, 25000);
|
|
1341
1363
|
|
|
1342
|
-
// Re-join room after reconnect
|
|
1364
|
+
// Re-join room after reconnect. The promise is kept on
|
|
1365
|
+
// _rejoinPromise so action() can gate sends until the room
|
|
1366
|
+
// membership and sync are restored — otherwise a stale move could
|
|
1367
|
+
// go out before the client has caught up.
|
|
1343
1368
|
if (self.roomId) {
|
|
1344
1369
|
self._joined = false;
|
|
1345
1370
|
self._joinPromise = null;
|
|
1346
|
-
self.join(self.roomId)
|
|
1371
|
+
self._rejoinPromise = self.join(self.roomId)
|
|
1347
1372
|
.then(function() {
|
|
1348
1373
|
Usion.log('Reconnected - joined room ' + self.roomId);
|
|
1349
1374
|
self.requestSync(self._lastSequence || 0);
|
|
1350
1375
|
})
|
|
1351
1376
|
.catch(function(err) {
|
|
1352
1377
|
Usion.log('Rejoin failed: ' + (err && err.message ? err.message : String(err)));
|
|
1378
|
+
})
|
|
1379
|
+
.then(function() {
|
|
1380
|
+
self._rejoinPromise = null;
|
|
1381
|
+
// Send any moves queued while offline, now that membership
|
|
1382
|
+
// and sync are restored. (Socket.IO v4 emits 'reconnect' on
|
|
1383
|
+
// the Manager, not the Socket, so this is the reliable hook.)
|
|
1384
|
+
if (self._flushOfflineQueue) self._flushOfflineQueue();
|
|
1353
1385
|
});
|
|
1354
1386
|
}
|
|
1355
1387
|
|
|
@@ -1359,9 +1391,7 @@ var Usion = (function () {
|
|
|
1359
1391
|
self.socket.on('connect_error', function(err) {
|
|
1360
1392
|
self._connecting = false;
|
|
1361
1393
|
Usion.log('Game socket error: ' + err.message);
|
|
1362
|
-
|
|
1363
|
-
self._eventHandlers.connectionError(err);
|
|
1364
|
-
}
|
|
1394
|
+
self._dispatch('connectionError', err);
|
|
1365
1395
|
reject(err);
|
|
1366
1396
|
});
|
|
1367
1397
|
|
|
@@ -1374,104 +1404,92 @@ var Usion = (function () {
|
|
|
1374
1404
|
self._heartbeatInterval = null;
|
|
1375
1405
|
}
|
|
1376
1406
|
Usion.log('Game socket disconnected: ' + reason);
|
|
1377
|
-
|
|
1378
|
-
self._eventHandlers.disconnect(reason);
|
|
1379
|
-
}
|
|
1407
|
+
self._dispatch('disconnect', reason);
|
|
1380
1408
|
});
|
|
1381
1409
|
|
|
1382
1410
|
self.socket.on('reconnect', function(attemptNumber) {
|
|
1383
1411
|
Usion.log('Game socket reconnected after ' + attemptNumber + ' attempts');
|
|
1384
|
-
|
|
1385
|
-
self._eventHandlers.reconnect(attemptNumber);
|
|
1386
|
-
}
|
|
1412
|
+
self._dispatch('reconnect', attemptNumber);
|
|
1387
1413
|
});
|
|
1388
1414
|
|
|
1389
1415
|
// Game event handlers
|
|
1390
1416
|
self.socket.on('game:joined', function(data) {
|
|
1391
1417
|
if (data.sequence !== undefined) {
|
|
1392
1418
|
self._lastSequence = data.sequence;
|
|
1419
|
+
// Everything up to this sequence is reflected in the joined
|
|
1420
|
+
// state — actions at or below it must not be re-delivered.
|
|
1421
|
+
self._lastActionApplied = Math.max(self._lastActionApplied, data.sequence);
|
|
1393
1422
|
}
|
|
1394
|
-
|
|
1395
|
-
self._eventHandlers.joined(data);
|
|
1396
|
-
}
|
|
1423
|
+
self._dispatch('joined', data);
|
|
1397
1424
|
});
|
|
1398
1425
|
|
|
1399
1426
|
self.socket.on('game:player_joined', function(data) {
|
|
1400
|
-
|
|
1401
|
-
self._eventHandlers.playerJoined(data);
|
|
1402
|
-
}
|
|
1427
|
+
self._dispatch('playerJoined', data);
|
|
1403
1428
|
});
|
|
1404
1429
|
|
|
1405
1430
|
self.socket.on('game:player_left', function(data) {
|
|
1406
|
-
|
|
1407
|
-
self._eventHandlers.playerLeft(data);
|
|
1408
|
-
}
|
|
1431
|
+
self._dispatch('playerLeft', data);
|
|
1409
1432
|
});
|
|
1410
1433
|
|
|
1411
1434
|
self.socket.on('game:state', function(data) {
|
|
1412
1435
|
if (data.sequence !== undefined) {
|
|
1413
1436
|
self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1414
1437
|
}
|
|
1415
|
-
|
|
1416
|
-
self._eventHandlers.stateUpdate(data);
|
|
1417
|
-
}
|
|
1438
|
+
self._dispatch('stateUpdate', data);
|
|
1418
1439
|
});
|
|
1419
1440
|
|
|
1420
1441
|
self.socket.on('game:sync', function(data) {
|
|
1421
1442
|
if (data.sequence !== undefined) {
|
|
1422
1443
|
self._lastSequence = data.sequence;
|
|
1444
|
+
// The sync payload carries all actions up to this sequence; a
|
|
1445
|
+
// live echo of one of them must not be applied a second time.
|
|
1446
|
+
self._lastActionApplied = Math.max(self._lastActionApplied, data.sequence);
|
|
1423
1447
|
}
|
|
1424
|
-
|
|
1425
|
-
self._eventHandlers.sync(data);
|
|
1426
|
-
}
|
|
1448
|
+
self._dispatch('sync', data);
|
|
1427
1449
|
// Also trigger stateUpdate for backwards compat
|
|
1428
|
-
|
|
1429
|
-
self._eventHandlers.stateUpdate(data);
|
|
1430
|
-
}
|
|
1450
|
+
self._dispatch('stateUpdate', data);
|
|
1431
1451
|
});
|
|
1432
1452
|
|
|
1433
1453
|
self.socket.on('game:action', function(data) {
|
|
1434
1454
|
if (data.sequence !== undefined) {
|
|
1455
|
+
// Drop duplicates: actions are delivered exactly once per
|
|
1456
|
+
// sequence, whether they arrive live, as the sender's own echo,
|
|
1457
|
+
// or replayed after a reconnect.
|
|
1458
|
+
if (data.sequence <= self._lastActionApplied) return;
|
|
1459
|
+
self._lastActionApplied = data.sequence;
|
|
1435
1460
|
self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1436
1461
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1462
|
+
self._dispatch('action', data);
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
self.socket.on('game:player_connection', function(data) {
|
|
1466
|
+
self._dispatch('playerConnection', data);
|
|
1440
1467
|
});
|
|
1441
1468
|
|
|
1442
1469
|
self.socket.on('game:realtime', function(data) {
|
|
1443
|
-
|
|
1444
|
-
self._eventHandlers.realtime(data);
|
|
1445
|
-
}
|
|
1470
|
+
self._dispatch('realtime', data);
|
|
1446
1471
|
});
|
|
1447
1472
|
|
|
1448
1473
|
self.socket.on('game:finished', function(data) {
|
|
1449
1474
|
if (data.sequence !== undefined) {
|
|
1450
1475
|
self._lastSequence = data.sequence;
|
|
1451
1476
|
}
|
|
1452
|
-
|
|
1453
|
-
self._eventHandlers.finished(data);
|
|
1454
|
-
}
|
|
1477
|
+
self._dispatch('finished', data);
|
|
1455
1478
|
});
|
|
1456
1479
|
|
|
1457
1480
|
self.socket.on('game:error', function(data) {
|
|
1458
1481
|
Usion.log('Game error: ' + (data.message || data.code));
|
|
1459
|
-
|
|
1460
|
-
self._eventHandlers.error(data);
|
|
1461
|
-
}
|
|
1482
|
+
self._dispatch('error', data);
|
|
1462
1483
|
});
|
|
1463
1484
|
|
|
1464
1485
|
self.socket.on('game:rematch_request', function(data) {
|
|
1465
|
-
|
|
1466
|
-
self._eventHandlers.rematchRequest(data);
|
|
1467
|
-
}
|
|
1486
|
+
self._dispatch('rematchRequest', data);
|
|
1468
1487
|
});
|
|
1469
1488
|
|
|
1470
1489
|
self.socket.on('game:restarted', function(data) {
|
|
1471
1490
|
self._lastSequence = 0; // Reset sequence on rematch
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
1491
|
+
self._lastActionApplied = 0;
|
|
1492
|
+
self._dispatch('restarted', data);
|
|
1475
1493
|
});
|
|
1476
1494
|
|
|
1477
1495
|
} catch (err) {
|
|
@@ -1480,6 +1498,82 @@ var Usion = (function () {
|
|
|
1480
1498
|
};
|
|
1481
1499
|
}
|
|
1482
1500
|
|
|
1501
|
+
/**
|
|
1502
|
+
* Usion SDK Errors — stable, machine-readable error codes.
|
|
1503
|
+
*
|
|
1504
|
+
* Developers should branch on `err.code`, never on message text. Messages
|
|
1505
|
+
* are human-readable and may change; codes are part of the public API and
|
|
1506
|
+
* follow the deprecation policy (never removed within a major version).
|
|
1507
|
+
*/
|
|
1508
|
+
|
|
1509
|
+
/** @type {Record<string, string>} */
|
|
1510
|
+
const ERROR_CODES = {
|
|
1511
|
+
NOT_CONNECTED: 'NOT_CONNECTED', // No live connection for this call
|
|
1512
|
+
NO_ROOM: 'NO_ROOM', // No room id provided/known
|
|
1513
|
+
ROOM_NOT_FOUND: 'ROOM_NOT_FOUND', // Room does not exist server-side
|
|
1514
|
+
NOT_PARTICIPANT: 'NOT_PARTICIPANT', // Caller is not a player in the room
|
|
1515
|
+
NOT_AUTHORITY: 'NOT_AUTHORITY', // Authority-only call (e.g. setState)
|
|
1516
|
+
NOT_AUTHENTICATED: 'NOT_AUTHENTICATED', // Missing/invalid auth server-side
|
|
1517
|
+
JOIN_TIMEOUT: 'JOIN_TIMEOUT', // Join did not complete in time
|
|
1518
|
+
CONNECT_TIMEOUT: 'CONNECT_TIMEOUT', // Connect did not complete in time
|
|
1519
|
+
STATE_TOO_LARGE: 'STATE_TOO_LARGE', // setState payload over the quota
|
|
1520
|
+
INVALID_STATE: 'INVALID_STATE', // setState payload not a JSON object
|
|
1521
|
+
INVALID_NEXT_TURN: 'INVALID_NEXT_TURN', // nextTurn is not a player in the room
|
|
1522
|
+
RATE_LIMITED: 'RATE_LIMITED', // Too many calls; back off
|
|
1523
|
+
REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', // Host/parent did not reply in time
|
|
1524
|
+
QUEUE_FULL: 'QUEUE_FULL', // Offline action queue at capacity
|
|
1525
|
+
UNSUPPORTED: 'UNSUPPORTED', // Not available in this transport
|
|
1526
|
+
UNKNOWN: 'UNKNOWN', // Unmapped error (see message)
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
class UsionError extends Error {
|
|
1530
|
+
/**
|
|
1531
|
+
* @param {string} code - One of ERROR_CODES
|
|
1532
|
+
* @param {string} [message] - Human-readable detail (may change between versions)
|
|
1533
|
+
*/
|
|
1534
|
+
constructor(code, message) {
|
|
1535
|
+
super(message || code);
|
|
1536
|
+
this.name = 'UsionError';
|
|
1537
|
+
this.code = ERROR_CODES[code] ? code : ERROR_CODES.UNKNOWN;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Backend error strings → stable codes. Order matters: first match wins.
|
|
1542
|
+
/** @type {Array<[RegExp, string]>} */
|
|
1543
|
+
const BACKEND_PATTERNS = [
|
|
1544
|
+
[/not authenticated/i, ERROR_CODES.NOT_AUTHENTICATED],
|
|
1545
|
+
[/room_id required|no room id/i, ERROR_CODES.NO_ROOM],
|
|
1546
|
+
[/room not found/i, ERROR_CODES.ROOM_NOT_FOUND],
|
|
1547
|
+
[/not a participant/i, ERROR_CODES.NOT_PARTICIPANT],
|
|
1548
|
+
[/room authority/i, ERROR_CODES.NOT_AUTHORITY],
|
|
1549
|
+
[/exceeds .*limit/i, ERROR_CODES.STATE_TOO_LARGE],
|
|
1550
|
+
[/state must be/i, ERROR_CODES.INVALID_STATE],
|
|
1551
|
+
[/next_turn must be/i, ERROR_CODES.INVALID_NEXT_TURN],
|
|
1552
|
+
[/rate limit|too many/i, ERROR_CODES.RATE_LIMITED],
|
|
1553
|
+
[/join timeout/i, ERROR_CODES.JOIN_TIMEOUT],
|
|
1554
|
+
[/connection timeout|connect timeout/i, ERROR_CODES.CONNECT_TIMEOUT],
|
|
1555
|
+
[/request timeout/i, ERROR_CODES.REQUEST_TIMEOUT],
|
|
1556
|
+
[/not connected/i, ERROR_CODES.NOT_CONNECTED],
|
|
1557
|
+
];
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* Normalize anything (backend `{error}` string, Error, raw string) into a
|
|
1561
|
+
* UsionError with the best-matching stable code.
|
|
1562
|
+
* @param {*} err
|
|
1563
|
+
* @param {string} [fallbackCode] - Code to use when nothing matches
|
|
1564
|
+
* @returns {UsionError}
|
|
1565
|
+
*/
|
|
1566
|
+
function toUsionError(err, fallbackCode) {
|
|
1567
|
+
if (err instanceof UsionError) return err;
|
|
1568
|
+
const message = err && err.message ? err.message : String(err || 'Unknown error');
|
|
1569
|
+
for (let i = 0; i < BACKEND_PATTERNS.length; i++) {
|
|
1570
|
+
if (BACKEND_PATTERNS[i][0].test(message)) {
|
|
1571
|
+
return new UsionError(BACKEND_PATTERNS[i][1], message);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return new UsionError(fallbackCode || ERROR_CODES.UNKNOWN, message);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1483
1577
|
/**
|
|
1484
1578
|
* Usion SDK Game Proxy — postMessage relay through parent app
|
|
1485
1579
|
*/
|
|
@@ -1523,7 +1617,7 @@ var Usion = (function () {
|
|
|
1523
1617
|
setTimeout(function() {
|
|
1524
1618
|
if (!self.connected) {
|
|
1525
1619
|
self._connecting = false;
|
|
1526
|
-
reject(new
|
|
1620
|
+
reject(new UsionError(ERROR_CODES.CONNECT_TIMEOUT, 'Proxy connection timeout'));
|
|
1527
1621
|
}
|
|
1528
1622
|
}, 10000);
|
|
1529
1623
|
});
|
|
@@ -1564,69 +1658,97 @@ var Usion = (function () {
|
|
|
1564
1658
|
self._connecting = false;
|
|
1565
1659
|
break;
|
|
1566
1660
|
|
|
1661
|
+
case 'GAME_DISCONNECTED':
|
|
1662
|
+
// The host app's socket dropped; it will rejoin and resync for us.
|
|
1663
|
+
self.connected = false;
|
|
1664
|
+
self._dispatch('disconnect', data.reason || 'transport');
|
|
1665
|
+
break;
|
|
1666
|
+
|
|
1667
|
+
case 'GAME_RECONNECTED':
|
|
1668
|
+
self.connected = true;
|
|
1669
|
+
self._dispatch('reconnect', data.attempts || 1);
|
|
1670
|
+
break;
|
|
1671
|
+
|
|
1672
|
+
case 'GAME_PLAYER_CONNECTION':
|
|
1673
|
+
self._dispatch('playerConnection', data);
|
|
1674
|
+
break;
|
|
1675
|
+
|
|
1567
1676
|
case 'GAME_JOINED':
|
|
1568
1677
|
self._joined = true;
|
|
1569
|
-
if (data.sequence !== undefined)
|
|
1678
|
+
if (data.sequence !== undefined) {
|
|
1679
|
+
self._lastSequence = data.sequence;
|
|
1680
|
+
self._lastActionApplied = Math.max(self._lastActionApplied, data.sequence);
|
|
1681
|
+
}
|
|
1570
1682
|
if (self._proxyJoinResolve) {
|
|
1571
1683
|
self._proxyJoinResolve(data);
|
|
1572
1684
|
self._proxyJoinResolve = null;
|
|
1573
1685
|
}
|
|
1574
|
-
|
|
1686
|
+
self._dispatch('joined', data);
|
|
1575
1687
|
break;
|
|
1576
1688
|
|
|
1577
1689
|
case 'GAME_JOIN_ERROR':
|
|
1578
1690
|
self._joined = false;
|
|
1579
1691
|
if (self._proxyJoinReject) {
|
|
1580
|
-
self._proxyJoinReject(
|
|
1692
|
+
self._proxyJoinReject(toUsionError(data.message || 'Join failed'));
|
|
1581
1693
|
self._proxyJoinReject = null;
|
|
1582
1694
|
}
|
|
1583
1695
|
break;
|
|
1584
1696
|
|
|
1585
1697
|
case 'GAME_PLAYER_JOINED':
|
|
1586
|
-
|
|
1698
|
+
self._dispatch('playerJoined', data);
|
|
1587
1699
|
break;
|
|
1588
1700
|
|
|
1589
1701
|
case 'GAME_PLAYER_LEFT':
|
|
1590
|
-
|
|
1702
|
+
self._dispatch('playerLeft', data);
|
|
1591
1703
|
break;
|
|
1592
1704
|
|
|
1593
1705
|
case 'GAME_STATE':
|
|
1594
1706
|
if (data.sequence !== undefined) self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1595
|
-
|
|
1707
|
+
self._dispatch('stateUpdate', data);
|
|
1596
1708
|
break;
|
|
1597
1709
|
|
|
1598
1710
|
case 'GAME_ACTION_DATA':
|
|
1599
|
-
if (data.sequence !== undefined)
|
|
1600
|
-
|
|
1711
|
+
if (data.sequence !== undefined) {
|
|
1712
|
+
// Drop duplicates: actions are delivered exactly once per
|
|
1713
|
+
// sequence (live, sender echo, or replay after reconnect).
|
|
1714
|
+
if (data.sequence <= self._lastActionApplied) break;
|
|
1715
|
+
self._lastActionApplied = data.sequence;
|
|
1716
|
+
self._lastSequence = Math.max(self._lastSequence, data.sequence);
|
|
1717
|
+
}
|
|
1718
|
+
self._dispatch('action', data);
|
|
1601
1719
|
break;
|
|
1602
1720
|
|
|
1603
1721
|
case 'GAME_REALTIME_DATA':
|
|
1604
|
-
|
|
1722
|
+
self._dispatch('realtime', data);
|
|
1605
1723
|
break;
|
|
1606
1724
|
|
|
1607
1725
|
case 'GAME_FINISHED':
|
|
1608
1726
|
if (data.sequence !== undefined) self._lastSequence = data.sequence;
|
|
1609
|
-
|
|
1727
|
+
self._dispatch('finished', data);
|
|
1610
1728
|
break;
|
|
1611
1729
|
|
|
1612
1730
|
case 'GAME_ERROR':
|
|
1613
1731
|
Usion.log('Game error via proxy: ' + (data.message || data.code));
|
|
1614
|
-
|
|
1732
|
+
self._dispatch('error', data);
|
|
1615
1733
|
break;
|
|
1616
1734
|
|
|
1617
1735
|
case 'GAME_RESTARTED':
|
|
1618
1736
|
self._lastSequence = 0;
|
|
1619
|
-
|
|
1737
|
+
self._lastActionApplied = 0;
|
|
1738
|
+
self._dispatch('restarted', data);
|
|
1620
1739
|
break;
|
|
1621
1740
|
|
|
1622
1741
|
case 'GAME_REMATCH_REQUEST':
|
|
1623
|
-
|
|
1742
|
+
self._dispatch('rematchRequest', data);
|
|
1624
1743
|
break;
|
|
1625
1744
|
|
|
1626
1745
|
case 'GAME_SYNC':
|
|
1627
|
-
if (data.sequence !== undefined)
|
|
1628
|
-
|
|
1629
|
-
|
|
1746
|
+
if (data.sequence !== undefined) {
|
|
1747
|
+
self._lastSequence = data.sequence;
|
|
1748
|
+
self._lastActionApplied = Math.max(self._lastActionApplied, data.sequence);
|
|
1749
|
+
}
|
|
1750
|
+
self._dispatch('sync', data);
|
|
1751
|
+
self._dispatch('stateUpdate', data);
|
|
1630
1752
|
break;
|
|
1631
1753
|
}
|
|
1632
1754
|
});
|
|
@@ -1637,6 +1759,7 @@ var Usion = (function () {
|
|
|
1637
1759
|
* Usion SDK Game Methods — join, leave, action, realtime, sync, etc.
|
|
1638
1760
|
*/
|
|
1639
1761
|
|
|
1762
|
+
|
|
1640
1763
|
/**
|
|
1641
1764
|
* Add game action methods to game module
|
|
1642
1765
|
* @param {object} game - The game module object
|
|
@@ -1678,7 +1801,7 @@ var Usion = (function () {
|
|
|
1678
1801
|
setTimeout(function() {
|
|
1679
1802
|
if (!self._joined && self._proxyJoinReject) {
|
|
1680
1803
|
self._proxyJoinReject = null;
|
|
1681
|
-
reject(new
|
|
1804
|
+
reject(new UsionError(ERROR_CODES.JOIN_TIMEOUT, 'Join timeout'));
|
|
1682
1805
|
}
|
|
1683
1806
|
}, 15000);
|
|
1684
1807
|
});
|
|
@@ -1687,19 +1810,19 @@ var Usion = (function () {
|
|
|
1687
1810
|
|
|
1688
1811
|
self._joinPromise = new Promise(function(resolve, reject) {
|
|
1689
1812
|
if (!self.socket || !self.connected) {
|
|
1690
|
-
reject(new
|
|
1813
|
+
reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
|
|
1691
1814
|
return;
|
|
1692
1815
|
}
|
|
1693
1816
|
|
|
1694
1817
|
if (!roomId) {
|
|
1695
|
-
reject(new
|
|
1818
|
+
reject(new UsionError(ERROR_CODES.NO_ROOM, 'No room ID provided'));
|
|
1696
1819
|
return;
|
|
1697
1820
|
}
|
|
1698
1821
|
|
|
1699
1822
|
self.socket.emit('game:join', { room_id: roomId }, function(response) {
|
|
1700
1823
|
if (response.error) {
|
|
1701
1824
|
self._joined = false;
|
|
1702
|
-
reject(
|
|
1825
|
+
reject(toUsionError(response.message || response.error));
|
|
1703
1826
|
} else {
|
|
1704
1827
|
self._joined = true;
|
|
1705
1828
|
if (response.sequence !== undefined) {
|
|
@@ -1747,13 +1870,25 @@ var Usion = (function () {
|
|
|
1747
1870
|
};
|
|
1748
1871
|
|
|
1749
1872
|
/**
|
|
1750
|
-
* Send a game action
|
|
1873
|
+
* Send a game action.
|
|
1874
|
+
*
|
|
1875
|
+
* RELIABILITY CONTRACT: the platform echoes every action back to the
|
|
1876
|
+
* sender (with the authoritative sequence number). Apply game state ONLY
|
|
1877
|
+
* in onAction — never optimistically on send — so every client applies
|
|
1878
|
+
* the same actions in the same order. The SDK deduplicates by sequence,
|
|
1879
|
+
* so an action is delivered exactly once even across reconnect replays.
|
|
1880
|
+
*
|
|
1751
1881
|
* @param {string} actionType - Type of action (e.g., 'move')
|
|
1752
1882
|
* @param {object} actionData - Action data
|
|
1883
|
+
* @param {object} [opts] - Options. opts.nextTurn: player ID whose turn
|
|
1884
|
+
* is next — the server remembers it and hands it to any (re)joining
|
|
1885
|
+
* client as current_turn, so turn state survives reconnects.
|
|
1753
1886
|
* @returns {Promise} Resolves when action is processed
|
|
1754
1887
|
*/
|
|
1755
|
-
game.action = function(actionType, actionData) {
|
|
1888
|
+
game.action = function(actionType, actionData, opts) {
|
|
1756
1889
|
const self = this;
|
|
1890
|
+
const nextTurn = opts && opts.nextTurn;
|
|
1891
|
+
const queueOffline = !!(opts && opts.queueOffline);
|
|
1757
1892
|
|
|
1758
1893
|
if (self.directMode) {
|
|
1759
1894
|
self._sendDirect('action', {
|
|
@@ -1763,29 +1898,153 @@ var Usion = (function () {
|
|
|
1763
1898
|
return Promise.resolve({ success: true });
|
|
1764
1899
|
}
|
|
1765
1900
|
|
|
1901
|
+
// Opt-in offline queue: instead of failing while disconnected, hold
|
|
1902
|
+
// the move and send it (in order) once the connection recovers.
|
|
1903
|
+
// Turn-based games get "your move is saved and sends when you're
|
|
1904
|
+
// back" for free; realtime games should NOT use this (stale inputs).
|
|
1905
|
+
if (queueOffline && !self.connected) {
|
|
1906
|
+
return self._queueOfflineAction(actionType, actionData, opts);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1766
1909
|
if (self._useProxy) {
|
|
1767
|
-
|
|
1910
|
+
const proxyMsg = { type: 'GAME_ACTION', room_id: self.roomId, action_type: actionType, action_data: actionData };
|
|
1911
|
+
if (nextTurn) proxyMsg.next_turn = nextTurn;
|
|
1912
|
+
Usion._post(proxyMsg);
|
|
1768
1913
|
return Promise.resolve({ success: true });
|
|
1769
1914
|
}
|
|
1770
1915
|
|
|
1916
|
+
// Gate sends while a post-reconnect rejoin is in flight, so a stale
|
|
1917
|
+
// move can't go out before the client has resynced.
|
|
1918
|
+
const gate = self._rejoinPromise || Promise.resolve();
|
|
1919
|
+
return gate.then(function() {
|
|
1920
|
+
return new Promise(function(resolve, reject) {
|
|
1921
|
+
if (!self.socket || !self.connected) {
|
|
1922
|
+
if (queueOffline) {
|
|
1923
|
+
self._queueOfflineAction(actionType, actionData, opts).then(resolve, reject);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const payload = {
|
|
1931
|
+
room_id: self.roomId,
|
|
1932
|
+
action_type: actionType,
|
|
1933
|
+
action_data: actionData
|
|
1934
|
+
};
|
|
1935
|
+
if (nextTurn) payload.next_turn = nextTurn;
|
|
1936
|
+
self.socket.emit('game:action', payload, function(response) {
|
|
1937
|
+
if (response.error) {
|
|
1938
|
+
reject(toUsionError(response.message || response.error));
|
|
1939
|
+
} else {
|
|
1940
|
+
if (response.sequence !== undefined) {
|
|
1941
|
+
self._lastSequence = response.sequence;
|
|
1942
|
+
}
|
|
1943
|
+
resolve(response);
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
});
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
// ── Offline action queue (opt-in via action(..., { queueOffline: true })) ──
|
|
1951
|
+
|
|
1952
|
+
const OFFLINE_QUEUE_MAX = 20;
|
|
1953
|
+
|
|
1954
|
+
/** @private Hold an action until the connection recovers. */
|
|
1955
|
+
game._queueOfflineAction = function(actionType, actionData, opts) {
|
|
1956
|
+
const self = this;
|
|
1957
|
+
if (!self._offlineQueue) self._offlineQueue = [];
|
|
1958
|
+
if (self._offlineQueue.length >= OFFLINE_QUEUE_MAX) {
|
|
1959
|
+
return Promise.reject(new UsionError(ERROR_CODES.QUEUE_FULL,
|
|
1960
|
+
'Offline action queue is full (' + OFFLINE_QUEUE_MAX + ')'));
|
|
1961
|
+
}
|
|
1962
|
+
Usion.log('Queued action while offline: ' + actionType);
|
|
1963
|
+
return new Promise(function(resolve, reject) {
|
|
1964
|
+
self._offlineQueue.push({
|
|
1965
|
+
actionType: actionType,
|
|
1966
|
+
actionData: actionData,
|
|
1967
|
+
opts: Object.assign({}, opts, { queueOffline: false }),
|
|
1968
|
+
resolve: resolve,
|
|
1969
|
+
reject: reject,
|
|
1970
|
+
});
|
|
1971
|
+
self._ensureOfflineFlushHook();
|
|
1972
|
+
});
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
/** @private Flush queued actions, in order, once reconnected. */
|
|
1976
|
+
game._ensureOfflineFlushHook = function() {
|
|
1977
|
+
const self = this;
|
|
1978
|
+
if (self._offlineFlushHooked) return;
|
|
1979
|
+
self._offlineFlushHooked = true;
|
|
1980
|
+
self.on('reconnect', function() { self._flushOfflineQueue(); });
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1983
|
+
/** @private */
|
|
1984
|
+
game._flushOfflineQueue = function() {
|
|
1985
|
+
const self = this;
|
|
1986
|
+
const queue = self._offlineQueue;
|
|
1987
|
+
if (!queue || !queue.length) return;
|
|
1988
|
+
self._offlineQueue = [];
|
|
1989
|
+
Usion.log('Flushing ' + queue.length + ' queued action(s) after reconnect');
|
|
1990
|
+
// Send strictly in order: each action waits for the previous ack so
|
|
1991
|
+
// the server assigns sequences in the order the player acted.
|
|
1992
|
+
let chain = Promise.resolve();
|
|
1993
|
+
queue.forEach(function(item) {
|
|
1994
|
+
chain = chain.then(function() {
|
|
1995
|
+
return self.action(item.actionType, item.actionData, item.opts)
|
|
1996
|
+
.then(item.resolve, item.reject);
|
|
1997
|
+
});
|
|
1998
|
+
});
|
|
1999
|
+
};
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* Checkpoint the authoritative game state on the server. Any client that
|
|
2003
|
+
* joins or rejoins the room receives the latest checkpoint as game_state
|
|
2004
|
+
* in the join ack and in game:sync — recovery becomes "load checkpoint,
|
|
2005
|
+
* replay the tail" instead of replaying every action from zero.
|
|
2006
|
+
*
|
|
2007
|
+
* Only the room authority (player_ids[0] / host) may call this. The
|
|
2008
|
+
* serialized state is capped at 64 KB.
|
|
2009
|
+
*
|
|
2010
|
+
* @param {*} state - JSON-serializable authoritative game state
|
|
2011
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
2012
|
+
*/
|
|
2013
|
+
game.setState = function(state) {
|
|
2014
|
+
const self = this;
|
|
2015
|
+
|
|
2016
|
+
if (self.directMode) {
|
|
2017
|
+
// Direct-mode game servers own their state; no platform checkpoint.
|
|
2018
|
+
return Promise.resolve({ success: false, error: 'not_supported_in_direct_mode', code: ERROR_CODES.UNSUPPORTED });
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
if (self._useProxy) {
|
|
2022
|
+
return Usion._request('GAME_SET_STATE', { room_id: self.roomId, state: state || {} })
|
|
2023
|
+
.then(function(res) {
|
|
2024
|
+
if (res && res.error) {
|
|
2025
|
+
return { success: false, error: res.error, code: toUsionError(res.error).code };
|
|
2026
|
+
}
|
|
2027
|
+
return res || { success: true };
|
|
2028
|
+
})
|
|
2029
|
+
.catch(function(err) {
|
|
2030
|
+
const ue = toUsionError(err, ERROR_CODES.REQUEST_TIMEOUT);
|
|
2031
|
+
return { success: false, error: ue.message, code: ue.code };
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1771
2035
|
return new Promise(function(resolve, reject) {
|
|
1772
2036
|
if (!self.socket || !self.connected) {
|
|
1773
|
-
reject(new
|
|
2037
|
+
reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
|
|
1774
2038
|
return;
|
|
1775
2039
|
}
|
|
1776
|
-
|
|
1777
|
-
self.socket.emit('game:action', {
|
|
2040
|
+
self.socket.emit('game:set_state', {
|
|
1778
2041
|
room_id: self.roomId,
|
|
1779
|
-
|
|
1780
|
-
action_data: actionData
|
|
2042
|
+
state: state || {}
|
|
1781
2043
|
}, function(response) {
|
|
1782
|
-
if (response.error) {
|
|
1783
|
-
|
|
2044
|
+
if (response && response.error) {
|
|
2045
|
+
resolve({ success: false, error: response.error, code: toUsionError(response.error).code });
|
|
1784
2046
|
} else {
|
|
1785
|
-
|
|
1786
|
-
self._lastSequence = response.sequence;
|
|
1787
|
-
}
|
|
1788
|
-
resolve(response);
|
|
2047
|
+
resolve(response || { success: true });
|
|
1789
2048
|
}
|
|
1790
2049
|
});
|
|
1791
2050
|
});
|
|
@@ -1889,13 +2148,13 @@ var Usion = (function () {
|
|
|
1889
2148
|
|
|
1890
2149
|
return new Promise(function(resolve, reject) {
|
|
1891
2150
|
if (!self.socket || !self.connected) {
|
|
1892
|
-
reject(new
|
|
2151
|
+
reject(new UsionError(ERROR_CODES.NOT_CONNECTED, 'Not connected'));
|
|
1893
2152
|
return;
|
|
1894
2153
|
}
|
|
1895
2154
|
|
|
1896
2155
|
self.socket.emit('game:forfeit', { room_id: self.roomId }, function(response) {
|
|
1897
2156
|
if (response.error) {
|
|
1898
|
-
reject(
|
|
2157
|
+
reject(toUsionError(response.message || response.error));
|
|
1899
2158
|
} else {
|
|
1900
2159
|
resolve(response);
|
|
1901
2160
|
}
|
|
@@ -2057,7 +2316,9 @@ var Usion = (function () {
|
|
|
2057
2316
|
var inFrame = window.parent && window.parent !== window;
|
|
2058
2317
|
var inRNWebView = !!window.ReactNativeWebView;
|
|
2059
2318
|
if (inFrame || inRNWebView) {
|
|
2060
|
-
|
|
2319
|
+
var body = Object.assign({}, payload || {});
|
|
2320
|
+
body._diag = Usion.diagnostics ? Usion.diagnostics() : undefined;
|
|
2321
|
+
Usion._post({ type: 'GAME_DEBUG', payload: body });
|
|
2061
2322
|
}
|
|
2062
2323
|
} catch (e) { /* non-fatal */ }
|
|
2063
2324
|
};
|
|
@@ -4063,6 +4324,36 @@ var Usion = (function () {
|
|
|
4063
4324
|
*/
|
|
4064
4325
|
|
|
4065
4326
|
|
|
4327
|
+
// Map any reasonable spelling of a game event onto the internal handler
|
|
4328
|
+
// name: 'game:player_joined' / 'player_joined' / 'playerJoined' → 'playerJoined'.
|
|
4329
|
+
const _EVENT_ALIASES = {
|
|
4330
|
+
joined: 'joined',
|
|
4331
|
+
player_joined: 'playerJoined',
|
|
4332
|
+
player_left: 'playerLeft',
|
|
4333
|
+
player_connection: 'playerConnection',
|
|
4334
|
+
state: 'stateUpdate',
|
|
4335
|
+
state_update: 'stateUpdate',
|
|
4336
|
+
sync: 'sync',
|
|
4337
|
+
action: 'action',
|
|
4338
|
+
realtime: 'realtime',
|
|
4339
|
+
finished: 'finished',
|
|
4340
|
+
restarted: 'restarted',
|
|
4341
|
+
error: 'error',
|
|
4342
|
+
rematch_request: 'rematchRequest',
|
|
4343
|
+
disconnect: 'disconnect',
|
|
4344
|
+
reconnect: 'reconnect',
|
|
4345
|
+
connection_error: 'connectionError',
|
|
4346
|
+
};
|
|
4347
|
+
|
|
4348
|
+
function _normalizeEventName(event) {
|
|
4349
|
+
let name = String(event || '');
|
|
4350
|
+
if (name.indexOf('game:') === 0) name = name.slice(5);
|
|
4351
|
+
if (_EVENT_ALIASES[name]) return _EVENT_ALIASES[name];
|
|
4352
|
+
// camelCase passthrough ('playerJoined', 'stateUpdate', …)
|
|
4353
|
+
const snake = name.replace(/([A-Z])/g, function(m) { return '_' + m.toLowerCase(); });
|
|
4354
|
+
return _EVENT_ALIASES[snake] || name;
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4066
4357
|
/**
|
|
4067
4358
|
* Create the game module with all sub-modules applied
|
|
4068
4359
|
* @param {object} Usion - Reference to the main Usion object
|
|
@@ -4088,6 +4379,11 @@ var Usion = (function () {
|
|
|
4088
4379
|
_heartbeatInterval: null,
|
|
4089
4380
|
_pingMeter: null,
|
|
4090
4381
|
_pongWaiters: [],
|
|
4382
|
+
// Reliability state: highest action sequence already delivered to the
|
|
4383
|
+
// game (duplicate echoes/replays are dropped), and the in-flight rejoin
|
|
4384
|
+
// promise that gates action() sends right after a reconnect.
|
|
4385
|
+
_lastActionApplied: 0,
|
|
4386
|
+
_rejoinPromise: null,
|
|
4091
4387
|
|
|
4092
4388
|
/**
|
|
4093
4389
|
* Connect to the game socket server
|
|
@@ -4170,34 +4466,85 @@ var Usion = (function () {
|
|
|
4170
4466
|
return self._connectPromise;
|
|
4171
4467
|
},
|
|
4172
4468
|
|
|
4173
|
-
// Event handler registrations
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4469
|
+
// Event handler registrations.
|
|
4470
|
+
//
|
|
4471
|
+
// Each onX(cb) keeps the long-standing "single handler, last one wins"
|
|
4472
|
+
// behavior for back-compat, but now ALSO returns an unsubscribe
|
|
4473
|
+
// function. For multiple listeners use game.on(event, cb), which
|
|
4474
|
+
// supports any number of listeners, works before connect() in every
|
|
4475
|
+
// transport, and returns an unsubscribe function.
|
|
4476
|
+
onJoined: function(callback) { return this._setHandler('joined', callback); },
|
|
4477
|
+
onPlayerJoined: function(callback) { return this._setHandler('playerJoined', callback); },
|
|
4478
|
+
onPlayerLeft: function(callback) { return this._setHandler('playerLeft', callback); },
|
|
4479
|
+
onStateUpdate: function(callback) { return this._setHandler('stateUpdate', callback); },
|
|
4480
|
+
onSync: function(callback) { return this._setHandler('sync', callback); },
|
|
4481
|
+
onAction: function(callback) { return this._setHandler('action', callback); },
|
|
4482
|
+
onRealtime: function(callback) { return this._setHandler('realtime', callback); },
|
|
4483
|
+
onGameFinished: function(callback) { return this._setHandler('finished', callback); },
|
|
4484
|
+
onGameRestarted: function(callback) { return this._setHandler('restarted', callback); },
|
|
4485
|
+
onError: function(callback) { return this._setHandler('error', callback); },
|
|
4486
|
+
onRematchRequest: function(callback) { return this._setHandler('rematchRequest', callback); },
|
|
4487
|
+
onDisconnect: function(callback) { return this._setHandler('disconnect', callback); },
|
|
4488
|
+
onReconnect: function(callback) { return this._setHandler('reconnect', callback); },
|
|
4489
|
+
onConnectionError: function(callback) { return this._setHandler('connectionError', callback); },
|
|
4490
|
+
onPlayerConnection: function(callback) { return this._setHandler('playerConnection', callback); },
|
|
4491
|
+
|
|
4492
|
+
/** @private Set the single legacy handler; returns an unsubscribe fn. */
|
|
4493
|
+
_setHandler: function(name, callback) {
|
|
4494
|
+
const self = this;
|
|
4495
|
+
self._eventHandlers[name] = callback;
|
|
4496
|
+
return function() {
|
|
4497
|
+
if (self._eventHandlers[name] === callback) {
|
|
4498
|
+
self._eventHandlers[name] = null;
|
|
4499
|
+
}
|
|
4500
|
+
};
|
|
4501
|
+
},
|
|
4188
4502
|
|
|
4189
4503
|
/**
|
|
4190
|
-
*
|
|
4504
|
+
* @private Deliver an event to the legacy single handler and every
|
|
4505
|
+
* game.on() listener. All game event delivery flows through here.
|
|
4506
|
+
*/
|
|
4507
|
+
_dispatch: function(name) {
|
|
4508
|
+
const args = Array.prototype.slice.call(arguments, 1);
|
|
4509
|
+
const single = this._eventHandlers[name];
|
|
4510
|
+
if (single) {
|
|
4511
|
+
try { single.apply(null, args); } catch (e) { Usion.log('game handler error (' + name + '): ' + e.message); }
|
|
4512
|
+
}
|
|
4513
|
+
const list = this._listeners[name];
|
|
4514
|
+
if (list && list.length) {
|
|
4515
|
+
const copy = list.slice();
|
|
4516
|
+
for (let i = 0; i < copy.length; i++) {
|
|
4517
|
+
try { copy[i].apply(null, args); } catch (e) { Usion.log('game listener error (' + name + '): ' + e.message); }
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
},
|
|
4521
|
+
|
|
4522
|
+
/**
|
|
4523
|
+
* Register an additional event listener. Unlike the onX methods this
|
|
4524
|
+
* supports multiple listeners per event, can be called before
|
|
4525
|
+
* connect(), and works in every transport (standalone, embedded
|
|
4526
|
+
* proxy, direct). Accepts internal names ('action'), wire names
|
|
4527
|
+
* ('game:action'), or snake_case ('player_joined').
|
|
4191
4528
|
* @param {string} event - Event name
|
|
4192
4529
|
* @param {function} callback - Handler function
|
|
4530
|
+
* @returns {function} Unsubscribe function
|
|
4193
4531
|
*/
|
|
4194
4532
|
on: function(event, callback) {
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4533
|
+
const self = this;
|
|
4534
|
+
const name = _normalizeEventName(event);
|
|
4535
|
+
if (!self._listeners[name]) self._listeners[name] = [];
|
|
4536
|
+
self._listeners[name].push(callback);
|
|
4537
|
+
return function() {
|
|
4538
|
+
const list = self._listeners[name];
|
|
4539
|
+
if (!list) return;
|
|
4540
|
+
const i = list.indexOf(callback);
|
|
4541
|
+
if (i >= 0) list.splice(i, 1);
|
|
4542
|
+
};
|
|
4198
4543
|
}
|
|
4199
4544
|
};
|
|
4200
4545
|
|
|
4546
|
+
game._listeners = {};
|
|
4547
|
+
|
|
4201
4548
|
// Apply sub-modules
|
|
4202
4549
|
applyGameDirect(game, Usion);
|
|
4203
4550
|
applyGameSocket(game, Usion);
|
|
@@ -4616,6 +4963,9 @@ var Usion = (function () {
|
|
|
4616
4963
|
Usion.bot = createBotModule(Usion);
|
|
4617
4964
|
Usion.fileStorage = createFileStorageModule(Usion);
|
|
4618
4965
|
Usion.game = createGameModule(Usion);
|
|
4966
|
+
// Stable error class + codes — developers branch on err.code, not message text.
|
|
4967
|
+
Usion.UsionError = UsionError;
|
|
4968
|
+
Usion.ERROR_CODES = ERROR_CODES;
|
|
4619
4969
|
// Unified backend channel (used by lobby etc.; works standalone + embedded).
|
|
4620
4970
|
applyBackendChannel(Usion);
|
|
4621
4971
|
Usion.lobby = createLobbyModule(Usion);
|