mirror of
https://github.com/danbulant/slightlyComplicatedTicTacToe
synced 2026-06-19 14:31:16 +00:00
330 lines
13 KiB
JavaScript
330 lines
13 KiB
JavaScript
const decoder = new TextDecoder();
|
|
const PORT = 8080;
|
|
|
|
/**
|
|
* @typedef Room
|
|
* @property {string} name
|
|
* @property {Client} host
|
|
* @property {Client[]} clients
|
|
*/
|
|
/**
|
|
* @typedef Client
|
|
* @property {string} name
|
|
* @property {WebSocket} connection
|
|
* @property {Room?} room
|
|
*/
|
|
|
|
/**
|
|
* @type {Map<string, Room>}
|
|
*/
|
|
const rooms = new Map();
|
|
/**
|
|
* @type {Map<WebSocket, Client>}
|
|
*/
|
|
const clients = new Map();
|
|
|
|
function deleteRoom(room) {
|
|
rooms.delete(room);
|
|
|
|
for(let [, client] of clients) {
|
|
if(!client.room) {
|
|
client.connection.send(
|
|
JSON.stringify({ t: "room_deleted", name: room })
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
require("uWebSockets.js")
|
|
.App({})
|
|
.ws("/", {
|
|
idleTimeout: 32,
|
|
maxPayloadLength: 16 * 1024 * 1024,
|
|
upgrade: (res, req, context) => {
|
|
console.log(
|
|
`An Http connection wants to become WebSocket, URL: ${req.getUrl()}?${req.getQuery()}!`
|
|
);
|
|
let name = req.getQuery("name").trim().toLocaleLowerCase();
|
|
if (
|
|
!name ||
|
|
typeof name !== "string" ||
|
|
name.length < 2 ||
|
|
name.length > 64 ||
|
|
!name.trim()
|
|
) {
|
|
console.log("Invalid name");
|
|
return res.end("invalid_name");
|
|
}
|
|
name = name.trim();
|
|
if ([...clients.values()].find((client) => client.name === name)) {
|
|
console.log("Duplicate name");
|
|
return res.end("name_used");
|
|
}
|
|
/* This immediately calls open handler, you must not use res after this call */
|
|
res.upgrade(
|
|
{
|
|
name,
|
|
},
|
|
/* Spell these correctly */
|
|
req.getHeader("sec-websocket-key"),
|
|
req.getHeader("sec-websocket-protocol"),
|
|
req.getHeader("sec-websocket-extensions"),
|
|
context
|
|
);
|
|
},
|
|
open: (ws) => {
|
|
console.log(
|
|
"CON",
|
|
ws.name,
|
|
decoder.decode(ws.getRemoteAddressAsText())
|
|
);
|
|
clients.set(ws, {
|
|
connection: ws,
|
|
name: ws.name,
|
|
ipa: decoder.decode(ws.getRemoteAddressAsText()),
|
|
room: null,
|
|
});
|
|
},
|
|
message: (ws, message, isBinary) => {
|
|
if (isBinary) return ws.end(0, "invalid_message");
|
|
try {
|
|
const data = JSON.parse(decoder.decode(message));
|
|
const client = clients.get(ws);
|
|
console.log(client.name, data);
|
|
if (data.t === "ping") return ws.ping();
|
|
switch (data.t) {
|
|
case "ping": {
|
|
return ws.ping();
|
|
}
|
|
case "create": {
|
|
if (client.room)
|
|
return ws.send(
|
|
JSON.stringify({
|
|
t: "error",
|
|
e: "already_in_room",
|
|
})
|
|
);
|
|
const name = data.name.trim().toLocaleLowerCase();
|
|
if (
|
|
!name ||
|
|
typeof name !== "string" ||
|
|
name.length < 2 ||
|
|
name.length > 64 ||
|
|
!name.trim()
|
|
)
|
|
return res.send(
|
|
JSON.stringify({
|
|
t: "error",
|
|
e: "invalid_room_name",
|
|
})
|
|
);
|
|
if (
|
|
[...rooms.values()].find(
|
|
(room) => room.name === name
|
|
)
|
|
)
|
|
return ws.send(
|
|
JSON.stringify({
|
|
t: "error",
|
|
e: "room_name_used",
|
|
})
|
|
);
|
|
const room = {
|
|
name: name,
|
|
host: ws,
|
|
clients: [client],
|
|
};
|
|
rooms.set(room.name, room);
|
|
client.room = room;
|
|
for(let [, cclient] of clients) {
|
|
if(!cclient.room) {
|
|
cclient.connection.send(
|
|
JSON.stringify({ t: "room_created", name })
|
|
);
|
|
}
|
|
}
|
|
return ws.send(
|
|
JSON.stringify({ t: "create", name })
|
|
);
|
|
}
|
|
case "leave": {
|
|
const room = client.room;
|
|
if (!room)
|
|
return ws.send(
|
|
JSON.stringify({
|
|
t: "error",
|
|
e: "room_not_found",
|
|
})
|
|
);
|
|
if (!room.clients.includes(client))
|
|
return ws.send(
|
|
JSON.stringify({ t: "error", e: "not_in_room" })
|
|
);
|
|
room.clients.splice(room.clients.indexOf(client), 1);
|
|
if (room.clients.length === 0) {
|
|
deleteRoom(room.name);
|
|
} else if (room.host == ws) {
|
|
room.host = room.clients[0];
|
|
room.clients.forEach((client) =>
|
|
client.connection.send(
|
|
JSON.stringify({
|
|
t: "host",
|
|
host: client.name,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
client.room = null;
|
|
room.clients.forEach((client) =>
|
|
client.connection.send(
|
|
JSON.stringify({
|
|
t: "leave",
|
|
client: client.name,
|
|
})
|
|
)
|
|
);
|
|
ws.send(
|
|
JSON.stringify({ t: "left", name: client.name })
|
|
);
|
|
break;
|
|
}
|
|
case "join": {
|
|
if (!client) return ws.end(0, "missing_client");
|
|
const room = rooms.get(data.name);
|
|
if (!room)
|
|
return ws.send(
|
|
JSON.stringify({
|
|
t: "error",
|
|
e: "room_not_found",
|
|
})
|
|
);
|
|
if (client.room)
|
|
return ws.send(
|
|
JSON.stringify({
|
|
t: "error",
|
|
e: "already_in_other_room",
|
|
})
|
|
);
|
|
if (room.clients.includes(client))
|
|
return ws.send(
|
|
JSON.stringify({
|
|
t: "error",
|
|
e: "already_in_room",
|
|
})
|
|
);
|
|
if (room.clients.length > 5)
|
|
return ws.send(
|
|
JSON.stringify({ t: "error", e: "room_full" })
|
|
);
|
|
room.clients.push(client);
|
|
client.room = room;
|
|
const srcclient = client;
|
|
room.clients
|
|
.filter((t) => t !== client)
|
|
.forEach((client) =>
|
|
client.connection.send(
|
|
JSON.stringify({
|
|
t: "join",
|
|
client: srcclient.name,
|
|
})
|
|
)
|
|
);
|
|
ws.send(
|
|
JSON.stringify({
|
|
t: "joined",
|
|
name: room.name,
|
|
client: client.name,
|
|
host: room.host.name,
|
|
clients: room.clients.map((t) => t.name),
|
|
})
|
|
);
|
|
break;
|
|
}
|
|
case "broadcast": {
|
|
if (!client) return ws.end(0, "missing_client");
|
|
const room = client.room;
|
|
if (!room)
|
|
return ws.send(
|
|
JSON.stringify({
|
|
t: "error",
|
|
e: "room_not_found",
|
|
})
|
|
);
|
|
|
|
if (!room.clients.includes(client))
|
|
return ws.send(
|
|
JSON.stringify({ t: "error", e: "not_in_room" })
|
|
);
|
|
|
|
let clientName = client.name
|
|
room.clients.forEach((client) => {
|
|
client.connection.send(
|
|
JSON.stringify({
|
|
t: "broadcast",
|
|
client: clientName,
|
|
d: data.d,
|
|
})
|
|
);
|
|
});
|
|
}
|
|
case "list": {
|
|
ws.send(
|
|
JSON.stringify({
|
|
t: "list",
|
|
rooms: [...rooms.values()]
|
|
.filter((t) => t.clients.length < 2)
|
|
.map((t) => ({
|
|
name: t.name,
|
|
host: t.host.name,
|
|
count: t.clients.length,
|
|
})),
|
|
})
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn(e);
|
|
return ws.end(0, "internal");
|
|
}
|
|
},
|
|
close: (ws, code, message) => {
|
|
console.log("DIS1", ws, code, message);
|
|
try {
|
|
if (clients.get(ws)) {
|
|
const client = clients.get(ws);
|
|
console.log("DIS", client.name, client.ipa);
|
|
let room = client.room;
|
|
if (room) {
|
|
room.clients.splice(room.clients.indexOf(client), 1);
|
|
if (room.clients.length === 0) {
|
|
deleteRoom(room.name);
|
|
} else if (room.host == ws) {
|
|
room.host = room.clients[0];
|
|
room.clients.forEach((client) =>
|
|
client.connection.send(
|
|
JSON.stringify({
|
|
t: "host",
|
|
host: client.name,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
const srcclient = client;
|
|
room.clients.forEach((client) =>
|
|
client.connection.send(JSON.stringify({ t: "leave", client: srcclient.name }))
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn("Error during closing", e);
|
|
}
|
|
clients.delete(ws);
|
|
},
|
|
})
|
|
.get("/*", (res, req) => {
|
|
res.writeStatus("200 OK").writeHeader("Content-Type", "text/plain").end("OK");
|
|
})
|
|
.listen(PORT, () => {
|
|
console.log(`Listening on port ${PORT}`);
|
|
});
|