Created
December 6, 2011 10:34
-
-
Save valscion/1437717 to your computer and use it in GitHub Desktop.
NetMatch server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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