UNPKG

janusweb

Version:
1,202 lines (1,140 loc) 120 kB
elation.require([ 'ui.textarea', 'ui.window', 'engine.things.generic', 'engine.things.label', 'engine.things.skybox', 'janusweb.object', 'janusweb.portal', 'janusweb.image', 'janusweb.video', 'janusweb.text', 'janusweb.janusparagraph', 'janusweb.sound', 'janusweb.januslight', 'janusweb.janusparticle', 'janusweb.janusghost', 'janusweb.translators.bookmarks', 'janusweb.translators.reddit', 'janusweb.translators.error', 'janusweb.translators.blank', 'janusweb.translators.default', 'janusweb.translators.dat', 'janusweb.translators.janusvfs' ], function() { let roomTranslators = false; function initRoomTranslators() { roomTranslators = { '^janus-vfs:': elation.janusweb.translators.janusvfs({janus: janus}), '^about:blank$': elation.janusweb.translators.blank({janus: janus}), '^bookmarks$': elation.janusweb.translators.bookmarks({janus: janus}), '^dat:': elation.janusweb.translators.dat({janus: janus}), '^https?:\/\/(www\.)?reddit.com': elation.janusweb.translators.reddit({janus: janus}), '^error$': elation.janusweb.translators.error({janus: janus}), '^default$': elation.janusweb.translators.default({janus: janus}) }; } elation.component.add('engine.things.janusroom', function() { this.postinit = function() { elation.engine.things.janusroom.extendclass.postinit.call(this); this.toneMappingTypes = { 'none': THREE.NoToneMapping, 'linear': THREE.LinearToneMapping, 'reinhard': THREE.ReinhardToneMapping, 'uncharted2': THREE.Uncharted2ToneMapping, 'cineon': THREE.CineonToneMapping, 'acesfilmic': THREE.ACESFilmicToneMapping, }; this.defineProperties({ 'janus': { type: 'object' }, 'url': { type: 'string', default: false }, 'urlfragment': { type: 'string', default: false }, 'referrer': { type: 'string' }, 'deferload': { type: 'boolean', default: false }, 'roomid': { type: 'string' }, 'corsproxy': { type: 'string', default: false }, 'baseurl': { type: 'string', default: false }, 'source': { type: 'string' }, 'skybox': { type: 'boolean', default: true, set: this.toggleSkybox }, 'skybox_intensity': { type: 'float', set: this.setSkybox, default: 1.0 }, 'skybox_equi': { type: 'string', set: this.setSkybox }, 'skybox_left_id': { type: 'string', set: this.setSkybox }, 'skybox_right_id': { type: 'string', set: this.setSkybox }, 'skybox_up_id': { type: 'string', set: this.setSkybox }, 'skybox_down_id': { type: 'string', set: this.setSkybox }, 'skybox_front_id': { type: 'string', set: this.setSkybox }, 'skybox_back_id': { type: 'string', set: this.setSkybox }, 'cubemap_irradiance_id': { type: 'string' }, 'cubemap_radiance_id': { type: 'string' }, 'fog': { type: 'boolean', default: false, set: this.setFog }, 'fog_mode': { type: 'string', default: 'exp', set: this.setFog }, 'fog_density': { type: 'float', default: 1.0, set: this.setFog }, 'fog_start': { type: 'float', default: 0.0, set: this.setFog }, 'fog_end': { type: 'float', default: 100.0, set: this.setFog }, 'fog_col': { type: 'color', default: 0x000000, set: this.setFog }, 'ambient': { type: 'color', default: 0x666666, set: this.updateLights }, 'near_dist': { type: 'float', default: 0.01, set: this.setNearFar }, 'far_dist': { type: 'float', default: 1000.0, set: this.setNearFar }, 'pbr': { type: 'boolean', default: true }, 'toon': { type: 'boolean', default: false }, 'bloom': { type: 'float', default: 0.05, set: this.updateBloom }, 'tonemapping_type': { type: 'string', default: 'linear', set: this.updateToneMapping }, 'tonemapping_exposure': { type: 'float', default: 0.8, set: this.updateToneMapping }, 'defaultlights': { type: 'bool', default: true, set: this.updateLights }, 'shadows': { type: 'bool', default: false, set: this.updateShadows }, 'party_mode': { type: 'bool', default: true }, 'walk_speed': { type: 'float', default: 1.8, set: this.updatePlayerSpeed }, 'run_speed': { type: 'float', default: 5.4, set: this.updatePlayerSpeed }, 'jump_velocity': { type: 'float', default: 5.0 }, 'flying': { type: 'boolean', default: true, set: this.updateFlying }, 'gravity': { type: 'float', default: 0, set: this.updateGravity }, 'teleport': { type: 'bool', default: true, set: this.updateTeleport }, 'locked': { type: 'bool', default: false }, 'cursor_visible': { type: 'bool', default: true }, 'cursor_opacity': { type: 'float', default: .8 }, 'use_local_asset': { type: 'string', set: this.updateLocalAsset }, 'col': { type: 'color', set: this.updateLocalAsset }, 'private': { type: 'bool', default: false }, 'server': { type: 'string' }, 'port': { type: 'int' }, 'rate': { type: 'int', default: 200 }, 'voip': { type: 'string', default: 'janus' }, 'voipid': { type: 'string' }, 'voiprange': { type: 'float', default: 1 }, 'voipserver': { type: 'string', default: 'voip.janusxr.org' }, 'classList': { type: 'object', default: [] }, 'className': { type: 'string', default: '', set: this.setClassName }, 'gazetime': { type: 'float', default: 1000 }, 'selfavatar': { type: 'boolean', default: false }, 'requires': { type: 'string' }, 'onload': { type: 'string' }, 'sync': { type: 'boolean', default: false }, 'pointerlock': { type: 'boolean', default: true, set: this.updatePointerLock }, 'players': { type: 'object', get: this.getRoomPlayers }, 'defaultview': { type: 'string' }, 'showavatar': { type: 'bool', default: true, set: this.updateAvatarVisibility }, 'spawnradius': { type: 'float', default: 0 }, }); if (!roomTranslators) { initRoomTranslators(); } this.spawnpoint = new THREE.Object3D(); this.roomsrc = ''; this.changes = {}; this.deletions = []; this.appliedchanges = {}; this.openportals = []; this.roomassets = {}; this.pendingassets = []; this.jsobjects = {}; this.cookies = {}; this.websurfaces = {}; this.ghosts = {}; this.images = {}; this.sounds = {}; this.videos = {}; this.loaded = false; this.completed = false; this.parseerror = false; this.roomscripts = []; this.customElements = {}; this.unknownElements = {}; this.eventlistenerproxies = {}; this.pendingScriptMap = {}; // FIXME - binding functions to this instance so we can unbind events later. Should be done at a lower level this.onRoomEdit = elation.bind(this, this.onRoomEdit); this.onClick = elation.bind(this, this.onClick); this.onKeyDown = elation.bind(this, this.onKeyDown); this.onKeyUp = elation.bind(this, this.onKeyUp); this.onObjectClick = elation.bind(this, this.onObjectClick); this.onMouseDown = elation.bind(this, this.onMouseDown); this.onMouseUp = elation.bind(this, this.onMouseUp); this.onMouseMove = elation.bind(this, this.onMouseMove); this.onScriptTick = elation.bind(this, this.onScriptTick); this.roomedit = { snap: .01, modes: ['pos', 'rotation', 'scale', 'col'], movespeed: new THREE.Vector3(), modeid: 0, object: false, distancescale: 1 }; if (this.url) { let hashidx = this.url.indexOf('#'); if (hashidx != -1) { this.urlhash = this.url.substr(hashidx+1); this.setHash(this.urlhash) this.url = this.url.substr(0, hashidx); } this.roomid = md5(this.url); if (!this.deferload) { this.load(this.url, this.baseurl); } } else if (this.source) { this.loadFromSource(this.source); } elation.events.add(this, 'thing_remove', elation.bind(this, this.onThingRemove)); document.addEventListener('visibilitychange', ev => this.handleVisibilityChange()); } this.createChildren = function() { this.collidable = false; this.setCollider('sphere', {radius: 1e4}); /* this.skyboxcollider = this.createObject('object', { js_id: 'room_skybox', collision_id: 'sphere', collision_scale: V(10000), collision_trigger: true, pickable: true, }); this.skyboxcollider.collidable = false; */ this.objects['3d'].add(this.spawnpoint); this.lastthink = 0; this.thinktime = 0; } this.createLights = function() { this.roomlights = { ambient: this.spawn('light_ambient', this.id + '_ambient', { color: this.properties.ambient }), directional: this.spawn('light_directional', this.id + '_sun', { position: [-20,50,25], intensity: 0.1 }), point: this.spawn('light_point', this.id + '_point', { position: [22,19,-15], intensity: 0.1 }) }; } this.updateLights = function() { if (!this.roomlights) { this.createLights(); } if (!this.objects['3d']) return; this.roomlights.ambient.lightobj.color = this.ambient._target; if (this.defaultlights) { if (this.roomlights.directional.parent != this) { this.add(this.roomlights.directional); } if (this.roomlights.point.parent != this) { this.add(this.roomlights.point); } } else { if (this.roomlights.directional.parent) { this.roomlights.directional.parent.remove(this.roomlights.directional); } if (this.roomlights.point.parent) { this.roomlights.point.parent.remove(this.roomlights.point); } } } this.setActive = function() { this.active = true; this.setSkybox(); this.setFog(); this.updateBloom(); this.updateToneMapping(); this.setNearFar(); this.setPlayerPosition(); if (typeof player != 'undefined' && this.defaultview && this.defaultview != player.cameraview) { player.setCameraView(this.defaultview); } this.updateAvatarVisibility(); elation.events.fire({element: this, type: 'room_active', data: this}); } this.setPlayerPosition = function(pos, orientation) { if (!pos) { let spawnpoint = this.getSpawnpoint(this.referrer); pos = spawnpoint.position; orientation = spawnpoint.orientation; } var player = this.engine.client.player; if (player.parent !== this) { // Reparent player to the room if necessary this.appendChild(player.getProxyObject()); } player.reset_position(); player.position.copy(pos); player.orientation.copy(orientation); player.properties.movestrength = 80 * this.properties.walk_speed; player.properties.runstrength = 80 * this.properties.run_speed; player.cursor_visible = (!janus.hmd ? elation.utils.any(this.cursor_visible, true) : false); player.cursor_opacity = elation.utils.any(this.cursor_opacity, .8); // FIXME - for some reason the above call sometimes orients the player backwards. Doing it on a delay fixes it... //setTimeout(elation.bind(player, player.reset_position), 0); } this.setHash = function(hash) { this.urlhash = hash; if (document.location.origin + document.location.pathname == this.url) { document.location.hash = hash; } let spawnpoint = this.getSpawnpoint(); if (typeof player != 'undefined') { player.startposition.copy(spawnpoint.position); player.startorientation.copy(spawnpoint.orientation); } } this.getSpawnpoint = function(referrer) { let spawnpoint = { position: this.spawnpoint.position, orientation: this.spawnpoint.quaternion, }; if (this.spawnradius) { let dist = Math.random() * this.spawnradius, angle = Math.random() * 2 * Math.PI; spawnpoint.position.x += Math.sin(angle) * dist; spawnpoint.position.z += Math.cos(angle) * dist; } if (referrer) { let links = this.getObjectsByTagName('link'); for (let i = 0; i < links.length; i++) { if (links[i].url == referrer) { //spawnpoint.position = links[i].position; spawnpoint.position = links[i].localToWorld(V(0,0,player.fatness*2)); spawnpoint.orientation = links[i].orientation.clone(); let node = links[i]; let roomproxy = this.getProxyObject(); while (node.parent && node.parent != roomproxy) { node = node.parent; node.handleFrameUpdates({}); spawnpoint.orientation.multiply(node.orientation); } spawnpoint.orientation.multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0))); // Flip 180 degrees from portal orientation break; } } } else if (this.urlhash) { let obj = this.getObjectById(this.urlhash); if (obj) { obj.localToWorld(spawnpoint.position.set(0,0,0)); spawnpoint.orientation.setFromRotationMatrix(obj.objects['3d'].matrixWorld.lookAt(spawnpoint.position, obj.localToWorld(V(0,0,1)), obj.localToWorld(V(0,1,0).sub(spawnpoint.position)))); } } return spawnpoint; } this.setSkybox = function() { if (!this.loaded) return; if (!this.skybox) { this.engine.systems.render.renderer.setClearAlpha(0); return; } else { this.engine.systems.render.renderer.setClearAlpha(1); } if (!this.skyboxobj) { this.skyboxobj = this.spawn('skybox', this.id + '_sky', { position: [0,0,0], collidable: false, }); } if (this.skyboxtexture) { this.skyboxobj.setTexture(this.skyboxtexture); //return; } var hasSkybox = (this.skybox_left_id || this.skybox_right_id || this.skybox_top_id || this.skybox_bottom_id || this.skybox_left_id || this.skybox_right_id) != undefined; var assets = []; if (this.skybox_equi) { let equi = this.getAsset('image', this.skybox_equi); if (equi.loaded) { this.skyboxtexture = equi.getInstance(); this.skyboxtexture.mapping = THREE.EquirectangularReflectionMapping; this.skyboxtexture.encoding = THREE.sRGBEncoding; if (this.janus.currentroom === this) { this.skyboxobj.setTexture(this.skyboxtexture); } elation.events.fire({element: this, type: 'skybox_update'}); } else { elation.events.add(equi, 'asset_load', ev => { this.skyboxtexture = ev.target._texture; this.skyboxtexture.mapping = THREE.EquirectangularReflectionMapping; this.skyboxtexture.encoding = THREE.sRGBEncoding; if (this.janus.currentroom === this) { this.skyboxobj.setTexture(this.skyboxtexture); } elation.events.fire({element: this, type: 'skybox_update'}); }); equi.getInstance(); } } else if (hasSkybox) { assets = [ this.getAsset('image', this.skybox_right_id || 'black'), this.getAsset('image', this.skybox_left_id || 'black'), this.getAsset('image', this.skybox_up_id || 'black'), this.getAsset('image', this.skybox_down_id || 'black'), this.getAsset('image', this.skybox_front_id || 'black'), this.getAsset('image', this.skybox_back_id || 'black') ]; } else { var skyboxname = 'dayskybox'; assets = [ this.getAsset('image', skyboxname + '_right'), this.getAsset('image', skyboxname + '_left'), this.getAsset('image', skyboxname + '_up'), this.getAsset('image', skyboxname + '_down'), this.getAsset('image', skyboxname + '_front'), this.getAsset('image', skyboxname + '_back') ]; } // TODO - THREE.DDSLoader only supports compressed DDS textures, but JanusVR sends uncompressed RGBA888 (I think) /* if (this.cubemap_irradiance_id) { var asset = this.getAsset('image', this.cubemap_irradiance_id); console.log('irradiance', this.cubemap_irradiance_id, asset); } if (this.cubemap_radiance_id) { var asset = this.getAsset('image', this.cubemap_radiance_id); console.log('radiance', this.cubemap_radiance_id, asset); } */ var loaded = 0, errored = 0; var texures = []; assets.forEach(elation.bind(this, function(asset) { if (asset) { var n = asset.getInstance(); if (asset.loaded) { loaded++; if (loaded + errored == 6) { this.processSkybox(assets); } } else { elation.events.add(n, 'asset_load,asset_error', elation.bind(this, function(ev) { if (ev.type == 'asset_load') loaded++; else errored++; if (loaded + errored == 6) { this.processSkybox(assets); } })); } } })); return false; } this.processSkybox = function(assets) { if (assets[0] && assets[1] && assets[2] && assets[3] && assets[4] && assets[5]) { var images = []; assets.forEach(function(asset) { var t = asset.getInstance(); images.push(t.image); }); // Handle skyboxes with missing textures. We need to figure out // the skybox texture size, then create a blank canvas of that size // Images of size 16x16 are (probably!) placeholders var width = undefined, height = undefined; images.forEach(function(img) { if (img.width != 16 && img.height != 16) { width = img.width; height = img.height; } }); if (width && height) { for (var i = 0; i < images.length; i++) { if (images[i] instanceof HTMLCanvasElement && images[i].width != width && images[i].height != height) { images[i].width = width; images[i].height = height; } // flip skybox 180 degrees if (images[i] instanceof HTMLCanvasElement) { var ctx = images[i].getContext('2d'); if (i == 2 || i == 3) { // Y+ or Y- // Y+ ctx.translate(images[i].width/2, images[i].height/2); ctx.rotate(Math.PI); ctx.translate(-images[i].width/2, -images[i].height/2); } ctx.drawImage(images[i],0,0,images[i].width,images[i].height); } } } if (images[0] && images[1] && images[2] && images[3] && images[4] && images[5]) { // flip skybox 180 degrees images = [images[1],images[0],images[2],images[3],images[5],images[4]]; var texture = new THREE.CubeTexture( images ); texture.encoding = THREE.sRGBEncoding; texture.needsUpdate = true; this.skyboxtexture = texture; if (this.janus.currentroom === this) { this.skyboxobj.setTexture(this.skyboxtexture); } elation.events.fire({element: this, type: 'skybox_update'}); return true; } } } this.toggleSkybox = function() { if (this.skybox) { this.engine.systems.render.renderer.setClearAlpha(1); if (this.skyboxtexture) { this.skyboxobj.setTexture(this.skyboxtexture); } else { this.setSkybox(); } } else { this.engine.systems.render.renderer.setClearAlpha(0); if (this.skyboxobj) { this.skyboxobj.setTexture(null); } } } this.setFog = function() { if (this.fog) { var fogcol = this.properties.fog_col || 0; var fogcolor = new THREE.Color(); if (fogcol[0] == '#') { fogcolor.setHex(parseInt(fogcol.substr(1), 16)); } else if (elation.utils.isString(fogcol) && fogcol.indexOf(' ') != -1) { var rgb = fogcol.split(' '); fogcolor.setRGB(rgb[0], rgb[1], rgb[2]); } else if (fogcol instanceof THREE.Color) { fogcolor.copy(fogcol); } else { fogcolor.setHex(fogcol); } if (this.properties.fog_mode == 'exp' || this.properties.fog_mode == 'exp2') { this.engine.systems.world.setFogExp(parseFloat(this.properties.fog_density), fogcolor); } else { this.engine.systems.world.setFog(this.properties.fog_start, this.properties.fog_end, fogcolor); } } else { this.engine.systems.world.disableFog(); } } this.updateBloom = function() { // adjust bloom setting var bloomfilter = this.engine.client.view.effects['bloom']; if (bloomfilter) { bloomfilter.copyUniforms.opacity.value = this.bloom; } var bloomfilter2 = this.engine.client.view.effects['unrealbloom']; if (bloomfilter2) { bloomfilter2.strength = this.bloom / 2; // unrealbloom seems to be a lot stronger than the old bloom method, so we scale it to keep things somewhat in line } } this.updateToneMapping = function() { if (this.active) { this.engine.systems.render.renderer.toneMapping = this.toneMappingTypes[this.tonemapping_type] || 0; this.engine.systems.render.renderer.toneMappingExposure = this.tonemapping_exposure; this.refresh(); } } this.setNearFar = function() { if (this.active) { this.engine.client.player.camera.camera.near = this.properties.near_dist; this.engine.client.player.camera.camera.far = this.properties.far_dist; } } this.showDebug = function() { this.engine.client.player.disable(); if (!this.debugwindow) { var content = elation.ui.panel_vertical({classname: 'janusweb_room_debug'}); this.debugwindow = elation.ui.window({title: 'Janus Room', content: content, append: document.body, center: true}); this.debugeditor = elation.ui.textarea({append: content, value: this.getRoomSource(), classname: 'janusweb_room_source', wrap: 'off'}); this.debugwindow.settitle('Room Source: ' + this.properties.url); var applybutton = elation.ui.button({label: 'Apply Changes'}); applybutton.disabled = true; var refreshbutton = elation.ui.button({label: 'Refresh'}); var buttons = elation.ui.buttonbar({ classname: 'janusweb_room_debug_buttons', append: content, buttons: { apply: applybutton, refresh: refreshbutton, } }); this.debugwindow.setcontent(content); this.debugbuttons = buttons.buttons; elation.events.add(this.debugeditor, 'input', elation.bind(this, this.handleEditorInput)); elation.events.add(this.debugeditor, 'change', elation.bind(this, this.handleEditorChange)); elation.events.add(applybutton, 'click', elation.bind(this, this.handleEditorApply)); elation.events.add(refreshbutton, 'click', elation.bind(this, this.handleEditorRefresh)); elation.events.add(this, 'room_edit', (ev) => { this.debugeditor.value = this.getRoomSource(); console.log('updated source'); }); } //elation.ui.content({append: content, content: this.properties.url, classname: 'janusweb_room_url'}); this.debugwindow.show(); this.debugwindow.center(); } this.load = function(url, baseurloverride) { if (!url) { url = this.properties.url; } else { this.properties.url = url; } var baseurl = baseurloverride; if (!baseurl) { if (url && !this.baseurl) { baseurl = url.split('/'); if (baseurl.length > 3) baseurl.pop(); baseurl = baseurl.join('/') + '/'; this.baseurl = baseurl; } } else { this.baseurl = baseurl; } this.jsobjects = {}; this.cookies = {}; this.websurfaces = {}; this.images = {}; this.videos = {}; this.setTitle('loading...'); var proxyurl = this.corsproxy || ''; var fullurl = url; if (fullurl[0] == '/' && fullurl[1] != '/') fullurl = this.baseurl + fullurl; if (!fullurl.match(/^https?:/) && !fullurl.match(/^\/\//)) { fullurl = self.location.origin + fullurl; } else if (!fullurl.match(/^https?:\/\/(localhost|127\.0\.0\.1)/) && fullurl.indexOf(document.location.origin) != 0) { fullurl = proxyurl + fullurl; } elation.events.fire({element: this, type: 'room_load_queued'}); var translator = this.getTranslator(url); /* // Disabled - using outerHTML to get the source is a nice way to avoid an extra network trip, // but due to how browser parsers work it means we can't get back unmodified XML, so it's best // to just make an XHR call and let the browser cache handle it if (url == document.location.href) { setTimeout(elation.bind(this, function() { this.roomsrc = this.parseSource(document.documentElement.outerHTML); var roomdata = this.parseFireBox(this.roomsrc.source); this.createRoomObjects(roomdata); this.setActive(); this.loaded = false; elation.events.fire({type: 'janus_room_load', element: this}); }), 0); } else */ if (translator) { setTimeout(elation.bind(this, function() { // TODO - use the new official translators here! translator.exec({url: url, janus: this.properties.janus, room: this}) .then(elation.bind(this, function(objs) { if (objs.room.title) { this.setTitle(objs.room.title); } this.roomsrc = objs.source; this.loadRoomAssets(objs); this.createRoomObjects(objs); this.loaded = true; this.setActive(); elation.events.fire({element: this, type: 'room_load_processed'}); elation.events.fire({type: 'janus_room_load', element: this}); })); }), 0); } else if (url.indexOf('data:') == 0) { var prefix = 'data:text/html,'; var data = decodeURIComponent(url.substr(prefix.length)); setTimeout(elation.bind(this, function() { elation.events.fire({element: this, type: 'room_load_processing'}); this.loadFromSource(data); }), 0); } else { var started = false; elation.net.get(fullurl, null, { headers: { //'User-Agent':'FireBox 1.0' //'X-Requested-With':'JanusWeb Client' }, callback: elation.bind(this, function(data, xhr) { var responseURL = xhr.getResponseHeader('X-Final-URL'); if (!responseURL) { responseURL = xhr.responseURL.replace(proxyurl, ''); } if (responseURL != this.properties.url) { var url = responseURL; if (!baseurloverride) { baseurl = url.split('/'); baseurl.pop(); baseurl = baseurl.join('/') + '/'; this.baseurl = baseurl; } this.properties.url = url; } elation.events.fire({element: this, type: 'room_load_processing'}); this.loadFromSource(data); }), failurecallback: elation.bind(this, function(xhr) { var translator = roomTranslators['^error$']; translator.exec({janus: this.properties.janus, room: this, error: xhr.status || 404}) .then(elation.bind(this, function(objs) { var datapath = elation.config.get('janusweb.datapath', '/media/janusweb'); var assetpath = datapath + 'assets/translator/errors/'; this.baseurl = assetpath; this.loadRoomAssets(objs); this.createRoomObjects(objs); this.enable(); this.setActive(); })); }), onprogress: elation.bind(this, function(ev) { if (!started) { started = true; elation.events.fire({element: this, type: 'room_load_start'}); } var progress = { src: ev.target.responseURL, total: ev.total, loaded: ev.loaded }; elation.events.fire({element: this, type: 'room_load_progress', data: progress}); }) }); } } this.validateSource = function(sourcecode) { return new Promise(elation.bind(this, function(resolve, reject) { var source = this.parseSource(sourcecode); var datapath = elation.config.get('janusweb.datapath', '/media/janusweb'); try { var roomdata = this.janus.parser.parse(source.source, this.baseurl, datapath); resolve(true); } catch (e) { reject(); } })); } this.loadFromSource = function(sourcecode, baseurl) { if (baseurl) { this.baseurl = baseurl; } var source = this.parseSource(sourcecode); if (source && source.source) { this.roomsrc = source.source; var datapath = elation.config.get('janusweb.datapath', '/media/janusweb'); try { var roomdata = this.janus.parser.parse(source.source, this.baseurl, datapath); this.loadRoomAssets(roomdata); this.createRoomObjects(roomdata); this.loaded = true; if (this.active) { this.setActive(); } //this.parseerror = false; elation.events.fire({element: this, type: 'room_load_processed'}); elation.events.fire({type: 'janus_room_load', element: this}); if (this.pendingassets.length == 0 && !this.completed) { setTimeout(() => elation.events.fire({element: this, type: 'room_load_complete'}), 0); } } catch (e) { console.error('Janus room parse error:', e.message); this.parseerror = e.message; elation.events.fire({type: 'room_load_error', element: this, data: e.message}); } } else { var translator = this.getTranslator('default'); setTimeout(elation.bind(this, function() { // TODO - use the new official translators here! translator.exec({url: this.url, janus: this.properties.janus, room: this}) .then(elation.bind(this, function(objs) { this.roomsrc = objs.source; this.loadRoomAssets(objs); this.createRoomObjects(objs); this.loaded = true; this.setActive(); elation.events.fire({element: this, type: 'room_load_processed'}); elation.events.fire({type: 'janus_room_load', element: this}); })); }), 0); } } this.parseSource = function(data) { this.fullsource = data; var titlere = /<title>([\s\S]*?)<\/title>/si; var re = /<fireboxroom>[\s\S]*?<\/fireboxroom>/si; var re2 = /<janus-viewer[^>]*>[\s\S]*?<\/janus-viewer>/si; var mtitle = data.match(titlere); var parsed = { title: 'Untitled Room', source: false }; if (mtitle) { parsed.title = mtitle[1]; this.setTitle(mtitle[1]); } else { this.setTitle(null); } var m = data.match(re); if (m) { parsed.source = m[0]; } else { let m2 = data.match(re2); if (m2) { parsed.source = m2[0].replaceAll('janus-viewer', 'fireboxroom'); } } return parsed; } this.updateSource = function(source) { var datapath = elation.config.get('janusweb.datapath', '/media/janusweb'); try { var roomdata = this.janus.parser.parse(source, this.baseurl, datapath); } catch (e) { } console.log('updated roomdata!', roomdata); if (roomdata && roomdata.room) { for (let k in roomdata.room) { let val = roomdata.room[k]; if (val !== null && room[k] != val) { console.log('update room value', k, room[k], val); room[k] = val; } } if (roomdata.object) { for (let i = 0; i < roomdata.object.length; i++) { let objdata = roomdata.object[i]; let roomobj = room.objects[objdata.js_id]; if (roomobj) { for (let k in objdata) { let val = objdata[k]; if (roomobj[k] != objdata[k] && val !== null) { roomobj[k] = val; } } } else { objdata.persist = true; this.createObject('object', objdata); } } } } } this.loadRoomAssets = function(roomdata) { if (roomdata && roomdata.assets && roomdata.assets.assetlist && roomdata.assets.assetlist.length > 0) { var assetlist = roomdata.assets.assetlist; if (roomdata.assets.websurfaces) { elation.utils.merge(roomdata.assets.websurfaces, this.websurfaces); if (!this.roomassets.websurface) this.roomassets.websurface = {}; elation.utils.merge(roomdata.assets.websurfaces, this.roomassets.websurface); } if (roomdata.assets.ghosts) { var ghosts = roomdata.assets.ghosts; for (var i = 0; i < ghosts.length; i++) { var ghost = ghosts[i]; this.ghosts[ghost.id] = ghost; assetlist.push({ assettype: 'file', name: ghost.id, src: ghost.src }); } //elation.utils.merge(roomdata.assets.ghosts, this.ghosts); } if (roomdata.assets.shaders) { let shaders = roomdata.assets.shaders; for (var i = 0; i < shaders.length; i++) { if (shaders[i].uniforms) { //shaders[i].uniforms = this.parseShaderUniforms(shaders[i].uniforms); } } } //this.assetpack = elation.engine.assets.loadJSON(assetlist, this.baseurl); if (!this.assetpack) { this.assetpack = new elation.engine.assets.pack({name: this.id + '_assets', baseurl: this.baseurl, json: assetlist}); } else { this.assetpack.loadJSON(assetlist); } } } this.clear = function() { for (var k in this.children) { this.remove(this.children[k]); } this.createLights(); } this.createRoomObjects = function(roomdata, parent, isinternal) { var room = roomdata.room, assets = roomdata.assets || []; // Prevent our loads from echoing back to the server as edits this.applyingEdits = true; // Parse room objects from the XML, and create Janus objects for them var exclude = ['assets', 'room', 'source']; for (var k in roomdata) { if (exclude.indexOf(k) == -1) { roomdata[k].forEach(elation.bind(this, function(n) { n.persist = true; n.isinternal = isinternal; this.createObject(k, n, parent); })); } } if (room && !parent) { if (room.use_local_asset) { var modelid = (room.visible !== false ? room.use_local_asset : undefined), collisionid = room.use_local_asset + '_collision', collisionscale = V(1,1,1), collisionpos = V(0,0,0); if (room.use_local_asset == 'room_plane') { collisionid = 'cube'; collisionscale.set(100000,100,100000); collisionpos.set(0,-50,0); } this.localasset = this.createObject('object', { id: modelid, collision_id: collisionid, collision_scale: collisionscale, collision_pos: collisionpos, col: room.col, //fwd: room.fwd, xdir: room.xdir, ydir: room.ydir, zdir: room.zdir, shadows: true }); } // set player spawnpoint based on room info if (room.pos) { this.spawnpoint.position.fromArray(room.pos); } if (room.xdir || room.ydir || room.zdir) { room.orientation = this.janus.parser.getOrientation(room.xdir, room.ydir, room.zdir); } if (room.orientation) { this.spawnpoint.quaternion.copy(room.orientation); this.spawnpoint.quaternion.multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(0,Math.PI,0))); // Janus Native starts the player backwards this.spawnpoint.updateMatrixWorld(); } // If we have a referrer, check to see if a reciprocal link exists. If it does, use this as our spawn point. if (this.referrer) { let hasReciprocalLink = false; let links = this.getObjectsByTagName('link'); if (links && links.length > 0) { links.forEach(link => { let url = this.getFullRoomURL(link.url); if (url == this.referrer) { this.spawnpoint.quaternion.copy(link.orientation).invert(); this.spawnpoint.position.copy(link.pos); this.spawnpoint.position.add(this.spawnpoint.localToWorld(V(0,0,-player.fatness))); hasReciprocalLink = true; } }); } if (!hasReciprocalLink) { // If no reciprocal link was found, spawn one so we can find our way back let linkrot = new EulerDegrees(); linkrot.radians.copy(this.spawnpoint.rotation); //linkrot.x *= THREE.MathUtils.RAD2DEG; linkrot.y = linkrot.y + 180; //linkrot.z *= THREE.MathUtils.RAD2DEG; let linkpos = this.spawnpoint.localToWorld(V(0,0,player.fatness/2)); this.createObject('link', { pos: linkpos, rotation: linkrot, url: this.referrer, round: true, shader_id: 'defaultportal', js_id: 'reciprocal-link', }); } } if (this.active) { setTimeout(() => this.setPlayerPosition(), 0); } if (typeof room.skybox != 'undefined') this.properties.skybox = room.skybox; if (room.skybox_equi) this.properties.skybox_equi = room.skybox_equi; if (room.skybox_left_id) this.properties.skybox_left_id = room.skybox_left_id; if (room.skybox_right_id) this.properties.skybox_right_id = room.skybox_right_id; if (room.skybox_up_id) this.properties.skybox_up_id = room.skybox_up_id; if (room.skybox_down_id) this.properties.skybox_down_id = room.skybox_down_id; if (room.skybox_front_id) this.properties.skybox_front_id = room.skybox_front_id; if (room.skybox_back_id) this.properties.skybox_back_id = room.skybox_back_id; if (typeof room.skybox_intensity != 'undefined') this.skybox_intensity = room.skybox_intensity; if (room.cubemap_radiance_id) this.properties.cubemap_radiance_id = room.cubemap_radiance_id; if (room.cubemap_irradiance_id) this.properties.cubemap_irradiance_id = room.cubemap_irradiance_id; this.setSkybox(); if (room.server) this.properties.server = room.server; if (room.port) this.properties.port = room.port; if (room.rate) this.properties.rate = room.rate; if (room.voip) this.properties.voip = room.voip || 'janus'; if (room.voipid) this.properties.voipid = room.voipid; if (room.voiprange) this.properties.voiprange = room.voiprange; if (room.voipserver) this.properties.voipserver = room.voipserver; if (room.gazetime) this.properties.gazetime = room.gazetime; if (typeof room.pbr != 'undefined') this.properties.pbr = room.pbr; if (typeof room.toon != 'undefined') this.properties.toon = room.toon; if (typeof room.ambient != 'undefined') this.ambient = room.ambient; if (typeof room.defaultlights != 'undefined') this.defaultlights = room.defaultlights; if (typeof room.selfavatar != 'undefined') this.properties.selfavatar = (room.selfavatar == "true"); this.properties.near_dist = parseFloat(room.near_dist) || 0.01; this.properties.far_dist = parseFloat(room.far_dist) || 1000; this.properties.fog = room.fog; this.properties.private = room.private; this.properties.fog_mode = room.fog_mode || 'exp'; this.properties.fog_density = room.fog_density; this.properties.fog_start = parseFloat(room.fog_start) || this.properties.near_dist; this.properties.fog_end = parseFloat(room.fog_end) || this.properties.far_dist; this.fog_col = room.fog_col || room.fog_color; this.properties.bloom = room.bloom || 0.05; this.properties.tonemapping_type = room.tonemapping_type || 'linear'; this.properties.tonemapping_exposure = room.tonemapping_exposure || this.tonemapping_exposure; this.properties.shadows = elation.utils.any(room.shadows, false); this.properties.party_mode = elation.utils.any(room.party_mode, true); this.properties.locked = room.locked || false; this.gravity = elation.utils.any(room.gravity, 0); this.flying = elation.utils.any(room.flying, true); this.teleport = elation.utils.any(room.teleport, true); this.pointerlock = elation.utils.any(room.pointerlock, true); this.defaultview = elation.utils.any(room.defaultview, null); this.showavatar = elation.utils.any(room.showavatar, true); if ('spawnradius' in room) this.spawnradius = room.spawnradius; if (typeof player != 'undefined' && this.defaultview && this.defaultview != player.cameraview) { player.setCameraView(this.defaultview); } //if (room.col) this.properties.col = room.col; this.properties.walk_speed = room.walk_speed || 1.8; this.properties.run_speed = room.run_speed || 5.4; this.properties.cursor_visible = room.cursor_visible; this.properties.cursor_opacity = room.cursor_opacity; let cookieStorageID = 'cookies.' + this.url; try { if (cookieStorageID in localStorage) { this.cookies = JSON.parse(localStorage['cookies.' + this.url]); } } catch (e) { this.cookies = []; } if (room.onload) { this.properties.onload = room.onload; this.addEventListenerProxy('room_load_complete', (ev) => { let func = new Function(room.onload); func();}); } if (assets.scripts) { this.pendingScripts = 0; this.loadScripts(assets.scripts); } let requires = room.requires || room.require; if (requires) { let roomproxy = this.getProxyObject(); roomproxy.require(requires); this.requires = requires; } } this.applyingEdits = false; //if (!this.active) { // this.setActive(); //} //this.showDebug(); } this.loadScripts = function(scripts) { scripts.forEach(s => { this.loadNewAsset('script', { src: s.src, override: { room: this.getProxyObject() } }); let scriptasset = this.getAsset('script', s.src); let script = scriptasset.getInstance(); if (scriptasset.loaded) { // If the script is already part of the document, remove it and readd it so it's reevaluated if (script.parentNode) { script.parentNode.removeChild(script); } let oldscript = script; script = document.createElement('script'); script.src = oldscript.src; document.head.appendChild(script); } else { if (!this.pendingScriptMap[s.src]) { this.pendingScripts++; this.pendingScriptMap[s.src] = true;; elation.events.add(scriptasset, 'asset_error', (ev) => { console.error('Failed to load Janus room script', scriptasset); this.doScriptOnload(); }); elation.events.add(script, 'asset_load', () => { script.onload = () => this.doScriptOnload(); document.head.appendChild(script); }); } } this.roomscripts.push(script); }); } this.getTranslator = function(url) { var keys = Object.keys(roomTranslators); for (var i = 0; i < keys.length; i++) { var re = new RegExp(keys[i]); if (url.match(re)) { return roomTranslators[keys[i]]; } } // TODO - implement default page handling as translator return false; } this.enable = function() { var keys = Object.keys(this.children); for (var i = 0; i < keys.length; i++) { var obj = this.children[keys[i]]; if (obj.start) { obj.start(); } } if (!this.enabled) { this.enabled = true; this.engine.systems.ai.add(this); elation.events.add(window, 'click', this.onClick); elation.events.add(window, 'keydown', this.onKeyDown); elation.events.add(window, 'keyup', this.onKeyUp); //elation.events.add(this, 'mousedown,touchstart', this.onMouseDown); //elation.events.add(this, 'mouseup,touchend', this.onMouseUp); //elation.events.add(this, 'mousemove', this.onMouseMove); elation.events.add(this, 'click', this.onObjectClick); /* elation.events.add(this, 'dragenter', this.handleDragOver); elation.events.add(this, 'dragover', this.handleDragOver); elation.events.add(this, 'drop', this.handleDrop); */ elation.events.add(this, 'thing_think', this.onScriptTick); elation.events.fire({type: 'room_enable', data: this}); if (this.audionodes) { this.audionodes.gain.connect(this.audionodes.listener.getInput()); //console.log('connect room audio to graph', this.audionodes.gain, this.audionodes.listener.getInput(), this); this.fadeAudioIn(2); } this.engine.systems.controls.pointerLockEnabled = this.pointerlock; if (this.engine.systems.admin) { elation.events.add(this.engine.systems.admin, 'admin_edit_change', elation.bind(this, this.onRoomEdit)); } } //this.showDebug(); } this.disable = function() { var keys = Object.keys(this.children); for (var i = 0; i < keys.length; i++) { var obj = this.children[keys[i]]; if (obj.stop) { obj.stop(); } } if (this.enabled) { this.engine.systems.ai.remove(this); elation.events.fire({type: 'room_disable', data: this}); this.enabled = false; elation.events.remove(this, 'thing_think', this.onScriptTick); elation.events.remove(window, 'click', this.onClick); elation.events.remove(window, 'keydown', this.onKeyDown); elation.events.remove(window, 'keyup', this.onKeyUp); elation.events.remove(this.engine.client.container, 'mousedown,touchstart', this.onMouseDown); elation.events.remove(this.engine.client.container, 'mouseup,touchend', this.onMouseUp); elation.events.remove(this, 'click', this.onObjectClick); elation.events.remove(this, 'dragover', this.handleDragOver); elation.events.remove(this, 'dragenter', this.handleDragOver); elation.events.remove(this, 'drop', this.handleDrop); if (this.intervals) { console.log('stop intervals:', this.intervals); while (this.intervals.length > 0) { clearInterval(this.intervals.pop()); } } if (this.timers) { console.log('stop timers:', this.timers); while (this.timers.length > 0) { clearTimeout(this.timers.pop()); } } if (this.audionodes) { this.fadeAudioOut(1); setTimeout(() => { this.audionodes.gain.disconnect(this.audionodes.listener.getInput()); console.log('disconnect room audio from graph', this.audionodes.gain, this.audionodes.listener.getInput(), this); }, 1500); } //this.engine.systems.controls.pointerLockEnabled = this.pointerlock; } } this.setTitle = function(title) { if (!title) title = 'Untitled Page'; this.title = title; document.title = this.title; } this.applyEditXML = function(editxml) { var xml = elation.utils.parseXML('<edit>' + editxml + '</edit>'); var edit = xml.edit._children; if (!edit) return; var keys = Object.keys(edit); var hasNew = false; var waslocked = this.locked; //this.locked = true; this.applyingEdits = true; var skip = ['sync', 'autosync']; keys.forEach(elation.bind(this, function(k) { var newobjs = edit[k]; if (!elation.utils.isArray(newobjs)) newobjs = [newobjs]; var diff = { assets: { objects: [], image: [],