Skip to content

Instantly share code, notes, and snippets.

@valscion
Created December 6, 2011 10:34
Show Gist options
  • Save valscion/1437717 to your computer and use it in GitHub Desktop.
Save valscion/1437717 to your computer and use it in GitHub Desktop.
NetMatch server
var dgram = require('dgram'),
http = require('http'),
Buffer = require('buffer').Buffer,
util = require('util');
function Player () {
this.id = "";
this.active = false;
this.loggedIn = false;
this.name = "";
this.skill = 21 - this.id;
this.fightRotate = 1.5 + this.skill / 1.5;
this.shootingAngle = 4.0 + this.id * 1.5;
this.fov = 100 + this.skill * 3.5;
this.hasAmmo = false;
this.admin = false;
this.kicked = false;
this.kickedReason = "";
this.zombie = true;
this.health = 100;
}
Array.prototype.flatten = function flatten (){
var flat = [];
for (var i = 0, l = this.length; i < l; i++){
var type = Object.prototype.toString.call(this[i]).split(' ').pop().split(']').shift().toLowerCase();
if (type) { flat = flat.concat(/^(array|collection|arguments|object)$/.test(type) ? flatten.call(this[i]) : this[i]); }
}
return flat;
};
Number.prototype.toBytes = function () {
return [
(this >> 0) & 0xFF,
(this >> 8) & 0xFF,
(this >> 16) & 0xFF,
(this >> 24) & 0xFF
];
};
String.prototype.toBytes = function () {
var a = this.length.toBytes();
for(var i = 0; i < this.length; ++i) {
a.push(this.charCodeAt(i));
}
return a;
};
function int4(i) {
return Number(i).toBytes();
}
function short(i) {
return [
(this >> 0) & 0xFF,
(this >> 8) & 0xFF
];
}
function Packet (buffer) {
this.buf = buffer;
this.ind = 0;
this.b = function () { // READ A BYTE
var bit = this.buf[this.ind];
this.ind++;
return bit;
}
this.s = function () { // READ A STRING
var l = this.i(),
sb = this.buf.slice(this.ind, this.ind + l);
this.ind = this.ind + l
return sb.toString();
}
this.i = function () { // READ AN INTEGER
var a = ((this.buf[this.ind + 3] & 0xFF) << 24) +
((this.buf[this.ind + 2] & 0xFF) << 16) +
((this.buf[this.ind + 1] & 0xFF) << 8 ) +
((this.buf[this.ind ] & 0xFF) << 0 );
if (this.buf[this.ind + 3] >> 7 & 0xFF) {a = a - 4294967296;}
this.ind = this.ind + 4;
return a;
}
this.sh = function () { // READ A SHORT
var a = ((this.buf[this.ind + 1] & 0xFF) << 8 ) +
((this.buf[this.ind ] & 0xFF) << 0 );
if (this.buf[this.ind + 1] >> 7 & 0xFF) {a = a - 65536;}
this.ind = this.ind + 2;
return a;
}
};
var nm = {
config: {
regHost: "netmatch.vesq.org",
regPath: "/reg/gss.php",
port: 61821,
register: false,
description: "Node.js :O powered server by tuhoojabotti",
map: 0,
mapLoop: 0,
mapList: "",
version: "v2.4",
maxPlayers: 5,
registered: false,
closing: false,
gameMode: "DM",
spawnProtection: 15000
},
players: [],
messages: [],
createServer: function () {
if (nm.config.register && nm.config.description) {
console.log("Registering server...");
nm.registerServer();
}
// Init players
for(var i = 0; i < nm.config.maxPlayers; ++i) {
var pl = new Player();
pl.name = "bot" + i
pl.pid = i + 1;
pl.x = Math.round(-100 + Math.random() * 200);
pl.y = Math.round(-100 + Math.random() * 200);
//pl.active = true;
//pl.zombie = true;
nm.players.push(pl);
}
//console.log(nm.players[0]);
console.log("Server started!");
},
destroyServer: function () {
nm.unRegisterServer();
nm.config.closing = true;
console.log("Waiting for clients...");
// Give 2,5s for clients to contact for the last time.
setTimeout(function () {
console.log("Bye, C'ya soon!");
httpserv.close(); // No more web
sock.close(); // No more udp
process.exit(); // Foreveralone.jpg
}, 2500);
},
registerServer: function () {
var p = nm.config.regPath +
'?profile=NetMatch' +
'&ver=2.4' +
'&mode=reg' +
'&desc=' + escape(nm.config.description) +
'&port=' + nm.config.port;
http.get({
host: nm.config.regHost,
path: p
}, function (res) {
var data = "";
if (res.statusCode !== 200) { console.log("Registering failed!"); return; }
res.on('data', function (chunk) {data += chunk;});
res.on('end', function () {
if (data !== "ok") {
console.log("Registering failed: " + data);
} else {
nm.config.registered = true;
console.log("Register OK!");
}
});
}).on('error', function (e) {
console.log("Registering failed: " + e.message);
});
},
unRegisterServer: function () {
console.log("UnRegistering server");
if (nm.config.registered === true) {
var p = nm.config.regPath +
'?profile=NetMatch' +
'&mode=unreg' +
'&port=' + nm.config.port;
http.get({
host: nm.config.regHost,
path: p
}).on('error', function (e) {
console.log("UnRegistering failed: " + e.message);
});
nm.config.registered = false;
}
},
replyClient: function (peer, arr) {
var rep = new Buffer(arr.flatten()); // Flatten
//console.log(rep);
sock.send(rep, 0, rep.length, peer.port, peer.address);
},
handlePacket: function (buf, peer) {
//console.log(peer);
//console.log(buf);
var profile,
p = new Packet(buf.slice(4)),
type = p.b(),
playerID,
player,
sendNames = false,
replyData = [];
//console.log("Packet reseived (" + nm.nets[type] + "): " + buf.toString());
if (type === nm.net.login) { ///////////////////////////////////////////////////// LOGIN
if (p.s() === nm.config.version) {
profile = p.s();
for(var i = 0; i < nm.config.maxPlayers; ++i) {
var pl = nm.players[i];
if (pl.name === profile.toLowerCase()) {
if (pl.kicked === true || pl.active === false) {
pl.name = "";
} else { // Nick already in use!
console.log("ERR: Nick already in use!");
nm.replyClient(peer, [
int4(0),
nm.net.login,
nm.net.loginfailed,
nm.net.nicknameinuse
]);
return;
}
}
}
// Find open slot for player
for(var i = 0; i < nm.config.maxPlayers; ++i) {
var pl;
if (!nm.players[i].active) {
delete nm.players[i];
pl = new Player();
pl.pid = i + 1;
pl.id = peer.address + ':' + peer.port;
pl.active = true;
pl.loggedIn = false;
pl.name = profile;
//pl.x = 0;
//pl.y = 0;
pl.angle = 0;
pl.zombie = false;
pl.health = 100;
pl.kills = 0;
pl.deaths = 0;
pl.weapon = nm.wpn.pistol;
pl.lastAct = new Date().getMilliseconds();
pl.spawnT = new Date().getMilliseconds();
pl.admin = false;
pl.kicked = false;
pl.kickedReason = "";
pl.zombie = false;
if (nm.config.gameMode === 'TDM') {
pl.team = 1 + Math.round(Math.random());
}
nm.players[i] = pl;
// Answer to client
nm.replyClient(peer, [
Number(0).toBytes(),
nm.net.login,
nm.net.loginok,
pl.pid,
1,
"Luna".toBytes(),
Number(-1170754068).toBytes(),
"".toBytes()
]);
console.log(pl.name + " logged in (" + peer.address + ")");
// Send data about other players
//setInterval(nm.updateClient, 100, peer, true);
//nm.updateClient(peer, true);
return;
}
}
} else { // Wrong client version!
nm.replyClient(peer, [
int4(0),
nm.net.login,
nm.net.loginfailed,
nm.net.wrongversion,
nm.config.version.toBytes()
]);
return;
}
}
///// SERVER STUFF /////
playerID = p.b(); // All packets have this except net.login
if (playerID < 1 || playerID > nm.config.maxPlayers) {return;}
player = nm.playerByID(playerID);
// Check if playes is kicked
if (player.kicked) {
nm.replyClient(peer, [
nm.net.kicked,
player.kickerID,
playerID,
player.kickedReason.toBytes()
]);
return;
}
// Check if this is an active player
if (!player.active) {
nm.replyClient(peer, [nm.net.nologin]);
return;
}
// Update player's lag
player.lag = new Date().getMilliseconds() - player.lastAct;
// Update player's existance
player.lastAct = new Date().getMilliseconds()
switch(type) {
case nm.net.logout: ////////////////////////////////////////////////////////// LOGOUT
if (player) {
player.active = player.loggedIn = player.admin = false;
console.log(player.name + " logged out");
return;
}
break;
case nm.net.playername: //////////////////////////////////////////////////////// PLAYERNAME
sendNames = true;
break;
case nm.net.player: ////////////////////////////////////////////////////////// PLAYER
var x, y, a, b, wep, amm, sho, pic;
x = p.sh();
y = p.sh();
a = p.sh();
b = p.b(); // A byte with multiple values
// Unpack the byte:
wep = (b << 28) >> 28; // Weapon
amm = (b << 27) >> 31; // hasAmmo
sho = (b << 26) >> 31; // Shooting
// Picked item ID (0 if none)
pic = p.b();
// Only update player if not dead
if (!player.death) {
player.x = x;
player.y = y;
player.angle = a;
player.weapon = wep;
player.hasAmmo = amm;
//if (sho) {nm.createBullet(playerID);}
if (pic) { // Picking an item
// IMPLEMENT ME!
}
}
if (player.health) {player.death = false;}
if (!player.loggedIn) {player.loggedIn = true;}
break;
default: ////////////////////////////////////////////////////////// UNIMPLEMENTED
console.log("UNIMPLEMENTED! (" + type + "): " + nm.nets[type]);
}
//console.log(sendNames);
nm.updateClient(peer, true);//sendNames);
},
updateClient: function (to, sendNames) {
///// SEND ALL PLAYER's DATA TO <to> /////
var replyData = []; // Gather all data here.
//console.log("update to "+to.address+":"+to.port);
for(var i = 0; i < nm.config.maxPlayers; ++i) {
pl = nm.players[i];
if (sendNames && pl.active) {
replyData.push([
int4(0),
nm.net.playername,
pl.pid,
pl.name.toBytes(),
pl.zombie,
pl.team
].flatten());
}
// IMPLEMENT TEXT MESSAGES HERE :)
// Update visible and alive players
var visible = true;
//if (Math.abs(player.x - pl.x) > 450 || Math.abs(player.y - pl.y) > 350) {visible = false;}
if (sendNames || visible || pl.health <= 0) {
b = (pl.weapon % 16) << 0; // Weapon
b += pl.hasAmmo << 4; // Ammo
b += (pl.team == 2) << 6; // Team and Spawn protection --v
b += (pl.spawnTime + nm.config.spawnProtection > new Date().getMilliseconds()) << 7;
replyData.push([
nm.net.player,
pl.pid,
short(pl.x),
short(pl.y),
short(pl.angle),
b,
pl.health,
short(pl.kills),
short(pl.deaths)
].flatten());
}
// IMPLEMENT RADAR ARROWS HERE
// ALSO IMPLEMENT BULLETS, SERVERMSG, ITEM, KILL ETC.. xD
}
replyData.push(nm.net.end);
nm.replyClient(to, replyData);
},
kickPlayer: function (pid, kid, reason) {},
parseCommand: function (c, pid) {},
setTeam: function (pid, team) {},
playerByID: function (pid) {
for(var i = 0; i < nm.config.maxPlayers; ++i) {
if (nm.players[i].pid === pid) {
return nm.players[i];
}
}
},
setBotLimit: function () {},
net: {
login: 1,
logout: 2,
loginfailed: 3,
loginok: 4,
wrongversion: 5,
toomanyplayers: 6,
nologin: 7,
player: 8,
newbullet: 9,
playername: 10,
textmessage: 11,
bullethit: 12,
radar: 13,
item: 14,
killmessage: 15,
sessiontime: 16,
banned: 17,
mapchange: 18,
kicked: 19,
servermsg: 20,
nicknameinuse: 21,
serverclosing: 22,
teaminfo: 23,
speedhack: 24,
end: 255
},
nets: {}, //Object.keys(this.net).reduce(function (acc, key) { acc[this.net[key]] = key; return acc }, {})
wpn: {
pistol: 1,
mgun: 2,
bazooka: 3,
shotgun: 4,
launcher: 5,
chainsaw: 6,
count: 6
},
wpns: {}
};
///////////////////////////////////////////////////////////
// Open up an UDP Socket
sock = dgram.createSocket("udp4", function (pack, peer) {
if (!nm.config.closing) {
nm.handlePacket(pack, peer)
} else { // Killing the server, must the the clients
nm.replyClient(peer, [
int4(0),
nm.net.serverclosing,
nm.net.end
]);
}
});
sock.on('listening', function() {
console.log('Listening on port ' + nm.config.port);
nm.nets = Object.keys(nm.net).reduce(function (acc, key) { acc[nm.net[key]] = key; return acc }, {});
nm.wpns = Object.keys(nm.wpn).reduce(function (acc, key) { acc[nm.wpn[key]] = key; return acc }, {});
nm.createServer();
});
sock.bind(nm.config.port, '217.30.184.184');
// Listen to STDIN
process.stdin.resume();
process.on('SIGINT', function () {
console.log('\nKilling server!');
nm.destroyServer();
});
// Also run a HTTP Server :)
var httpserv = http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.write('Running NetMatch server! :O)\n\n');
response.write('Players:\n');
for(var i = 0; i < nm.config.maxPlayers; ++i) {
var pl = nm.players[i]
if(pl.name !== '' && pl.active === true) {
response.write(pl.name + ' ('+ pl.x + ', ' + pl.y + ') weapon: ' + nm.wpns[pl.weapon] + '\n');
response.write(util.inspect(pl));
}
response.end();
}
});
httpserv.listen(nm.config.port);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment