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.
- package/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- package/package.json +35 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multiplayer Game Server (SSL/WSS Version)
|
|
3
|
+
*
|
|
4
|
+
* A dedicated secure WebSocket server for multiplayer games.
|
|
5
|
+
* Usage: node ActionNetServerSSL.js [port] [maxPlayersPerRoom]
|
|
6
|
+
* Example: node ActionNetServerSSL.js 8443 4
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const https = require("https");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const WebSocket = require("ws");
|
|
12
|
+
const ActionNetServerUtils = require("./ActionNetServerUtils");
|
|
13
|
+
|
|
14
|
+
// ========================================
|
|
15
|
+
// CONFIGURATION
|
|
16
|
+
// ========================================
|
|
17
|
+
const CONFIG = {
|
|
18
|
+
port: 8443, // Default WSS port
|
|
19
|
+
debug: true,
|
|
20
|
+
maxPlayersPerRoom: -1,
|
|
21
|
+
// Windows certificate paths (absolute paths)
|
|
22
|
+
certPath: "C:\\path\\to\\your\\cert.pem",
|
|
23
|
+
keyPath: "C:\\path\\to\\your\\privkey.pem"
|
|
24
|
+
};
|
|
25
|
+
// ========================================
|
|
26
|
+
|
|
27
|
+
// Allow command line override if needed
|
|
28
|
+
const port = process.argv[2] || CONFIG.port;
|
|
29
|
+
const maxPlayersPerRoom = process.argv[3] || CONFIG.maxPlayersPerRoom;
|
|
30
|
+
|
|
31
|
+
// Load SSL certificates
|
|
32
|
+
let server;
|
|
33
|
+
try {
|
|
34
|
+
const options = {
|
|
35
|
+
cert: fs.readFileSync(CONFIG.certPath),
|
|
36
|
+
key: fs.readFileSync(CONFIG.keyPath)
|
|
37
|
+
};
|
|
38
|
+
server = https.createServer(options);
|
|
39
|
+
console.log("SSL certificates loaded successfully");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("ERROR loading SSL certificates:", error.message);
|
|
42
|
+
console.error(`Expected files at:`);
|
|
43
|
+
console.error(` Cert: ${CONFIG.certPath}`);
|
|
44
|
+
console.error(` Key: ${CONFIG.keyPath}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create WebSocket server
|
|
49
|
+
const wss = new WebSocket.Server({ server });
|
|
50
|
+
|
|
51
|
+
// Create ActionNet server utilities
|
|
52
|
+
const utils = new ActionNetServerUtils(wss);
|
|
53
|
+
|
|
54
|
+
// Game state management
|
|
55
|
+
const gameRooms = new Map(); // roomName -> game state
|
|
56
|
+
|
|
57
|
+
console.log(`Starting Secure Multiplayer Game server on port ${port}...`);
|
|
58
|
+
console.log(`Max players per room: ${maxPlayersPerRoom === -1 ? 'No limit' : maxPlayersPerRoom}`);
|
|
59
|
+
|
|
60
|
+
wss.on("connection", (ws, req) => {
|
|
61
|
+
console.log("New player connected from:", req.socket.remoteAddress);
|
|
62
|
+
|
|
63
|
+
// Send current room list immediately when client connects
|
|
64
|
+
const roomList = utils.getRoomList();
|
|
65
|
+
if (roomList.length > 0) {
|
|
66
|
+
utils.sendToClient(ws, {
|
|
67
|
+
type: "roomList",
|
|
68
|
+
rooms: roomList
|
|
69
|
+
});
|
|
70
|
+
console.log(`Sent current room list to new player: ${roomList.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ws.on("message", (data) => {
|
|
74
|
+
try {
|
|
75
|
+
const message = JSON.parse(data.toString());
|
|
76
|
+
if (message.type === "connect") {
|
|
77
|
+
const displayName = utils.generateUniqueDisplayName(message.username);
|
|
78
|
+
console.log("Received connect message:", { ...message, displayName });
|
|
79
|
+
} else if (CONFIG.debug && message.type !== "pieceUpdate") {
|
|
80
|
+
// Don't spam logs with pieceUpdate messages
|
|
81
|
+
console.log("Received message:", message.type, message.playerNumber || "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (message.type) {
|
|
85
|
+
case "connect":
|
|
86
|
+
handleConnect(ws, message);
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case "joinRoom":
|
|
90
|
+
handleJoinRoom(ws, message);
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case "leaveRoom":
|
|
94
|
+
handleLeaveRoom(ws, message);
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case "changeUsername":
|
|
98
|
+
handleChangeUsername(ws, message);
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case "ping":
|
|
102
|
+
// Auto-respond to pings
|
|
103
|
+
utils.sendToClient(ws, {
|
|
104
|
+
type: "pong",
|
|
105
|
+
sequence: message.sequence,
|
|
106
|
+
timestamp: message.timestamp
|
|
107
|
+
});
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
// All other messages are game messages - relay to room
|
|
112
|
+
relayGameMessage(ws, message);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Error parsing message:", error);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
ws.on("close", () => {
|
|
121
|
+
console.log("Player disconnected");
|
|
122
|
+
handleDisconnect(ws);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
ws.on("error", (error) => {
|
|
126
|
+
console.error("WebSocket error:", error);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handle player connection
|
|
132
|
+
*/
|
|
133
|
+
function handleConnect(ws, message) {
|
|
134
|
+
const success = utils.registerClient(ws, message);
|
|
135
|
+
|
|
136
|
+
if (!success) {
|
|
137
|
+
// Client was rejected (duplicate username)
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const client = utils.getClient(ws);
|
|
142
|
+
console.log(`Player ${client.displayName} connected to lobby`);
|
|
143
|
+
|
|
144
|
+
// Send dedicated success message for reliable detection
|
|
145
|
+
utils.sendToClient(ws, {
|
|
146
|
+
type: "connectSuccess"
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Send room list
|
|
150
|
+
utils.sendToClient(ws, {
|
|
151
|
+
type: "roomList",
|
|
152
|
+
rooms: utils.getRoomList()
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Broadcast updated room list to all clients
|
|
156
|
+
utils.broadcastToAll({
|
|
157
|
+
type: "roomList",
|
|
158
|
+
rooms: utils.getRoomList()
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle room join request
|
|
164
|
+
*/
|
|
165
|
+
function handleJoinRoom(ws, message) {
|
|
166
|
+
const client = utils.getClient(ws);
|
|
167
|
+
if (!client) {
|
|
168
|
+
console.warn("Join request from unregistered client");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const roomName = message.roomName;
|
|
173
|
+
|
|
174
|
+
// Validate room name
|
|
175
|
+
if (!roomName || roomName.trim() === "") {
|
|
176
|
+
utils.sendToClient(ws, {
|
|
177
|
+
type: "error",
|
|
178
|
+
text: "Invalid room name. Room name cannot be empty."
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check if room is full (if limit is set)
|
|
184
|
+
if (maxPlayersPerRoom !== -1 && utils.roomExists(roomName)) {
|
|
185
|
+
const roomClients = utils.getClientsInRoom(roomName);
|
|
186
|
+
if (roomClients.length >= maxPlayersPerRoom) {
|
|
187
|
+
utils.sendToClient(ws, {
|
|
188
|
+
type: "error",
|
|
189
|
+
text: "Room is full."
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Add client to room
|
|
196
|
+
const wasNewRoom = !utils.roomExists(roomName);
|
|
197
|
+
utils.addToRoom(ws, roomName);
|
|
198
|
+
|
|
199
|
+
// Initialize game state for the room if it's new
|
|
200
|
+
if (wasNewRoom) {
|
|
201
|
+
initializeGameRoom(roomName);
|
|
202
|
+
console.log(`Created new game room: ${roomName}`);
|
|
203
|
+
|
|
204
|
+
// Broadcast updated room list to all clients
|
|
205
|
+
utils.broadcastToAll({
|
|
206
|
+
type: "roomList",
|
|
207
|
+
rooms: utils.getRoomList()
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`Player ${client.displayName} joined room: ${roomName}`);
|
|
212
|
+
|
|
213
|
+
// Broadcast join message to the room
|
|
214
|
+
const joinMessage = wasNewRoom
|
|
215
|
+
? `${client.displayName} created and joined room "${roomName}"`
|
|
216
|
+
: `${client.displayName} joined room "${roomName}"`;
|
|
217
|
+
utils.broadcastToRoom(roomName, {
|
|
218
|
+
type: "system",
|
|
219
|
+
text: joinMessage,
|
|
220
|
+
roomName: roomName,
|
|
221
|
+
timestamp: Date.now()
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Send dedicated success message for reliable detection
|
|
225
|
+
utils.sendToClient(ws, {
|
|
226
|
+
type: "joinSuccess",
|
|
227
|
+
roomName: roomName
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Send user list for this room
|
|
231
|
+
utils.sendToClient(ws, {
|
|
232
|
+
type: "userList",
|
|
233
|
+
users: utils.getClientsInRoom(roomName),
|
|
234
|
+
roomName: roomName
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Broadcast updated user list to room
|
|
238
|
+
utils.broadcastToRoom(roomName, {
|
|
239
|
+
type: "userList",
|
|
240
|
+
users: utils.getClientsInRoom(roomName),
|
|
241
|
+
roomName: roomName
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Handle leave room request
|
|
247
|
+
*/
|
|
248
|
+
function handleLeaveRoom(ws, message) {
|
|
249
|
+
const client = utils.getClient(ws);
|
|
250
|
+
if (!client) return;
|
|
251
|
+
|
|
252
|
+
// Check if this player is the host BEFORE removing them
|
|
253
|
+
const wasHost = utils.isHost(ws);
|
|
254
|
+
|
|
255
|
+
const oldRoom = utils.removeFromRoom(ws, true); // true = return to lobby
|
|
256
|
+
|
|
257
|
+
if (oldRoom) {
|
|
258
|
+
console.log(`Player ${client.displayName} left room: ${oldRoom}`);
|
|
259
|
+
|
|
260
|
+
// Notify user
|
|
261
|
+
utils.sendToClient(ws, {
|
|
262
|
+
type: "system",
|
|
263
|
+
text: `Left Tetris battle "${oldRoom}". You are now in the lobby.`
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Send updated room list
|
|
267
|
+
utils.sendToClient(ws, {
|
|
268
|
+
type: "roomList",
|
|
269
|
+
rooms: utils.getRoomList()
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// If host left, close the room entirely
|
|
273
|
+
if (wasHost && utils.roomExists(oldRoom)) {
|
|
274
|
+
console.log(`Host left room ${oldRoom} - closing room and removing all players`);
|
|
275
|
+
|
|
276
|
+
// Notify remaining players that host left
|
|
277
|
+
utils.broadcastToRoom(oldRoom, {
|
|
278
|
+
type: "hostLeft",
|
|
279
|
+
id: client.id,
|
|
280
|
+
displayName: client.displayName,
|
|
281
|
+
roomName: oldRoom,
|
|
282
|
+
timestamp: Date.now()
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Get all clients still in the room
|
|
286
|
+
const remainingClients = Array.from(utils.rooms.get(oldRoom) || []);
|
|
287
|
+
|
|
288
|
+
// Remove all clients from the room and put them back in lobby
|
|
289
|
+
remainingClients.forEach(clientWs => {
|
|
290
|
+
utils.removeFromRoom(clientWs, true); // true = return to lobby
|
|
291
|
+
|
|
292
|
+
// Notify each client they were removed
|
|
293
|
+
utils.sendToClient(clientWs, {
|
|
294
|
+
type: "system",
|
|
295
|
+
text: `Room "${oldRoom}" closed because host left.`
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Send updated room list
|
|
299
|
+
utils.sendToClient(clientWs, {
|
|
300
|
+
type: "roomList",
|
|
301
|
+
rooms: utils.getRoomList()
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Delete the room
|
|
306
|
+
gameRooms.delete(oldRoom);
|
|
307
|
+
|
|
308
|
+
// Broadcast updated room list to everyone
|
|
309
|
+
utils.broadcastToAll({
|
|
310
|
+
type: "roomList",
|
|
311
|
+
rooms: utils.getRoomList()
|
|
312
|
+
});
|
|
313
|
+
} else if (!utils.roomExists(oldRoom)) {
|
|
314
|
+
// Room became empty naturally
|
|
315
|
+
console.log(`Deleted empty room: ${oldRoom}`);
|
|
316
|
+
gameRooms.delete(oldRoom);
|
|
317
|
+
|
|
318
|
+
// Broadcast updated room list
|
|
319
|
+
utils.broadcastToAll({
|
|
320
|
+
type: "roomList",
|
|
321
|
+
rooms: utils.getRoomList()
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
// Non-host left, room continues
|
|
325
|
+
// Broadcast user left to remaining clients in room
|
|
326
|
+
utils.broadcastToRoom(oldRoom, {
|
|
327
|
+
type: "userLeft",
|
|
328
|
+
id: client.id,
|
|
329
|
+
displayName: client.displayName,
|
|
330
|
+
roomName: oldRoom,
|
|
331
|
+
timestamp: Date.now()
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Send updated user list to room
|
|
335
|
+
utils.broadcastToRoom(oldRoom, {
|
|
336
|
+
type: "userList",
|
|
337
|
+
users: utils.getClientsInRoom(oldRoom),
|
|
338
|
+
roomName: oldRoom
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Handle player disconnect
|
|
346
|
+
*/
|
|
347
|
+
function handleDisconnect(ws) {
|
|
348
|
+
// Check if this player was the host BEFORE disconnecting
|
|
349
|
+
const wasHost = utils.isHost(ws);
|
|
350
|
+
const client = utils.getClient(ws);
|
|
351
|
+
const roomName = client ? client.roomName : null;
|
|
352
|
+
|
|
353
|
+
const clientInfo = utils.handleDisconnect(ws);
|
|
354
|
+
|
|
355
|
+
if (clientInfo) {
|
|
356
|
+
const { id, displayName } = clientInfo;
|
|
357
|
+
const disconnectRoomName = clientInfo.roomName;
|
|
358
|
+
|
|
359
|
+
if (disconnectRoomName) {
|
|
360
|
+
console.log(`Player ${displayName} disconnected from room: ${disconnectRoomName}`);
|
|
361
|
+
|
|
362
|
+
// If host disconnected, close the room entirely
|
|
363
|
+
if (wasHost && utils.roomExists(disconnectRoomName)) {
|
|
364
|
+
console.log(`Host disconnected from room ${disconnectRoomName} - closing room and removing all players`);
|
|
365
|
+
|
|
366
|
+
// Notify remaining players that host left
|
|
367
|
+
utils.broadcastToRoom(disconnectRoomName, {
|
|
368
|
+
type: "hostLeft",
|
|
369
|
+
id: id,
|
|
370
|
+
displayName: displayName,
|
|
371
|
+
roomName: disconnectRoomName,
|
|
372
|
+
timestamp: Date.now()
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Get all clients still in the room
|
|
376
|
+
const remainingClients = Array.from(utils.rooms.get(disconnectRoomName) || []);
|
|
377
|
+
|
|
378
|
+
// Remove all clients from the room and put them back in lobby
|
|
379
|
+
remainingClients.forEach(clientWs => {
|
|
380
|
+
utils.removeFromRoom(clientWs, true); // true = return to lobby
|
|
381
|
+
|
|
382
|
+
// Notify each client they were removed
|
|
383
|
+
utils.sendToClient(clientWs, {
|
|
384
|
+
type: "system",
|
|
385
|
+
text: `Room "${disconnectRoomName}" closed because host disconnected.`
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Send updated room list
|
|
389
|
+
utils.sendToClient(clientWs, {
|
|
390
|
+
type: "roomList",
|
|
391
|
+
rooms: utils.getRoomList()
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Delete the room
|
|
396
|
+
gameRooms.delete(disconnectRoomName);
|
|
397
|
+
|
|
398
|
+
// Broadcast updated room list to everyone
|
|
399
|
+
utils.broadcastToAll({
|
|
400
|
+
type: "roomList",
|
|
401
|
+
rooms: utils.getRoomList()
|
|
402
|
+
});
|
|
403
|
+
} else if (!utils.roomExists(disconnectRoomName)) {
|
|
404
|
+
// Room became empty naturally
|
|
405
|
+
console.log(`Deleted empty room: ${disconnectRoomName}`);
|
|
406
|
+
gameRooms.delete(disconnectRoomName);
|
|
407
|
+
|
|
408
|
+
// Broadcast updated room list
|
|
409
|
+
utils.broadcastToAll({
|
|
410
|
+
type: "roomList",
|
|
411
|
+
rooms: utils.getRoomList()
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
// Non-host disconnected, room continues
|
|
415
|
+
// Broadcast user left to room
|
|
416
|
+
utils.broadcastToRoom(disconnectRoomName, {
|
|
417
|
+
type: "userLeft",
|
|
418
|
+
id: id,
|
|
419
|
+
displayName: displayName,
|
|
420
|
+
roomName: disconnectRoomName,
|
|
421
|
+
timestamp: Date.now()
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Send updated user list
|
|
425
|
+
utils.broadcastToRoom(disconnectRoomName, {
|
|
426
|
+
type: "userList",
|
|
427
|
+
users: utils.getClientsInRoom(disconnectRoomName),
|
|
428
|
+
roomName: disconnectRoomName
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
console.log(`Player ${displayName} disconnected from lobby`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Handle username change request
|
|
439
|
+
*/
|
|
440
|
+
function handleChangeUsername(ws, message) {
|
|
441
|
+
const client = utils.getClient(ws);
|
|
442
|
+
if (!client) {
|
|
443
|
+
console.log("Username change from unknown client");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const newUsername = message.username;
|
|
448
|
+
|
|
449
|
+
// Validate new username
|
|
450
|
+
if (!newUsername || newUsername.trim() === "" || newUsername.length < 2) {
|
|
451
|
+
utils.sendToClient(ws, {
|
|
452
|
+
type: "error",
|
|
453
|
+
text: "Username must be at least 2 characters long"
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(newUsername)) {
|
|
459
|
+
utils.sendToClient(ws, {
|
|
460
|
+
type: "error",
|
|
461
|
+
text: "Username can only contain letters, numbers, underscores, and hyphens"
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Check if new username is already taken by another client
|
|
467
|
+
const existingClient = Array.from(utils.clients.values()).find(
|
|
468
|
+
(c) => c.username === newUsername && c.id !== client.id
|
|
469
|
+
);
|
|
470
|
+
if (existingClient) {
|
|
471
|
+
utils.sendToClient(ws, {
|
|
472
|
+
type: "error",
|
|
473
|
+
text: "Username is already taken"
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
console.log(`Player ${client.displayName} changing username from ${client.username} to ${newUsername}`);
|
|
479
|
+
|
|
480
|
+
// Update client info
|
|
481
|
+
const oldUsername = client.username;
|
|
482
|
+
const oldDisplayName = client.displayName;
|
|
483
|
+
|
|
484
|
+
// Generate display name for the new username (exclude self from check)
|
|
485
|
+
const newDisplayName = utils.generateUniqueDisplayName(newUsername, client.id);
|
|
486
|
+
client.username = newUsername;
|
|
487
|
+
client.displayName = newDisplayName;
|
|
488
|
+
|
|
489
|
+
console.log(`Player ${oldDisplayName} is now ${newDisplayName}`);
|
|
490
|
+
|
|
491
|
+
// Send success confirmation
|
|
492
|
+
utils.sendToClient(ws, {
|
|
493
|
+
type: "usernameChangeSuccess",
|
|
494
|
+
oldUsername: oldUsername,
|
|
495
|
+
newUsername: newUsername,
|
|
496
|
+
displayName: client.displayName
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Broadcast updated user list to current room
|
|
500
|
+
if (client.roomName) {
|
|
501
|
+
utils.broadcastToRoom(client.roomName, {
|
|
502
|
+
type: "userList",
|
|
503
|
+
users: utils.getClientsInRoom(client.roomName),
|
|
504
|
+
roomName: client.roomName
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Broadcast username change notification
|
|
508
|
+
utils.broadcastToRoom(client.roomName, {
|
|
509
|
+
type: "system",
|
|
510
|
+
text: `${oldDisplayName} changed their name to ${client.displayName}`,
|
|
511
|
+
roomName: client.roomName,
|
|
512
|
+
timestamp: Date.now()
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Relay game message to other players in the room
|
|
519
|
+
* Server doesn't care about message content - just routes it
|
|
520
|
+
*/
|
|
521
|
+
function relayGameMessage(ws, message) {
|
|
522
|
+
const client = utils.getClient(ws);
|
|
523
|
+
if (!client || !client.roomName) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Simply broadcast the entire message to room, excluding sender
|
|
528
|
+
utils.broadcastToRoom(client.roomName, message, ws);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Initialize game state for a new room
|
|
533
|
+
*/
|
|
534
|
+
function initializeGameRoom(roomName) {
|
|
535
|
+
gameRooms.set(roomName, {
|
|
536
|
+
players: new Map(),
|
|
537
|
+
startTime: Date.now(),
|
|
538
|
+
gameStarted: false
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
console.log(`Initialized game state for room: ${roomName}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Clean up game state for a room
|
|
546
|
+
*/
|
|
547
|
+
function cleanupGameRoom(roomName) {
|
|
548
|
+
gameRooms.delete(roomName);
|
|
549
|
+
console.log(`Cleaned up game state for room: ${roomName}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Start the server
|
|
553
|
+
server.listen(port, () => {
|
|
554
|
+
console.log(`Secure Multiplayer Game server running on wss://localhost:${port}`);
|
|
555
|
+
console.log("Press Ctrl+C to stop the server");
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Graceful shutdown
|
|
559
|
+
process.on("SIGINT", () => {
|
|
560
|
+
console.log("\nShutting down Game server...");
|
|
561
|
+
wss.clients.forEach((client) => {
|
|
562
|
+
client.close();
|
|
563
|
+
});
|
|
564
|
+
server.close(() => {
|
|
565
|
+
console.log("Game server stopped");
|
|
566
|
+
process.exit(0);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Handle uncaught exceptions
|
|
571
|
+
process.on("uncaughtException", (error) => {
|
|
572
|
+
console.error("Uncaught Exception:", error);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
577
|
+
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
});
|