action-engine-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +348 -0
  3. package/actionengine/3rdparty/goblin/goblin.js +9609 -0
  4. package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
  5. package/actionengine/camera/actioncamera.js +90 -0
  6. package/actionengine/camera/cameracollisionhandler.js +69 -0
  7. package/actionengine/character/actioncharacter.js +360 -0
  8. package/actionengine/character/actioncharacter3D.js +61 -0
  9. package/actionengine/core/app.js +430 -0
  10. package/actionengine/debug/basedebugpanel.js +858 -0
  11. package/actionengine/display/canvasmanager.js +75 -0
  12. package/actionengine/display/gl/programmanager.js +570 -0
  13. package/actionengine/display/gl/shaders/lineshader.js +118 -0
  14. package/actionengine/display/gl/shaders/objectshader.js +1756 -0
  15. package/actionengine/display/gl/shaders/particleshader.js +43 -0
  16. package/actionengine/display/gl/shaders/shadowshader.js +319 -0
  17. package/actionengine/display/gl/shaders/spriteshader.js +100 -0
  18. package/actionengine/display/gl/shaders/watershader.js +67 -0
  19. package/actionengine/display/graphics/actionmodel3D.js +191 -0
  20. package/actionengine/display/graphics/actionsprite3D.js +230 -0
  21. package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
  22. package/actionengine/display/graphics/lighting/actionlight.js +211 -0
  23. package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
  24. package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
  25. package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
  26. package/actionengine/display/graphics/renderableobject.js +44 -0
  27. package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
  28. package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
  29. package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
  30. package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
  31. package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
  32. package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
  33. package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
  34. package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
  35. package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
  36. package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
  37. package/actionengine/display/graphics/texture/texturemanager.js +242 -0
  38. package/actionengine/display/graphics/texture/textureregistry.js +177 -0
  39. package/actionengine/input/actionscrollablearea.js +1405 -0
  40. package/actionengine/input/inputhandler.js +1647 -0
  41. package/actionengine/math/geometry/geometrybuilder.js +161 -0
  42. package/actionengine/math/geometry/glbexporter.js +364 -0
  43. package/actionengine/math/geometry/glbloader.js +722 -0
  44. package/actionengine/math/geometry/modelcodegenerator.js +97 -0
  45. package/actionengine/math/geometry/triangle.js +33 -0
  46. package/actionengine/math/geometry/triangleutils.js +34 -0
  47. package/actionengine/math/mathutils.js +25 -0
  48. package/actionengine/math/matrix4.js +785 -0
  49. package/actionengine/math/physics/actionphysics.js +108 -0
  50. package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
  51. package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
  52. package/actionengine/math/physics/actionraycast.js +129 -0
  53. package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
  54. package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
  55. package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
  56. package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
  57. package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
  58. package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
  59. package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
  60. package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
  61. package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
  62. package/actionengine/math/quaternion.js +61 -0
  63. package/actionengine/math/vector2.js +277 -0
  64. package/actionengine/math/vector3.js +318 -0
  65. package/actionengine/math/viewfrustum.js +136 -0
  66. package/actionengine/network/ACTIONNETREADME.md +810 -0
  67. package/actionengine/network/client/ActionNetManager.js +802 -0
  68. package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
  69. package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
  70. package/actionengine/network/client/SyncSystem.js +422 -0
  71. package/actionengine/network/p2p/ActionNetPeer.js +142 -0
  72. package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
  73. package/actionengine/network/p2p/DataConnection.js +282 -0
  74. package/actionengine/network/p2p/README.md +510 -0
  75. package/actionengine/network/p2p/example.html +502 -0
  76. package/actionengine/network/server/ActionNetServer.js +577 -0
  77. package/actionengine/network/server/ActionNetServerSSL.js +579 -0
  78. package/actionengine/network/server/ActionNetServerUtils.js +458 -0
  79. package/actionengine/network/server/SERVERREADME.md +314 -0
  80. package/actionengine/network/server/package-lock.json +35 -0
  81. package/actionengine/network/server/package.json +13 -0
  82. package/actionengine/network/server/start.bat +27 -0
  83. package/actionengine/network/server/start.sh +25 -0
  84. package/actionengine/network/server/startwss.bat +27 -0
  85. package/actionengine/sound/audiomanager.js +1589 -0
  86. package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
  87. package/actionengine/sound/soundfont/actionparser.js +718 -0
  88. package/actionengine/sound/soundfont/actionreverb.js +252 -0
  89. package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
  90. package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
  91. package/actionengine/sound/soundfont/soundfont.js +2 -0
  92. package/dist/action-engine.min.js +328 -0
  93. package/package.json +35 -0
