<commit_before>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'
  ], function() {
  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 },
        'skybox_left': { type: 'string' },
        'skybox_right': { type: 'string' },
        'skybox_up': { type: 'string' },
        'skybox_down': { type: 'string' },
        'skybox_front': { type: 'string' },
        'skybox_back': { type: 'string' },
        '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 },
        'run_speed': { type: 'float', default:  5.4 },
        '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 },
        '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 },
        'voipserver': { type: 'string' },
        'classList': { type: 'object', default: [] },
        'className': { type: 'string', default: '', set: this.setClassName },
        'gazetime': { type: 'float', default: 1000 },
        'selfavatar': { type: 'boolean', default: false },
        'requirescripts': { type: 'string' },
        'onload': { type: 'string' },
        'sync': { type: 'boolean', default: false },
      });
      this.translators = {
        '^about:blank$': elation.janusweb.translators.blank({janus: this.janus}),
        '^bookmarks$': elation.janusweb.translators.bookmarks({janus: this.janus}),
        '^dat:': elation.janusweb.translators.dat({janus: this.janus}),
        '^https?:\/\/(www\.)?reddit.com': elation.janusweb.translators.reddit({janus: this.janus}),
        '^error$': elation.janusweb.translators.error({janus: this.janus}),
        '^default$': elation.janusweb.translators.default({janus: this.janus})
      };
      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 = {};

      // 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.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));
    }
    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.setSkybox();
      this.setFog();
      this.updateBloom();
      this.updateToneMapping();
      this.setNearFar();
      this.setPlayerPosition();
      this.active = true;
      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.position.copy(pos);
      player.orientation.copy(orientation);
      //player.reset_position();
      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 = 1;
      // 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();
      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 (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().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);
        }
      }
      return spawnpoint;
    }
    this.setSkybox = function() {
      if (!this.loaded) return;

      if (!this.skybox) {
        this.engine.systems.render.renderer.setClearAlpha(0);
      } 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 || this.skybox_right || this.skybox_top || this.skybox_bottom || this.skybox_left || this.skybox_right) != undefined;
      if (hasSkybox) {
        var assets = [
          this.getAsset('image', this.skybox_right || 'black'),
          this.getAsset('image', this.skybox_left || 'black'),
          this.getAsset('image', this.skybox_up || 'black'),
          this.getAsset('image', this.skybox_down || 'black'),
          this.getAsset('image', this.skybox_front || 'black'),
          this.getAsset('image', this.skybox_back || 'black')
        ];
      } else {
        var skyboxname = 'dayskybox';
        var 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();
            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.needsUpdate = true;
          this.skyboxtexture = texture;
          if (this.janus.currentroom === this) {
            this.skyboxobj.setTexture(this.skyboxtexture);
          }
          return true;
        }
      }
    }
    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() {
      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() {
      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 = 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();<commit_msg>Bypass cors proxy for same-origin content<commit_after>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'
  ], function() {
  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 },
        'skybox_left': { type: 'string' },
        'skybox_right': { type: 'string' },
        'skybox_up': { type: 'string' },
        'skybox_down': { type: 'string' },
        'skybox_front': { type: 'string' },
        'skybox_back': { type: 'string' },
        '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 },
        'run_speed': { type: 'float', default:  5.4 },
        '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 },
        '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 },
        'voipserver': { type: 'string' },
        'classList': { type: 'object', default: [] },
        'className': { type: 'string', default: '', set: this.setClassName },
        'gazetime': { type: 'float', default: 1000 },
        'selfavatar': { type: 'boolean', default: false },
        'requirescripts': { type: 'string' },
        'onload': { type: 'string' },
        'sync': { type: 'boolean', default: false },
      });
      this.translators = {
        '^about:blank$': elation.janusweb.translators.blank({janus: this.janus}),
        '^bookmarks$': elation.janusweb.translators.bookmarks({janus: this.janus}),
        '^dat:': elation.janusweb.translators.dat({janus: this.janus}),
        '^https?:\/\/(www\.)?reddit.com': elation.janusweb.translators.reddit({janus: this.janus}),
        '^error$': elation.janusweb.translators.error({janus: this.janus}),
        '^default$': elation.janusweb.translators.default({janus: this.janus})
      };
      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 = {};

      // 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.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));
    }
    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.setSkybox();
      this.setFog();
      this.updateBloom();
      this.updateToneMapping();
      this.setNearFar();
      this.setPlayerPosition();
      this.active = true;
      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.position.copy(pos);
      player.orientation.copy(orientation);
      //player.reset_position();
      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 = 1;
      // 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();
      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 (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().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);
        }
      }
      return spawnpoint;
    }
    this.setSkybox = function() {
      if (!this.loaded) return;

      if (!this.skybox) {
        this.engine.systems.render.renderer.setClearAlpha(0);
      } 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 || this.skybox_right || this.skybox_top || this.skybox_bottom || this.skybox_left || this.skybox_right) != undefined;
      if (hasSkybox) {
        var assets = [
          this.getAsset('image', this.skybox_right || 'black'),
          this.getAsset('image', this.skybox_left || 'black'),
          this.getAsset('image', this.skybox_up || 'black'),
          this.getAsset('image', this.skybox_down || 'black'),
          this.getAsset('image', this.skybox_front || 'black'),
          this.getAsset('image', this.skybox_back || 'black')
        ];
      } else {
        var skyboxname = 'dayskybox';
        var 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();
            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.needsUpdate = true;
          this.skyboxtexture = texture;
          if (this.janus.currentroom === this) {
            this.skyboxobj.setTexture(this.skyboxtexture);
          }
          return true;
        }
      }
    }
    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() {
      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() {
      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();
{'fraction_non_alphanumeric': 0.1521274619100706, 'fraction_numerical': 0.008570234113712374, 'mean_word_length': 2.398342541436464, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '570175', 'n_tokens_mistral': 13759, 'n_tokens_neox': 13333, 'n_words': 2578}