first commit! 🎉

This commit is contained in:
ari melody 2024-08-30 03:56:03 +01:00
commit c35c18bbbc
Signed by: ari
GPG key ID: CF99829C92678188
19 changed files with 1046 additions and 0 deletions

112
server/main.js Normal file
View file

@ -0,0 +1,112 @@
import http from "http";
import path from "path";
import fs, { openSync } from "fs";
import Log from "../common/log.js";
import { init as initWS } from "./ws.js";
var PORT = 3000;
var mime_types = {
'html': 'text/html',
'css': 'text/css',
'js': 'application/javascript',
'png': 'image/png',
'jpg': 'image/jpg',
'gif': 'image/gif',
};
process.argv.forEach((arg, index) => {
if (index < 2) return;
if (!arg.startsWith('-')) return;
switch (arg.substring(1)) {
case "port":
if (process.argv.length < index + 2) {
Log.error("FATAL: -port was supplied with no arguments.");
exit(1);
}
var val = process.argv[index + 1];
if (val.startsWith('-')) {
Log.error("FATAL: -port was supplied with no arguments.");
exit(1);
}
var port = Number(val);
if (isNaN(port)) {
Log.error("FATAL: -port was supplied with invalid arguments.");
exit(1);
}
PORT = port;
break;
}
});
const server = http.createServer(async (req, res) => {
var code = await new Promise((resolve) => {
if (req.method !== "GET") {
res.writeHead(501, {
"Content-Type": "text/plain",
"Server": "ari's awesome server"
});
res.end("Not Implemented");
return resolve(501);
}
// find the file
var filepath = path.join("client", req.url);
if (req.url.startsWith("/common/")) {
filepath = path.join("common", req.url.slice(8));
}
if (filepath.endsWith('/') || filepath.endsWith('\\')) {
filepath = path.join(filepath, "index.html");
}
if (!fs.existsSync(filepath)) {
res.writeHead(404, {
"Content-Type": "text/plain",
"Server": "ari's awesome server"
});
res.end("Not Found");
return resolve(404);
}
try {
var ext = path.extname(filepath).slice(1);
var mime_type = mime_types[ext] || "application/octet-stream";
const stream = fs.createReadStream(filepath);
res.writeHead(200, {
"Content-Type": mime_type,
"Server": "ari's awesome server"
});
stream.pipe(res);
return resolve(200);
stream.on("open", () => {
res.writeHead(200, {
"Content-Type": mime_type,
"Server": "ari's awesome server"
});
stream.pipe(res);
res.end();
return resolve(200);
});
stream.on("error", error => {
throw error;
});
} catch (error) {
Log.error(error);
res.writeHead(500, {
"Content-Type": "text/plain",
"Server": "ari's awesome server"
});
res.end("Internal Server Error");
return resolve(500);
}
});
Log.info(`${code} - ${req.method} ${req.url}`);
});
initWS(server);
server.on("listening", () => {
Log.info("server listening on port " + PORT);
});
server.listen(PORT, "0.0.0.0")

231
server/ws.js Normal file
View file