@@ -0,0 +1,502 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ActionNetP2P Discovery</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+ body {
12
+ font-family: monospace;
13
+ background: #1e1e1e;
14
+ color: #d4d4d4;
15
+ padding: 20px;
16
+ margin: 0;
17
+ }
18
+ .container {
19
+ max-width: 1200px;
20
+ margin: 0 auto;
21
+ }
22
+ h1 {
23
+ color: #4ec9b0;
24
+ margin-top: 0;
25
+ }
26
+ h2 {
27
+ color: #569cd6;
28
+ margin-top: 30px;
29
+ border-bottom: 1px solid #333;
30
+ padding-bottom: 5px;
31
+ }
32
+ .status {
33
+ padding: 10px;
34
+ margin: 10px 0;
35
+ border-radius: 4px;
36
+ background: #252526;
37
+ border-left: 3px solid #569cd6;
38
+ }
39
+ .status.connected {
40
+ border-left-color: #4ec9b0;
41
+ }
42
+ .status.error {
43
+ border-left-color: #f48771;
44
+ }
45
+ .status.info {
46
+ border-left-color: #dcdcaa;
47
+ }
48
+ .controls {
49
+ display: flex;
50
+ gap: 10px;
51
+ margin: 20px 0;
52
+ flex-wrap: wrap;
53
+ }
54
+ button {
55
+ padding: 8px 16px;
56
+ background: #569cd6;
57
+ color: #fff;
58
+ border: none;
59
+ border-radius: 4px;
60
+ cursor: pointer;
61
+ font-family: monospace;
62
+ }
63
+ button:hover {
64
+ background: #4ec9b0;
65
+ }
66
+ button:disabled {
67
+ background: #666;
68
+ cursor: not-allowed;
69
+ }
70
+ input {
71
+ padding: 8px;
72
+ background: #3e3e42;
73
+ color: #d4d4d4;
74
+ border: 1px solid #555;
75
+ border-radius: 4px;
76
+ font-family: monospace;
77
+ }
78
+ .log {
79
+ background: #1e1e1e;
80
+ border: 1px solid #333;
81
+ border-radius: 4px;
82
+ padding: 10px;
83
+ max-height: 400px;
84
+ overflow-y: auto;
85
+ margin: 10px 0;
86
+ font-size: 12px;
87
+ }
88
+ .log-entry {
89
+ margin: 3px 0;
90
+ padding: 2px 5px;
91
+ border-radius: 2px;
92
+ }
93
+ .log-entry.info {
94
+ color: #858585;
95
+ }
96
+ .log-entry.peer {
97
+ color: #4ec9b0;
98
+ }
99
+ .log-entry.connection {
100
+ color: #569cd6;
101
+ }
102
+ .log-entry.error {
103
+ color: #f48771;
104
+ }
105
+ .peers-list {
106
+ display: grid;
107
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
108
+ gap: 10px;
109
+ margin: 10px 0;
110
+ }
111
+ .peer-card {
112
+ background: #252526;
113
+ border: 1px solid #333;
114
+ border-radius: 4px;
115
+ padding: 10px;
116
+ border-left: 3px solid #4ec9b0;
117
+ }
118
+ .peer-card h3 {
119
+ margin: 0 0 5px 0;
120
+ color: #4ec9b0;
121
+ font-size: 13px;
122
+ }
123
+ .peer-card p {
124
+ margin: 3px 0;
125
+ font-size: 12px;
126
+ }
127
+ .stats {
128
+ display: grid;
129
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
130
+ gap: 15px;
131
+ margin: 20px 0;
132
+ }
133
+ .stat-box {
134
+ background: #252526;
135
+ border: 1px solid #333;
136
+ border-radius: 4px;
137
+ padding: 15px;
138
+ text-align: center;
139
+ }
140
+ .stat-number {
141
+ font-size: 24px;
142
+ color: #4ec9b0;
143
+ font-weight: bold;
144
+ }
145
+ .stat-label {
146
+ font-size: 12px;
147
+ color: #858585;
148
+ margin-top: 5px;
149
+ }
150
+ </style>
151
+ </head>
152
+ <body>
153
+ <div class="container">
154
+ <h1>ActionNetP2P Discovery Test</h1>
155
+
156
+ <div class="status connected">
157
+ <strong>Status:</strong> <span id="status">Initializing...</span>
158
+ </div>
159
+
160
+ <h2>Configuration</h2>
161
+ <div class="controls">
162
+ <div>
163
+ <label>Application ID:</label>
164
+ <input type="text" id="applicationId" value="app-id-00000" style="width: 200px">
165
+ </div>
166
+ <div>
167
+ <label>Username:</label>
168
+ <input type="text" id="username" value="Tester" style="width: 150px">
169
+ </div>
170
+ <button id="joinBtn" onclick="joinGame()">Join Game</button>
171
+ <button id="leaveBtn" onclick="leaveGame()" disabled>Leave Game</button>
172
+ <button id="clearLogBtn" onclick="clearLog()">Clear Log</button>
173
+ </div>
174
+
175
+ <h2>Stats</h2>
176
+ <div class="stats">
177
+ <div class="stat-box">
178
+ <div class="stat-number" id="totalPeerCount">0</div>
179
+ <div class="stat-label">Total Peers (from tracker)</div>
180
+ </div>
181
+ <div class="stat-box">
182
+ <div class="stat-number" id="connectedPeerCount">0</div>
183
+ <div class="stat-label">Connected Peers</div>
184
+ </div>
185
+ <div class="stat-box">
186
+ <div class="stat-number" id="connectionCount">0</div>
187
+ <div class="stat-label">Data Connections</div>
188
+ </div>
189
+ <div class="stat-box">
190
+ <div class="stat-number" id="trackerCount">0</div>
191
+ <div class="stat-label">Active Trackers</div>
192
+ </div>
193
+ </div>
194
+
195
+ <h2>Confirmed Peers</h2>
196
+ <div id="confirmedPeersList" class="peers-list">
197
+ <div style="color: #858585; padding: 10px;">No confirmed peers yet</div>
198
+ </div>
199
+
200
+ <h2>Peers Discovered</h2>
201
+ <div id="peersList" class="peers-list">
202
+ <div style="color: #858585; padding: 10px;">No peers discovered yet</div>
203
+ </div>
204
+
205
+ <h2>Log</h2>
206
+ <div id="log" class="log"></div>
207
+ </div>
208
+
209
+ <!-- Load P2P modules in order -->
210
+ <script src="ActionNetPeer.js"></script>
211
+ <script src="ActionNetTrackerClient.js"></script>
212
+ <script src="DataConnection.js"></script> <!-- Exports DataConnection class -->
213
+
214
+ <script>
215
+ // Test harness
216
+ let localPeerId = null; // Our peer ID for DataConnection initiator selection
217
+ let currentTracker = null;
218
+ let currentApplicationId = null;
219
+ let username = null;
220
+ const peers = new Map();
221
+ const connections = new Map();
222
+ const confirmedPeers = new Map(); // peerId -> {id, username, confirmed}
223
+
224
+ // Redirect console.log to UI log
225
+ const originalLog = console.log;
226
+ console.log = function(...args) {
227
+ const msg = args.map(arg => {
228
+ if (typeof arg === 'object') {
229
+ try {
230
+ return JSON.stringify(arg, null, 2);
231
+ } catch {
232
+ return String(arg);
233
+ }
234
+ }
235
+ return String(arg);
236
+ }).join(' ');
237
+ log(msg, 'info');
238
+ originalLog.apply(console, args);
239
+ };
240
+
241
+ const log = (msg, type = 'info') => {
242
+ const entry = document.createElement('div');
243
+ entry.className = `log-entry ${type}`;
244
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
245
+ document.getElementById('log').appendChild(entry);
246
+ document.getElementById('log').scrollTop = document.getElementById('log').scrollHeight;
247
+ };
248
+
249
+ const clearLog = () => {
250
+ document.getElementById('log').innerHTML = '';
251
+ };
252
+
253
+ const updateStats = () => {
254
+ document.getElementById('totalPeerCount').textContent = currentTracker ? currentTracker.getDiscoveredPeerCount() : 0;
255
+ document.getElementById('connectedPeerCount').textContent = peers.size;
256
+ document.getElementById('connectionCount').textContent = connections.size;
257
+ document.getElementById('trackerCount').textContent = currentTracker ? 1 : 0;
258
+ };
259
+
260
+ const updateConfirmedPeersList = () => {
261
+ const container = document.getElementById('confirmedPeersList');
262
+ if (confirmedPeers.size === 0) {
263
+ container.innerHTML = '<div style="color: #858585; padding: 10px;">No confirmed peers yet</div>';
264
+ return;
265
+ }
266
+
267
+ container.innerHTML = Array.from(confirmedPeers.values()).map(peer => `
268
+ <div class="peer-card">
269
+ <h3>${peer.username || peer.id}</h3>
270
+ <p><strong>Peer ID:</strong> ${peer.id}</p>
271
+ <p><strong>Status:</strong> ${peer.confirmed ? 'Confirmed' : 'Confirming'}</p>
272
+ </div>
273
+ `).join('');
274
+ };
275
+
276
+ const updatePeersList = () => {
277
+ const container = document.getElementById('peersList');
278
+ if (peers.size === 0) {
279
+ container.innerHTML = '<div style="color: #858585; padding: 10px;">No peers discovered yet</div>';
280
+ return;
281
+ }
282
+
283
+ container.innerHTML = Array.from(peers.values()).map(peer => `
284
+ <div class="peer-card">
285
+ <h3>${peer.id}</h3>
286
+ <p><strong>IP:</strong> ${peer.ip || 'N/A'}</p>
287
+ <p><strong>Port:</strong> ${peer.port || 'N/A'}</p>
288
+ <p><strong>Source:</strong> ${peer.source || 'unknown'}</p>
289
+ <p><strong>Time:</strong> ${new Date(peer.discovered || Date.now()).toLocaleTimeString()}</p>
290
+ </div>
291
+ `).join('');
292
+ };
293
+
294
+ const joinGame = async () => {
295
+ try {
296
+ currentApplicationId = document.getElementById('applicationId').value || 'app-id-00000';
297
+ username = document.getElementById('username').value || 'Tester';
298
+
299
+ log(`Joining network: ${currentApplicationId} as ${username}`, 'info');
300
+
301
+ document.getElementById('status').textContent = `Connecting to tracker...`;
302
+ document.getElementById('status').parentElement.className = 'status';
303
+
304
+ // Generate peer ID
305
+ localPeerId = 'peer_' + Math.random().toString(36).substr(2, 9);
306
+ log(`Generated peer ID: ${localPeerId}`, 'info');
307
+
308
+ // Generate infohash from application ID
309
+ const encoder = new TextEncoder();
310
+ const data = encoder.encode(currentApplicationId);
311
+ const hashBuffer = await crypto.subtle.digest('SHA-1', data);
312
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
313
+ const infohash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
314
+
315
+ log(`Application ID hash (infohash): ${infohash}`, 'info');
316
+
317
+ // Create tracker client with multiple trackers for redundancy
318
+ const trackerUrls = [
319
+ 'wss://tracker.openwebtorrent.com/',
320
+ 'wss://tracker.btorrent.xyz/',
321
+ 'wss://tracker.fastcast.nz/'
322
+ ];
323
+ const tracker = new ActionNetTrackerClient(trackerUrls, infohash, localPeerId, {
324
+ debug: true, // Enable logging for debugging
325
+ numwant: 50, // Max peers to request from tracker
326
+ announceInterval: 5000, // Start at 5 seconds for fast discovery
327
+ maxAnnounceInterval: 120000, // Back off to max 120 seconds
328
+ backoffMultiplier: 1.1 // Increase by 1.1x each cycle
329
+ });
330
+
331
+ // Handle peer connections
332
+ tracker.on('peer', (data) => {
333
+ const peerId = data.id;
334
+ log(`Peer connected: ${peerId}`, 'peer');
335
+ peers.set(peerId, {
336
+ id: peerId,
337
+ discovered: Date.now(),
338
+ source: data.source,
339
+ peer: data.peer
340
+ });
341
+ updateStats();
342
+ updatePeersList();
343
+ });
344
+
345
+ // Handle data connections (already created by tracker)
346
+ tracker.on('connection', (connection) => {
347
+ const peerId = connection.remotePeerId;
348
+
349
+ log(`Data connection created with peer: ${peerId}`, 'info');
350
+
351
+ connection.on('connect', () => {
352
+ log(`Data connection connected with ${peerId}`, 'info');
353
+ connections.set(peerId, connection);
354
+ updateStats();
355
+
356
+ // Send handshake
357
+ connection.send({
358
+ type: 'handshake',
359
+ peerId: localPeerId,
360
+ username: username,
361
+ applicationId: currentApplicationId
362
+ });
363
+ });
364
+
365
+ connection.on('data', (message) => {
366
+ if (message.type === 'handshake') {
367
+ // Validate handshake
368
+ if (message.applicationId !== currentApplicationId) {
369
+ log(`Handshake failed: peer on different application (${message.applicationId})`, 'error');
370
+ return;
371
+ }
372
+
373
+ // Add to confirmed peers
374
+ confirmedPeers.set(peerId, {
375
+ id: message.peerId,
376
+ username: message.username,
377
+ confirmed: true
378
+ });
379
+
380
+ log(`Peer confirmed: ${message.username} (${message.peerId})`, 'peer');
381
+ updateConfirmedPeersList();
382
+ } else {
383
+ log(`DataConnection message from ${peerId}: ${JSON.stringify(message)}`, 'connection');
384
+ }
385
+ });
386
+
387
+ connection.on('close', () => {
388
+ log(`Data connection closed with ${peerId}`, 'info');
389
+ connections.delete(peerId);
390
+ confirmedPeers.delete(peerId);
391
+ updateStats();
392
+ updateConfirmedPeersList();
393
+ });
394
+
395
+ connection.on('error', (err) => {
396
+ log(`Data connection error with ${peerId}: ${err.message}`, 'error');
397
+ });
398
+ });
399
+
400
+ // Handle tracker ready event
401
+ tracker.on('ready', () => {
402
+ log('Tracker ready, waiting for peers...', 'info');
403
+ document.getElementById('status').textContent = `Connected to tracker - Waiting for peers...`;
404
+ document.getElementById('status').parentElement.className = 'status connected';
405
+ });
406
+
407
+ // Handle tracker updates
408
+ tracker.on('update', (data) => {
409
+ const total = data.complete + data.incomplete;
410
+ log(`Tracker: ${data.complete} seeders, ${data.incomplete} leechers (${total} total)`, 'info');
411
+ updateStats();
412
+ });
413
+
414
+ // Handle peer disconnection
415
+ tracker.on('peer-disconnected', (data) => {
416
+ const peerId = data.id;
417
+ log(`Peer disconnected: ${peerId}`, 'info');
418
+
419
+ // Clean up connection
420
+ if (connections.has(peerId)) {
421
+ connections.get(peerId).close();
422
+ connections.delete(peerId);
423
+ }
424
+
425
+ // Clean up peer
426
+ peers.delete(peerId);
427
+ confirmedPeers.delete(peerId);
428
+
429
+ updateStats();
430
+ updateConfirmedPeersList();
431
+ updatePeersList();
432
+ });
433
+
434
+ // Handle peer connection failure
435
+ tracker.on('peer-failed', (data) => {
436
+ const peerId = data.id;
437
+ log(`Peer failed: ${peerId} - ${data.error.message}`, 'error');
438
+
439
+ // Clean up connection
440
+ if (connections.has(peerId)) {
441
+ connections.get(peerId).close();
442
+ connections.delete(peerId);
443
+ }
444
+
445
+ // Clean up peer
446
+ peers.delete(peerId);
447
+ confirmedPeers.delete(peerId);
448
+
449
+ updateStats();
450
+ updateConfirmedPeersList();
451
+ updatePeersList();
452
+ });
453
+
454
+ // Handle tracker errors
455
+ tracker.on('error', (err) => {
456
+ log(`Tracker error: ${err.message}`, 'error');
457
+ });
458
+
459
+ // Connect to tracker
460
+ await tracker.connect();
461
+ log('Connected to tracker and announced', 'info');
462
+ document.getElementById('joinBtn').disabled = true;
463
+ document.getElementById('leaveBtn').disabled = false;
464
+
465
+ log('To see peers: Open this page in another window/tab and join the same game', 'info');
466
+
467
+ // Store tracker for cleanup
468
+ currentTracker = tracker;
469
+
470
+ } catch (error) {
471
+ log(`Error: ${error.message}`, 'error');
472
+ document.getElementById('status').textContent = `Error: ${error.message}`;
473
+ document.getElementById('status').parentElement.className = 'status error';
474
+ }
475
+ };
476
+
477
+ const leaveGame = () => {
478
+ // Disconnect tracker
479
+ if (currentTracker) {
480
+ currentTracker.disconnect();
481
+ currentTracker = null;
482
+ }
483
+
484
+ peers.clear();
485
+ connections.clear();
486
+ confirmedPeers.clear();
487
+ updateStats();
488
+ updateConfirmedPeersList();
489
+ updatePeersList();
490
+
491
+ document.getElementById('status').textContent = 'Disconnected';
492
+ document.getElementById('status').parentElement.className = 'status';
493
+ document.getElementById('joinBtn').disabled = false;
494
+ document.getElementById('leaveBtn').disabled = true;
495
+
496
+ log('Left game', 'info');
497
+ };
498
+
499
+ log('P2P Test harness initializing...', 'info');
500
+ </script>
501
+ </body>
502
+ </html>