// TODO: Removing socket from zone on disconnet.
vmscript.watch('Config/world.json');
vmscript.watch('Config/network.json');
vmscript.watch('Config/motion.json');
vmscript.watch('Config/motion_monster.json');
/** @exports Zone */
vms('Zone', [
'Config/world.json',
'Config/network.json',
'Config/motion.json',
'Config/motion_monster.json'
], function() {
global.api.zoneAlive = function(callback, client){
console.log("Got zone alive callback", client);
var c = Zone.clientHashTable[client];
if(!c){
console.log("Checking one alive for unknown client");
return;
}
global.rpc.getCallback(callback).apply(c);
};
global.api.onMoveRegions = function(callback, client, moveRegions){
console.log("Got move regions callback", client);
var c = Zone.clientHashTable[client];
if(!c){
console.log("Checking move region for unknown client in zone", Zone.id);
return;
}
global.rpc.getCallback(callback).call(c, moveRegions);
};
global.api.onCharacterZone = function(user_hash, zone, character_name, callback){
var c = Zone.clientHashTable[user_hash];
if(!c){
console.log("On character zone from world have not found client with hash". user_hash);
return;
}
rpc.getCallback(callback).call(c, character_name, zone);
};
global.api.invalidateGuildForClient = function(client){
var c = Zone.clientNameTable[client];
if(!c){
return;
}
if(c.writable && c.character.state.Guild){
c.character.state.Guild.invalidate(c.character.state, function(guild){
c.write(new Buffer(Zone.send.onGuildToClient.pack({
PacketID: 0x54,
Switch: 0x42,
InvitedBy: guild.name,
GuildName: guild.name
})));
console.log(c.character.Name);
c.character.state.setGuild(guild);
// Find a better way maybe.
Zone.sendToAllArea(c, false, c.character.state.getPacket(), config.network.viewable_action_distance);
});
}
};
global.api.sendBufferToClient = function(client, buffer){
var c = Zone.clientNameTable[client];
if(!c){
return;
}
if(c.writable) c.write(buffer.buffer);
};
global.api.expelFromGuild = function(client, buffer){
var c = Zone.clientNameTable[client];
if(!c){
return;
}
console.log("SOme1 gots expelled :D");
c.write(buffer.buffer);
c.character.state.removeGuild();
c.character.GuildName = null;
Zone.sendToAllArea(c, true, c.character.state.getPacket(), config.network.viewable_action_distance);
};
global.api.reloadCharacterData = function(name){
var client = Zone.clientNameTable[name];
if(!client){
log.info("Could not reload data for", name);
return;
}
db.Character.findOne({Name: name}, function(err, character){
if(err){
return;
}
if(!character){
return;
}
for(var name in character){
client.character[name] = character[name];
}
var CharacterData = new Buffer(Zone.send.CharacterInfo.pack({
PacketID: 0x16,
Status: 0,
character: client.character,
Unknown: 0x00
}));
CharacterData = client.character.restruct(CharacterData);
client.write(CharacterData);
Zone.sendToAllArea(client, true, client.character.state.getPacket(), config.network.viewable_action_distance);
});
};
global.rpc.add(global.api);
/**
* Zone Instance, there should only be one per child zone process.
* @constructor
* @exports ZoneInstance
*/
function ZoneInstance() {
fs = require('fs');
util = require('./Modules/util.js');
CachedBuffer = require('./Modules/CachedBuffer.js');
PacketCollection = require('./Modules/PacketCollection.js');
restruct = require('./Modules/restruct');
Database = require('./Modules/db.js');
packets = require('./Helper/packets.js');
nav_mesh = require('./Modules/navtest-revised.js');
QuadTree = require('./Modules/QuadTree.js');
Random = require("random-js");
/**
* A seedable random engine.
* @type {Random}
*/
random = new Random(Random.engines.mt19937().autoSeed());
clone = require('clone');
bunyan = require('bunyan');
uuid = require('node-uuid');
vmscript.watch('./Generic');
vmscript.watch('./Helper/GMCommands.js');
this.initialized = false;
/**
* The packet collection for the zone server.
* @type {PacketCollection}
*/
this.packetCollection = null;
/**
* Socket transfer queue.
* This is used for zone transfers.
* @type {Object}
*/
this.socketTransferQueue = {};
this.send = {};
this.recv = {};
/**
* The zone ID.
* @type {Integer}
*/
this.id = parseInt(process.argv[2]);
/**
* The zone Name if possible.
* @type {String}
*/
this.name = process.argv[3];
/**
* The zones display name if possible.
* Configured in Config/zones.json
* @type {String}
*/
this.display_name = process.argv[4] || ''+this.id;
/**
* A cleaned up name of the zones display name for some reason.
* @type {String}
*/
this.clean_name = this.display_name.replace(/[\s#@]/gi, '');
/**
* AI Object
* @todo AI is a work in progress.
* @type {Object}
*/
this.AI = null;
/**
* QuadTree used to search quickly for entities.
* @type {QuadTree}
*/
this.QuadTree = null;
/**
* An array of clients connected to the zone.
* @type {Array}
*/
this.Clients = [];
/**
* An array of NPC on the zone.
* @type {Array}
*/
this.Npc = [];
/**
* An array of Items on the zone. (On ground)
* @type {Array}
*/
this.Items = [];
/**
* An array of NPC on the zone.
* @type {Array}
*/
this.NpcNodesHashTable = {};
this.clientHashTable = {};
this.clientNodeTable = {};
this.clientNameTable = {};
this.itemSlotSizes = {};
/**
* An array of Monsters on the zone.
* @type {Array}
*/
this.Monsters = [];
this.UniqueID = 0;
this.ExpInfo = {};
/**
* An instance of a bunyan logger for the zone to use.
* @type {Bunyan}
*/
global.log = bunyan.createLogger({
name: 'InfiniteSky/Zone.' + parseInt(process.argv[2]),
streams: [{
stream: process.stderr
}]
});
/**
* An uncaught exception handler.
*/
util.setupUncaughtExceptionHandler(function(err){ log.error(err); });
}
if (!global.zonePrototype) {
zonePrototype = ZoneInstance.prototype;
};
/**
* Add an instance of an NPC to the zone.
* @param {Object} element Information describing the NPC.
*/
zonePrototype.addNPC = function(element){
var npc = new Npc(element);
npc.setNode(this.QuadTree.addNode(new QuadTree.QuadTreeNode({
object: npc,
update: function() {
this.x = this.translateX(this.object.Location.X);
this.y = this.translateY(this.object.Location.Z);
},
type: 'npc'
})));
this.Npc.push(npc);
};
zonePrototype.init = function Zone__init() {
var self = this;
// Only allow init once.
if (this.inited) return;
this.inited = true;
this.packetCollection = new PacketCollection('ZonePC');
global.ZonePC = this.packetCollection;
Database(config.world.database.connection_string, function() {
log.info("Zone database connected");
self.databaseConnected = true;
vmscript.watch('Database');
vmscript.watch('Generic');
vmscript.on([
'Database',
'Generic'
], function() {
vmscript.on('Exp', function(){
db.Exp.find(null, 'Level EXPStart EXPEnd SkillPoint', function(err, exp){
if(err){
return;
}
if(!exp.length){
return;
}
for(var i=0; i<exp.length; i++){
var info = exp[i];
self.ExpInfo[info.Level] = {
Start: info.EXPStart,
End: info.EXPEnd,
SkillPoints: info.SkillPoint
};
}
});
});
vmscript.on('ItemInfo', function(){
db.Item.find(null, '_id ItemType', function(err, docs){
if(err){
console.log(err);
return;
}
for(var i=0; i<docs.length; i++){
self.itemSlotSizes[docs[i]._id] = docs[i].getSlotSize();
}
docs.length = 0;
if(typeof global.gc === 'function') global.gc();
function vmscript_WatchIfExists(path) {
fs.stat(path, function(err, stat) {
if (err) {
// Safe to ignore errors for this sometimes they wont exist.
return;
}
vmscript.watch(path);
});
}
vmscript_WatchIfExists('./Commands');
vmscript_WatchIfExists('./Processes/Zone/Packets');
vmscript_WatchIfExists('./Processes/Zone/Packets/'+self.id);
vmscript_WatchIfExists('./Processes/Zone/Packets/'+self.clean_name);
vmscript_WatchIfExists('./Processes/Zone/Commands');
vmscript_WatchIfExists('./Processes/Zone/Commands/'+self.id);
vmscript_WatchIfExists('./Processes/Zone/Commands/'+self.clean_name);
vmscript_WatchIfExists('./Processes/Zone/Scripts/'+self.id);
vmscript_WatchIfExists('./Processes/Zone/Scripts/'+self.clean_name);
});
});
});
});
zonePrototype.initSpawn = function(){
var self = this;
fs.readFile(config.world.data_path + 'spawninfo/' + util.padLeft(self.id,'0', 3) + '.NPC', function(err, data) {
if (err) {
//console.log(err);
} else {
var RecordCount = data.readUInt32LE(0);
var spawndata = restruct.struct('info', structs.SpawnInfo, RecordCount).unpack(data.slice(4));
var length = spawndata.info.length,
element = null;
for (var i = 0; i < length; i++) {
element = spawndata.info[i];
if (element.ID) {
self.addNPC(element);
}
}
}
});
};
// Load Navigation Mesh
var mesh_path = config.world.data_path + "navigation_mesh/" + this.name + '.obj';
fs.stat(mesh_path, function(err) {
if (err) {
// TODO: Add excpetion handler if we have no mesh to set the dimensions for quadtree.
self.QuadTree = new QuadTree({
x: -10000,
y: -10000,
size: 20000
});
self.initSpawn();
return;
}
self.AI = new nav_mesh(mesh_path, function(mesh) {
var height = Math.abs(mesh.dimensions.bottom) + Math.abs(mesh.dimensions.top) + 2;
var width = Math.abs(mesh.dimensions.right) + Math.abs(mesh.dimensions.left) + 2;
self.QuadTree = new QuadTree({
x: util.roundDivisable(mesh.dimensions.left, 2)-1,
y: util.roundDivisable(mesh.dimensions.top, 2)+1,
size: util.roundDivisable(Math.max(width, height), 2)
});
self.initSpawn();
});
});
// Setup listener for process messages
process.on('message', function(arg1, arg2) {
self.onProcessMessage(arg1, arg2);
});
};
zonePrototype.addSocket = function(socket) {
if (!this.QuadTree) {
console.log("QuadTree is not initialized");
return false;
}
// Attach functions to the socket here
// TODO: See if we can get this to work prototype like.
socket.sendInfoMessage = function(type, message) {
if (arguments.length === 1) {
message = type;
type = ':INFO';
}
ZonePC.sendMessageToSocket(this, type, message);
};
socket.unhandledPacket = function(message) {
ZonePC.sendMessageToSocket(this, ':WARN', 'Unhandled Packet: '+message);
}
socket.send2F = function(){
this.write(new Buffer(structs.HealingReplyPacket.pack({
'PacketID': 0x2F,
'Level': this.character.Level,
'Experience': this.character.Experience,
'Honor': this.character.Honor,
'CurrentHP': this.character.state.CurrentHP,
'CurrentChi': this.character.state.CurrentChi,
'PetActivity': this.character.Pet === null ? 0 : this.character.Pet.Activity,
'PetGrowth': this.character.Pet === null ? 0 : this.character.Pet.Growth
})));
}
socket.node = this.QuadTree.addNode(new QuadTree.QuadTreeNode({
object: socket,
update: function() {
this.x = this.translateX(this.object.character.state.Location.X);
this.y = this.translateY(this.object.character.state.Location.Z);
},
type: 'client'
}));
socket.character.state.NodeID = socket.node.id;
var hash = uuid.v1();
socket.hash = hash;
this.clientHashTable[hash] = socket;
this.clientNodeTable[socket.node.id] = socket;
this.clientNameTable[socket.character.Name] = socket;
return true;
};
zonePrototype.pullStates = function(client) {
var types = ['npc', 'item', 'monster'];
if(!client.character.state.hasPlayersAround){
client.character.state.hasPlayersAround = true;
types.push('client');
}
var found = this.QuadTree.query({
CVec3: client.character.state.Location,
radius: config.network.viewable_action_distance,
type: types
});
for (var i = 0; i < found.length; i++) {
var f = found[i];
if(f.object !== client && f.type === 'client') client.write(f.object.character.state.getPacket());
else if(f.object !== client) client.write(f.object.getPacket());
}
};
zonePrototype.broadcastState = function(client){
var state = client.character.state.getPacket();
var found = this.QuadTree.query({
CVec3: client.character.state.Location,
radius: config.network.viewable_action_distance,
type: ['client']
});
for (var i = 0; i < found.length; i++) {
var f = found[i];
if(f.object !== client) f.object.write(state);
}
};
zonePrototype.sendToAllAreaLocation = function(location, distance, buffer) {
var found = this.QuadTree.query({
CVec3: location,
radius: distance,
type: ['client']
});
for (var i = 0; i < found.length; i++) {
var f = found[i];
f.object.write(buffer);
}
};
zonePrototype.sendToAllArea = function(client, self, buffer, distance) {
var found = this.QuadTree.query({
CVec3: client.character.state.Location,
radius: distance,
type: ['client']
});
for (var i = 0; i < found.length; i++) {
var f = found[i];
if (!self && f.object === client) continue;
if (f.object.write) f.object.write(buffer);
}
};
zonePrototype.sendToAllAreaClan = function(client, self, buffer, distance, clan){
if (clan === undefined) {
clan = client.character.Clan;
}
var found = this.QuadTree.query({ CVec3: client.character.state.Location, radius: distance, type: ['client'] });
for(var i=0; i<found.length; i++){
var f = found[i];
if(!self && f.object === client) continue;
if (f.object.character.Clan !== clan) continue;
if(f.object.write) f.object.write(buffer);
}
};
zonePrototype.onFindAccount = function(socket, err, account) {
socket.account = account;
}
// Moves a character socket to a location and optionally a zoneID (Not yet implemented)
// Returns false if it failed, true if success
zonePrototype.move = function zone_move_character_socket(socket, location, zoneID) {
//var ChangeZone = false;
// Teleport to zone
// Make sure zoneID is number.
// if(zoneID && zoneID != this.character.MapID) {
// var thePort = 0;
// var theIP = '';
// var status = 0;
// console.log("Teleporting to Zone ID's not tested yet");
// // Check if zone id exists
// var TransferZone = worldserver.findZoneByID(zoneID);
// if(TransferZone == null) {
// console.log("Zone not found");
// status = 1;
// this.write(
// new Buffer(
// packets.MapLoadReply.pack({
// packetID: 0x0A,
// Status: status,
// IP: theIP,
// Port: thePort
// })));
// return false;
// }
// console.log('Zone found');
// if(Location) {
// // Use the location
// console.log('Location set');
// this.character.state.Location.X = location.X;
// this.character.state.Location.Y = location.Y;
// this.character.state.Location.Z = location.Z;
// this.character.state.Skill = 0;
// this.character.state.Frame = 0;
// this.sendActionStateToArea();
// } else {
// // Get a location for the zone
// console.log('Finding portal 0 endpoint');
// var PortalEndPoint = TransferZone.getPortalEndPoint(0);
// if(PortalEndPoint) {
// console.log('Location set');
// this.character.state.Location.X = PortalEndPoint.X;
// this.character.state.Location.Y = PortalEndPoint.Y;
// this.character.state.Location.Z = PortalEndPoint.Z;
// // Get random spot in that radius?
// } else {
// console.log('Location not set');
// this.character.state.Location.X = 0;
// this.character.state.Location.Y = 0;
// this.character.state.Location.Z = 0;
// }
// }
// // The Character State object for use in world for moving and health etc.
// //this.character.state.setFromCharacter(this.character);
// //console.log(this.character.state.Location);
// // Ask the zones/mapservers if they are ready for connections
// // If not then set Status to 1
// //status = 1;
// // Add to WorldServer client transfer.
// // Set the zoneID and XYZ they are to goto.
// this.character.MapID = TransferZone.getID();
// this.zoneTransfer = true;
// this.zoneForceTransfer = true;
// worldserver.addSocketToTransferQueue(this);
// console.log('Tell client which map server to connect too');
// //socket.characters[gamestart.Slot].MapID << get the map id of character :P
// // Get world.clients ip, check if it is on lan with server,
// // if so send it servers lan ip and port
// // otherwise send it real world ip and port
// theIP = config.externalIP;
// if(_util.cleanIP(this.remoteAddress).indexOf('127') == 0) {
// theIP = '127.0.0.1'
// }
// console.log('IP for client to connect too before translation: ' + theIP);
// for(var i = 0; i < natTranslations.length; i++) {
// if(natTranslations[i].contains(_util.cleanIP(this.remoteAddress))) {
// theIP = natTranslations[i].ip;
// break;
// }
// }
// console.log('IP for client to connect too: ' + theIP);
// thePort = config.ports.world;
// console.log({
// packetID: 0x0A,
// Status: status,
// IP: theIP,
// Port: thePort
// });
// this.account.save();
// this.character.save();
// this.write(
// new Buffer(
// packets.MapLoadReply.pack({
// packetID: 0x0A,
// Status: status,
// IP: theIP,
// Port: thePort
// })));
// return true;
// }
var oldLocation = socket.character.state.Location.copy();
if(location) {
socket.character.state.Location.X = location.X;
socket.character.state.Location.Y = location.Y;
socket.character.state.Location.Z = location.Z;
}
// Send character update packet
socket.character.state.Skill = 0;
socket.character.state.Frame = 0;
var packet = socket.character.state.getPacket();
Zone.sendToAllAreaLocation(oldLocation, config.network.viewable_action_distance, packet);
Zone.sendToAllArea(socket, true, packet, config.network.viewable_action_distance);
return true;
};
zonePrototype.giveSkillPoints = function zone_giveSkillPoints(client, value) {
client.character.SkillPoints += value;
}
zonePrototype.giveItem = function zone_giveItem(client, itemID, amount, combine, enchant) {
client.sendInfoMessage('Pretend you got Item '+itemID);
}
/**
* Gives experience to the character on a client socket.
* This method will also handle restoring HP and Chi upon level up, sending the exp notification out
* and any other experience/level related things.
* @param {socket} client The client socket.
* @param {Integer} value The amount of experience points to give.
*/
zonePrototype.giveEXP = function zone_giveEXP(client, value) {
if(value <= 0) return;
var expInfo = this.ExpInfo[client.character.Level];
client.character.Experience += value;
var reminder = expInfo.End - client.character.Experience;
var levelUp = 0;
while(reminder < 0){
levelUp++;
expInfo = this.ExpInfo[client.character.Level + levelUp];
if(!expInfo) break;
client.character.Experience++;
client.character.SkillPoints += expInfo.SkillPoints;
client.character.StatPoints += (client.character.Level + levelUp) > 99 && (client.character.Level + levelUp) <= 112 ? 0 : (client.character.Level + levelUp) > 112 ? 30 : 5;
if(client.character.Level + levelUp === 145) break;
reminder = (expInfo.End - expInfo.Start) + reminder;
}
var maxExperience = this.ExpInfo[145].End;
if(client.character.Level + levelUp >= 145 || client.character.Experience > maxExperience){
levelUp = 145 - client.character.Level;
client.character.Experience = maxExperience;
client.character.Level = 145;
}else{
client.character.Level += levelUp;
}
if(levelUp > 0){
client.character.infos.updateAll(function(){
client.character.Health = client.character.infos.MaxHP;
client.character.Chi = client.character.infos.MaxChi;
client.character.state.update();
client.character.state.setFromCharacter(client.character);
client.character.save(function(err){
if(err){
return;
}
Zone.sendToAllAreaLocation(client.character.state.Location, config.viewable_action_distance, new Buffer(structs.LevelUpPacket.pack({
PacketID: 0x2E,
LevelsGained: levelUp,
CharacterID: client.character.state.CharacterID,
NodeID: client.character.state.NodeID
})));
client.send2F();
});
});
}else{
client.character.save(function(err){
if(err){
return;
}
client.send2F();
});
}
};
zonePrototype.onProcessMessage = function(type, socket) {
if (socket) switch (type) {
case 'socket':
// console.log(socket);
socket.on('close', function(err) {
if(socket.node){
console.log("Node removed");
Zone.QuadTree.remove(socket.node);
socket.node = null;
socket.character.state.clearIntervals();
}
// socket.character.save(function(){
// console.log("Saved");
// });
});
socket.on('error', function(err) {
if(socket.node){
console.log("Node removed");
Zone.QuadTree.remove(socket.node);
socket.node = null;
socket.character.state.clearIntervals();
}
// socket.character.save(function(){
// console.log("Saved");
// });
});
socket.on('timeout', function() {});
var hash = socket.remoteAddress + ":" + socket.remotePort;
var characterData = this.socketTransferQueue[hash];
if (!characterData) {
console.log("could not retrive character data.");
socket.destroy();
return;
}
delete this.socketTransferQueue[hash];
var self = this;
db.Character.findOne({
_id: characterData.id,
AccountID: characterData.accountID
}, function(err, character) {
console.log("got character");
if (err) {
// console.log(err);
// TODO: Consider socket.disconnect
socket.destroy();
return;
}
if (!character) {
// console.log("Character not found");
socket.destroy();
return;
}
// Get account details
db.Account.findOne({_id: characterData.accountID},function(err, account) {
return self.onFindAccount(socket, err, account);
});
socket.character = character;
socket.character.infos = new CharacterInfos(socket);
socket.character.infos.updateAll(function() {
socket.character.state = new CharacterState();
socket.character.state.setAccountID(socket.character.AccountID);
socket.character.state.setCharacterID(socket.character._id);
socket.character.state.setFromCharacter(socket.character);
socket.character.DamageDealer = new DamageDealer(socket);
self.addSocket(socket);
if(socket.character.GuildName){
db.Guild.findByName(socket.character.GuildName, function(err, guild){
if(err){
return;
}
if(!guild){
return;
}
socket.character.state.setGuild(guild);
CachedBuffer.call(socket, self.packetCollection);
Zone.sendToAllArea(socket, true, socket.character.state.getPacket(), config.network.viewable_action_distance);
});
return;
}
CachedBuffer.call(socket, self.packetCollection);
Zone.sendToAllArea(socket, true, socket.character.state.getPacket(), config.network.viewable_action_distance);
// global.time = new Date().getTime();
//TODO: broadcastStates
//Set interval/timeout for 2 seconds, todo so
Zone.pullStates.bind(self, socket);
socket.character.state.Intervals.PullStates = setInterval(Zone.pullStates.bind(self, socket), 2000);
socket.character.state.Intervals.BroadcastState = setInterval(Zone.broadcastState.bind(self, socket), 5000);
socket.character.state.Intervals.Regeneration = setInterval(socket.character.state.regenerate, 5000);
});
});
break;
} else switch (type.type) {
case 'character':
this.socketTransferQueue[type.data.hash] = type.data;
break;
}
};
if (!global.Zone) {
global.Zone = new ZoneInstance();
Zone.init();
}
});