janusweb
Version:
Web client for JanusVR worlds
507 lines (460 loc) • 19.2 kB
JavaScript
elation.require(['janusweb.external.JanusClientConnection', 'janusweb.external.JanusFireboxParser'], function() {
/**
* multiplayermanager keeps track of all known rooms, and maintains a list of servers mapped to those rooms
* When new rooms are created, we register them with the multiplayermanager which adds some event listeners
* to the room. Each room fires events when the player joins or parts it, and the networkmanager uses these
* to manage subscriptions on each server. Networkmanager will forward all incoming packets to their
* respective rooms, and at the same time will regularly send updates about the player's state in the current
* active room to the appropriate server.
*
* Client should be good about unsubscribing from rooms as you traverse portals, the networkmanager will remain
* connected and subscribed to a room indefinitely unless told to leave
*/
elation.component.add('janusweb.multiplayermanager', function() {
this.init = function() {
this.janusweb = this.args.janusweb;
this.servers = {};
this.rooms = {};
this.roomservers = {};
this.remoteplayers = {};
this.enabled = false;
this.player = this.args.player;
this.parser = new JanusFireboxParser();
this.playerCount = 1;
this.remotePlayerCount = 0;
this.avatarNeedsUpdate = true;
this.avatarUpdateRate = 5000;
var hashargs = elation.url(),
server = elation.utils.any(this.args.server, hashargs['janus.server'], elation.config.get('janusweb.network.server')),
host = elation.utils.any(this.args.host, hashargs['janus.host'], elation.config.get('janusweb.network.host')),
//port = elation.utils.any(this.args.port, hashargs['janus.port'], elation.config.get('janusweb.network.port', 5567));
port = 5567;
if (server) {
this.defaultserver = server;
} else if (host.match(/^wss?:/)) {
// should be of the format ws://host:port or wss://host:port
this.defaultserver = host;
} else if (host && port) {
this.defaultserver = this.getServerURL(host, port);
}
this.defaultport = port;
elation.events.add(this.player, 'username_change', elation.bind(this, this.handleUsernameChange));
setInterval(elation.bind(this, this.updateAvatar), this.avatarUpdateRate);
}
this.enable = function(player) {
this.enabled = true;
this.player = player;
this.resetUpdateInterval();
elation.events.fire({type: 'enabled', element: this});
}
this.disable = function() {
this.enabled = false;
elation.events.fire({type: 'disabled', element: this});
}
this.getUpdateRate = function(room) {
var roomrate;
if (!room) room = this.activeroom;
if (room) {
roomrate = room.rate;
}
var rate = elation.utils.any(roomrate, this.args.rate, elation.config.get('janusweb.network.rate'), 20);
return rate;
}
this.resetUpdateInterval = function() {
if (this.updateinterval) {
this.stopUpdateInterval();
}
var rate = this.getUpdateRate();
this.updateinterval = setInterval(elation.bind(this, this.sendUpdate), rate);
}
this.stopUpdateInterval = function() {
if (this.updateinterval) {
clearTimeout(this.updateinterval);
}
}
this.getServerURL = function(host, port, secure) {
if (!port) port = this.defaultport;
port = 5567; // FIXME
if (typeof secure == 'undefined') secure = (port != 5566);
var protocol = (secure ? 'wss' : 'ws');
return protocol + '://' + host + ':' + port;
}
this.getServerURLForRoom = function(room, force) {
if (room.private) return false;
if (force || !this.roomservers[room.roomid]) {
var roomserver = this.defaultserver;
if (room.server) {
roomserver = this.getServerURL(room.server, room.port);
}
this.roomservers[room.roomid] = roomserver;
}
return this.roomservers[room.roomid];
}
this.getServerForRoom = function(room) {
var serverurl = this.getServerURLForRoom(room, true);
if (serverurl && !this.servers[serverurl] && !room.private) {
var server = new JanusClientConnection({
host: serverurl,
userId: this.janusweb.userId,
version: this.janusweb.version,
roomUrl: room.url
});
server.addEventListener('connect', elation.bind(this, this.handleServerConnect));
server.addEventListener('disconnect', elation.bind(this, this.handleServerDisconnect));
server.addEventListener('message', elation.bind(this, this.handleServerMessage));
server.addEventListener('login', elation.bind(this, this.updateAvatar));
//elation.events.add(this, 'room_change', elation.bind(this, function(ev) { console.log('DUR', ev); this.enter_room(ev.data); }));
//elation.events.add(this, 'room_disable', elation.bind(this, function(ev) { this.unsubscribe(ev.data.url); }));
this.servers[serverurl] = server;
}
// TODO - subscribe if not subscribed
return this.servers[serverurl];
}
this.registerRoom = function(room, subscribe) {
if (!this.enabled) return;
if (!this.rooms[room.roomid]) {
this.rooms[room.roomid] = room;
elation.events.add(room, 'join', elation.bind(this, this.handleRoomJoin));
elation.events.add(room, 'part', elation.bind(this, this.handleRoomPart));
}
if (!room.loaded) {
console.log('[MultiplayerManager] registered room, but not loaded yet:', room.url, room);
elation.events.add(room, 'room_load_processed', elation.bind(this, this.registerRoom, room, subscribe));
return;
}
console.log('[MultiplayerManager] registered room:', room.roomid, room.url, room);
if (subscribe) {
this.subscribe(room);
}
if (room == this.activeroom) {
this.resetUpdateInterval();
this.updateAvatar();
}
}
this.setActiveRoom = function(room) {
if (!this.enabled) return;
console.log('[MultiplayerManager] set active room:', room, this.activeroom);
// If we're already in a room, let's leave that one first
if (this.activeroom) {
var oldserver = this.getServerForRoom(this.activeroom);
//oldserver.leave_room(room.url);
oldserver.unsubscribe(room.url);
oldserver.active = false;
}
// Tell the server we're now in the new room
this.activeroom = room;
if (!room.private) {
var server = this.getServerForRoom(room);
var partymode = this.player.party_mode && room.party_mode;
server.subscribe(room.url);
server.enter_room(room.url, partymode);
server.active = true;
}
}
this.getJanusOrientation = (function() {
var tmpMat = new THREE.Matrix4(),
tmpVecX = new THREE.Vector3(),
tmpVecY = new THREE.Vector3(),
tmpVecZ = new THREE.Vector3();
return function(player, head) {
// takes a quaternion, returns an object OTF {dir: "0 0 0", up_dir: "0 0 0", view_dir: "0 0 0"}
tmpMat.makeRotationFromQuaternion(player.properties.orientation);
tmpMat.extractBasis(tmpVecX, tmpVecY, tmpVecZ);
var ret = {
dir: (tmpVecZ.x) + ' ' + (tmpVecZ.y) + ' ' + (tmpVecZ.z),
up_dir: '0 1 0',
//view_dir: this.tmpVecZ.toArray().join(' ')
};
if (head) {
//this.tmpMat.makeRotationFromQuaternion(head.properties.orientation);
tmpMat.copy(head.objects['3d'].matrixWorld);
tmpMat.extractBasis(tmpVecX, tmpVecY, tmpVecZ);
ret.view_dir = tmpVecZ.x + ' ' + tmpVecZ.y + ' ' + tmpVecZ.z,
ret.up_dir = tmpVecY.toArray().join(' ');
var headpos = head.properties.position;
//headpos.x *= -1;
//headpos.z *= -1;
//ret.head_pos = headpos.toArray().join(' '); //this.tmpMat.getPosition().toArray().join(' ');
ret.head_pos = [headpos.x, headpos.y, headpos.z];
} else {
ret.view_dir = '0 0 1';
ret.head_pos = '0 0 0';
}
return ret;
}
})();
this.sendUpdate = function() {
if (!this.enabled) return;
var player = this.player,
room = this.activeroom;
if (!room || !player) return;
var server = this.getServerForRoom(room);
if (!server || !server.loggedin) return;
var dirs = this.getJanusOrientation(player, player.head)
if (!this.movedata) {
this.movedata = {};
}
var moveData = this.movedata;
moveData.pos = player.properties.position.toArray().map(function(n) { return parseFloat(n.toFixed(4)); }).join(" ");
moveData.vel = player.properties.velocity.toArray().map(function(n) { return parseFloat(n.toFixed(4)); }).join(" ");
moveData.scale = player.properties.scale.toArray().map(function(n) { return parseFloat(n.toFixed(4)); }).join(" ");
moveData.rotvel = player.properties.angular.toArray().map(function(n) { return parseFloat(n.toFixed(4)); }).join(" ");
moveData.dir = dirs.dir;
moveData.up_dir = dirs.up_dir;
moveData.view_dir = dirs.view_dir;
moveData.head_pos = dirs.head_pos;
moveData.anim_id = player.getAnimationID();
//console.log('[MultiplayerManager] player update', moveData);
if (this.avatarNeedsUpdate || player.avatarNeedsUpdate) {
moveData.avatar = player.getCurrentAvatarData().replace(/"/g, "^");
this.avatarNeedsUpdate = false;
player.avatarNeedsUpdate = false;
} else if (moveData.avatar) {
delete moveData.avatar;
}
if (player.hasVoipData()) {
var voipdata = player.getVoipData();
moveData.speaking = true;
moveData.audio = window.btoa(voipdata);
} else {
moveData.speaking = false;
if (moveData.audio) {
delete moveData.audio;
}
}
if (room.hasChanges()) {
moveData.room_edit = room.getChanges().replace(/"/g, "^");
} else if (moveData.room_edit) {
delete moveData.room_edit;
}
if (room.hasDeletions()) {
moveData.room_delete = room.getDeletions().replace(/"/g, "^");
} else if (moveData.room_delete) {
delete moveData.room_delete;
}
if (player.hasHands()) {
var hands = player.getHandData();
if (hands.left && hands.left.active) {
moveData.hand0 = {
state: hands.left.state
};
}
if (hands.right && hands.right.active) {
moveData.hand1 = {
state: hands.right.state
};
}
}
let morphtargetdata = player.getMorphTargetData();
if (morphtargetdata) {
moveData.morphtargets = morphtargetdata
}
//console.log(moveData, server, room);
server.send({'method': 'move', 'data': moveData});
}
this.subscribe = function(room) {
if (!this.enabled) return;
var server = this.getServerForRoom(room);
console.log('[MultiplayerManager] subscribe', room.url);
if (server) {
server.subscribe(room.url);
}
}
this.unsubscribe = function(room) {
if (!this.enabled) return;
console.log('[MultiplayerManager] unsubscribe', room.url);
var server = this.getServerForRoom(room);
if (server) {
server.unsubscribe(room.url);
}
}
this.join = function(room) {
if (!this.enabled) return;
console.log('[MultiplayerManager] join', room.url);
var server = this.getServerForRoom(room);
var partymode = this.player.party_mode && room.party_mode;
if (server) {
server.subscribe(room.url);
server.enter_room(room.url, partymode);
}
}
this.part = function(room) {
if (!this.enabled) return;
console.log('[MultiplayerManager] part', room.url);
var server = this.getServerForRoom(room);
if (server) {
server.unsubscribe(room.url);
}
}
this.spawnRemotePlayer = function(data) {
var userId = data.userId;
var roomId = data.roomId;
var room = this.rooms[roomId] || this.activeroom;
var spawnpos = (data.position && data.position.pos ? data.position.pos.split(" ").map(parseFloat) : [0,0,0]);
//console.log('[MultiplayerManager] spawn remote guy', userId, roomId, room, spawnpos);
this.remoteplayers[userId] = room.spawn('remoteplayer', userId, { position: spawnpos, player_id: userId, player_name: userId, pickable: false, collidable: false, janus: this.janusweb, room: room});
var remote = this.remoteplayers[userId];
this.remotePlayerCount = Object.keys(this.remoteplayers).length;
this.playerCount = this.remotePlayerCount + 1;
// If a new player spawned, let's send an avatar update ASAP
this.updateAvatar();
remote.start();
return remote;
}
this.updateAvatar = function() {
this.avatarNeedsUpdate = true;
}
this.send = function(msg, room) {
//console.log('[MultiplayerManager] send', msg, room);
if (!room) room = this.activeroom;
var server = this.getServerForRoom(room);
server.send(msg);
}
this.handleServerConnect = function(msg) {
console.log('[MultiplayerManager] connected', msg);
//this.sendPlayerUpdate({first: true});
var server = msg.target;
elation.events.fire({element: this, type: 'janusweb_client_connected', data: this.janusweb.userId});
if (this.janusweb.chat) {
this.janusweb.chat.addmessage({userId: ' ! ', message: 'Connected to ' + server._host + ' as ' + this.janusweb.userId });
}
}
this.handleServerDisconnect = function(ev) {
console.log('[MultiplayerManager] disconnected', ev);
elation.events.fire({element: this, type: 'janusweb_client_disconnected', data: this.janusweb.userId});
if (this.janusweb.chat) {
this.janusweb.chat.addmessage({userId: ' ! ', message: 'Disconnected'});
}
}
this.handleServerMessage = function(msg) {
//console.log('[MultiplayerManager] server msg', msg);
var method = msg.data.method
if (method == 'user_moved') {
this.handleUserMoved(msg);
} else if (method == 'user_disconnected') {
this.handleUserDisconnect(msg);
} else if (method == 'user_enter') {
this.handleUserEnter(msg);
} else if (method == 'user_leave') {
this.handleUserLeave(msg);
} else if (method == 'user_portal') {
this.handlePortal(msg);
} else if (method == 'user_chat') {
this.handleUserChat(msg);
}
}
this.handleUserMoved = function(msg) {
var userId = msg.data.data.position._userId;
var room = this.rooms[msg.data.data.roomId] || this.activeroom;
if (!this.remoteplayers[userId]) {
var remoteplayer = this.spawnRemotePlayer(msg.data.data);
remoteplayer.setRoom(room);
elation.events.fire({element: this, type: 'janusweb_user_joined', data: remoteplayer});
} else {
var remote = this.remoteplayers[userId];
var movedata = msg.data.data.position;
if (remote.room !== room) {
remote.setRoom(room);
remote.start();
}
remote.updateData(movedata);
}
}
this.handleUserDisconnect = function(msg) {
var remoteplayer = this.remoteplayers[msg.data.data.userId];
console.log('[MultiplayerManager] player disconnected', msg, remoteplayer);
if (remoteplayer) {
if (this.janusweb.chat) {
this.janusweb.chat.addmessage({userId: ' ! ', message: msg.data.data.userId + ' disconnected' });
}
if (remoteplayer && remoteplayer.parent) {
remoteplayer.die();
}
delete this.remoteplayers[msg.data.data.userId];
this.remotePlayerCount = Object.keys(this.remoteplayers).length;
this.playerCount = this.remotePlayerCount + 1;
elation.events.fire({element: this, type: 'janusweb_user_disconnected', data: remoteplayer});
}
}
this.handleUserEnter = function(msg) {
var remoteplayer = this.remoteplayers[msg.data.data.userId];
console.log('[MultiplayerManager] player entered', msg, remoteplayer);
var room = this.rooms[msg.data.data.roomId];
if (!remoteplayer) {
remoteplayer = this.spawnRemotePlayer(msg.data.data);
}
if (remoteplayer.room !== room) {
remoteplayer.setRoom(room);
}
this.remotePlayerCount = Object.keys(this.remoteplayers).length;
this.playerCount = this.remotePlayerCount + 1;
elation.events.fire({element: this, type: 'janusweb_user_joined', data: remoteplayer});
if (this.janusweb.chat) {
this.janusweb.chat.addmessage({userId: ' ! ', message: msg.data.data.userId + ' joined room' });
}
}
this.handleUserLeave = function(msg) {
var remoteplayer = this.remoteplayers[msg.data.data.userId];
console.log('[MultiplayerManager] player left', msg, remoteplayer);
if (remoteplayer) {
if (remoteplayer) {
remoteplayer.setRoom(null);
}
this.remotePlayerCount = Object.keys(this.remoteplayers).length;
this.playerCount = this.remotePlayerCount + 1;
elation.events.fire({element: this, type: 'janusweb_user_left', data: remoteplayer});
if (this.janusweb.chat) {
this.janusweb.chat.addmessage({userId: ' ! ', message: msg.data.data.userId + ' left room' });
}
}
}
this.handlePortal = function(msg) {
var portaldata = msg.data.data;
var portalname = 'portal_' + portaldata.userId + '_' + md5(portaldata.url);
var node = this.parser.parseNode(portaldata);
var room = this.rooms[portaldata.roomId];
if (room) {
room.spawn('janusportal', portalname, {
url: portaldata.url,
janus: this.janusweb,
room: room,
title: portaldata.url,
position: node.pos,
orientation: node.orientation,
});
}
}
this.handleUserChat = function(msg) {
elation.events.fire({element: this, type: 'janusweb_user_chat', data: msg.data.data});
if (this.janusweb.chat) {
this.janusweb.chat.addmessage(msg.data.data);
}
}
this.handleRoomJoin = function(ev) {
if (this.activeroom != ev.target) {
if (this.activeroom) {
this.part(this.activeroom);
}
var room = this.activeroom = ev.target;
if (room.loaded) {
this.join(room);
} else {
elation.events.add(room, 'room_load_processed', elation.bind(this, this.join, room));
}
}
}
this.handleRoomPart = function(ev) {
if (this.activeroom == ev.target) {
this.part(this.activeroom);
this.activeroom = false;
}
}
this.handleUsernameChange = function(ev) {
var name = ev.data;
console.log('[MultiplayerManager] player changed username', name);
for (var k in this.servers) {
this.servers[k].setUserId(name);
}
}
});
});