@ -0,0 +1,231 @@
import { WebSocketServer } from "ws";
import Log from "../common/log.js";
import { WORLD_SIZE } from "../common/world.js";
import Player from "../common/player.js";
import Prop from "../common/prop.js";
const TICK_RATE = 30;
var clients = [];
var props = {
1: new Prop("the silly",
WORLD_SIZE / 2, WORLD_SIZE / 2,
"#ff00ff", "/img/ball.png")
};
var last_update = 0.0;
var ticks = 0;
var wss;
export function init(http_server) {
wss = new WebSocketServer({ server: http_server });
wss.on("connection", (socket) => {
clients.push(socket);
socket.on("error", error => {
Log.warn("Websocket connection closed due to error: " + error);
if (socket.player) {
broadcast({
type: "leave",
id: socket.id,
});
}
clients = clients.filter(s => s != socket);
});
socket.on("close", () => {
if (socket.player) {
Log.info(socket.player.name + " left the game.");
broadcast({
type: "leave",
id: socket.id,
});
}
clients = clients.filter(s => s != socket);
});
socket.on("message", msg => {
try {
const data = JSON.parse(msg);
if (!data.type)
throw new Error("Type not specified");
switch (data.type) {
case "join":
if (!data.name)
throw new Error("Name cannot be null");
var player_name = data.name.slice(0, 32);
player_name = player_name.replaceAll('<', '&lt;');
player_name = player_name.replaceAll('>', '&gt;');
player_name = player_name.trim();
socket.id = generateID();
socket.player = new Player(
data.name.slice(0, 32),
WORLD_SIZE / 2,
WORLD_SIZE / 2,
randomColour()
);
Log.info(socket.player.name + " joined the game.");
var lobby_players = {};
clients.forEach(client => {
if (!client.player) return;
lobby_players[client.id] = {
name: client.player.name,
x: client.player.x,
y: client.player.y,
col: client.player.colour,
};
});
var lobby_props = {};
Object.keys(props).forEach(id => {
const prop = props[id];
lobby_props[id] = {
name: prop.name,
x: prop.x,
y: prop.y,
col: prop.colour,
sprite: prop.sprite,
}
});
socket.send(JSON.stringify({
type: "welcome",
id: socket.id,
tick: ticks,
players: lobby_players,
props: lobby_props,
}));
clients.forEach(s => {
if (s.id == socket.id) return;
// send player joined event
s.send(JSON.stringify({
type: "join",
id: socket.id,
name: socket.player.name,
x: socket.player.x,
y: socket.player.y,
col: socket.player.colour,
}));
});
break;
case "update":
if (!socket.player)
throw new Error("Player does not exist");
if (data.x === undefined || data.y === undefined)
throw new Error("Movement vector not provided");
if (data.tick === undefined)
throw new Error("User tick not provided");
socket.player.tick = data.tick;
socket.player.in_x = Math.min(1.0, data.x);
socket.player.in_y = Math.min(1.0, data.y);
break;
case "chat":
if (data.msg === undefined)
throw new Error("Attempted chat with no message");
Log.info('<' + socket.player.name + '> ' + data.msg)
data.msg = data.msg.replaceAll("<", "&lt;")
data.msg = data.msg.replaceAll(">", "&gt;")
data.msg = data.msg.replaceAll("\n", "");
data.msg = data.msg.trim();
if (data.msg == "") return;
clients.forEach(client => {
client.send(JSON.stringify({
type: "chat",
player: socket.id,
msg: data.msg,
}));
});
break;
default:
throw new Error("Invalid message type");
}
} catch (error) {
if (socket.player) {
Log.warn("Received invalid packet from " + socket.player.id + ": " + error);
socket.send(JSON.stringify({
type: "kick",
reason: "Received invalid packet",
}));
} else {
Log.warn("Received invalid packet: " + error);
}
socket.close();
}
});
});
update();
}
function update() {
var delta = (performance.now() - last_update) / 1000;
// update players
var frame_players = {};
clients.forEach(client => {
if (!client.player) return;
client.player.update(delta);
client.player.x = Math.max(Math.min(client.player.x, WORLD_SIZE), 0);
client.player.y = Math.max(Math.min(client.player.y, WORLD_SIZE), 0);
frame_players[client.id] = {
x: client.player.x,
y: client.player.y,
};
});
// god help me this code is awful
// really leaning on this just being a tech demo here
var prop_players = [];
clients.forEach(client => {
if (client.player) prop_players.push(client.player);
});
var frame_props = {};
Object.keys(props).forEach(id => {
const prop = props[id];
prop.update(delta, prop_players);
frame_props[id] = {
x: prop.x,
y: prop.y,
}
});
// send update to players
clients.forEach(client => {
if (!client.player) return;
client.send(JSON.stringify({
type: "update",
tick: client.player.tick,
players: frame_players,
props: frame_props,
}));
})
last_update = performance.now();
ticks++;
setTimeout(update, 1000 / TICK_RATE);
}
function broadcast(data) {
clients.forEach(socket => {
socket.send(JSON.stringify(data));
});
}
function generateID() {
// five random digits followed by five digits from the end of unix timestamp
return (10000 + Math.floor(Math.random() * 90000)).toString() + (new Date() % 100000).toString();
}
function randomColour() {
var res = "#";
for (var i = 0; i < 6; i++)
res += "0123456789abcdef"[Math.floor(Math.random() * 16)];
return res;
}