Newer
Older
console.log(`Hubs version: ${process.env.BUILD_VERSION || "?"}`);
import "./assets/stylesheets/hub.scss";
import { patchWebGLRenderingContext } from "./utils/webgl";
patchWebGLRenderingContext();
import "three/examples/js/loaders/GLTFLoader";
import "networked-aframe/src/index";
import "naf-janus-adapter";
import "aframe-rounded";
import "aframe-motion-capture-components";
import "./utils/audio-context-fix";
import { getReticulumFetchUrl } from "./utils/phoenix-utils";
import { addAnimationComponents } from "./utils/animation";
import "./components/wasd-to-analog2d"; //Might be a behaviour or activator in the future
import "./components/bone-visibility";
joni
committed
import "./components/character-controller";
Brian Peiris
committed
import "./components/hoverable-visuals";
import "./components/hover-visuals";
import "./components/offset-relative-to";
import "./components/player-info";
import "./components/debug";
import "./components/freeze-controller";
import "./components/visible-while-frozen";
import "./components/stats-plus";
import "./components/networked-avatar";
import "./components/media-views";
import "./components/pinch-to-move";
import "./components/pitch-yaw-rotator";
import "./components/auto-scale-cannon-physics-body";
import "./components/position-at-box-shape-border";
import "./components/remove-networked-object-button";
import "./components/destroy-at-extreme-distances";
import "./components/scene-sound";
import "./components/emit-state-change";
import "./components/action-to-event";
import "./components/emit-scene-event-on-remove";
import "./components/stop-event-propagation";
import ReactDOM from "react-dom";
import React from "react";
import UIRoot from "./react-components/ui-root";
import HubChannel from "./utils/hub-channel";
import LinkChannel from "./utils/link-channel";
import { connectToReticulum } from "./utils/phoenix-utils";
import { disableiOSZoom } from "./utils/disable-ios-zoom";
import { proxiedUrlFor } from "./utils/media-utils";
import SceneEntryManager from "./scene-entry-manager";
import { createInWorldLogMessage } from "./react-components/chat-message";
import "./systems/nav";
import "./systems/personal-space-bubble";
import "./systems/app-mode";
import "./systems/userinput/userinput-debug";
import "./gltf-component-mappings";
import { App } from "./App";
window.APP = new App();
window.APP.RENDER_ORDER = {
HUD_BACKGROUND: 1,
HUD_ICONS: 2,
CURSOR: 3
};
const store = window.APP.store;
const qs = new URLSearchParams(location.search);
const isMobile = AFRAME.utils.device.isMobile();
window.APP.quality = qs.get("quality") || isMobile ? "low" : "high";
import "aframe-physics-system";
import "aframe-physics-extras";
import "super-hands";
import "./components/super-networked-interactable";
import "./components/networked-counter";
import "./components/event-repeater";
import "./components/controls-shape-offset";
import "./components/set-yxz-order";
import "./components/cardboard-controls";
import "./components/cursor-controller";
import "./components/nav-mesh-helper";
import "./components/tools/pen";
import "./components/tools/networked-drawing";
import "./components/tools/drawing-manager";
import registerNetworkSchemas from "./network-schemas";
import { warmSerializeElement } from "./utils/serialize-element";
import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js";
import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
Brian Peiris
committed
const isTelemetryDisabled = qsTruthy("disable_telemetry");
const isDebug = qsTruthy("debug");
if (!isBotMode && !isTelemetryDisabled) {
disableiOSZoom();
const concurrentLoadDetector = new ConcurrentLoadDetector();
concurrentLoadDetector.start();
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
function getPlatformUnsupportedReason() {
if (typeof RTCDataChannelEvent === "undefined") return "no_data_channels";
return null;
}
function pollForSupportAvailability(callback) {
const availabilityUrl = getReticulumFetchUrl("/api/v1/support/availability");
let isSupportAvailable = null;
const updateIfChanged = () =>
fetch(availabilityUrl).then(({ ok }) => {
if (isSupportAvailable === ok) return;
isSupportAvailable = ok;
callback(isSupportAvailable);
});
updateIfChanged();
setInterval(updateIfChanged, 30000);
}
function setupLobbyCamera() {
const camera = document.querySelector("#player-camera");
const previewCamera = document.querySelector("#environment-scene").object3D.getObjectByName("scene-preview-camera");
if (previewCamera) {
camera.object3D.position.copy(previewCamera.position);
camera.object3D.rotation.copy(previewCamera.rotation);
camera.object3D.updateMatrix();
} else {
const cameraPos = camera.object3D.position;
camera.object3D.position.set(cameraPos.x, 2.5, cameraPos.z);
}
camera.setAttribute("scene-preview-camera", "positionOnly: true; duration: 60");
camera.components["pitch-yaw-rotator"].set(camera.object3D.rotation.x, camera.object3D.rotation.y);
}
let uiProps = {};
function mountUI(props = {}) {
const scene = document.querySelector("a-scene");
Brian Peiris
committed
const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
const forcedVREntryType = qs.get("vr_entry_type");
<UIRoot
{...{
scene,
concurrentLoadDetector,
disableAutoExitOnConcurrentLoad,
forcedVREntryType,
}}
/>,
document.getElementById("ui-root")
);
function remountUI(props) {
uiProps = { ...uiProps, ...props };
mountUI(uiProps);
async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) {
const scene = document.querySelector("a-scene");
if (NAF.connection.isConnected()) {
// Send complete sync on phoenix re-join.
NAF.connection.entities.completeSync(null, true);
return;
}
const hub = data.hubs[0];
const defaultSpaceTopic = hub.topics[0];
const glbAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "glb");
const bundleAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle");
const sceneUrl = (glbAsset || bundleAsset).src;
const hasExtension = /\.gltf/i.test(sceneUrl) || /\.glb/i.test(sceneUrl);
console.log(`Scene URL: ${sceneUrl}`);
const environmentScene = document.querySelector("#environment-scene");
const objectsScene = document.querySelector("#objects-scene");
const objectsUrl = getReticulumFetchUrl(`/${hub.hub_id}/objects.gltf`);
const objectsEl = document.createElement("a-entity");
objectsEl.setAttribute("gltf-model-plus", { src: objectsUrl, useCache: false, inflate: true });
objectsScene.appendChild(objectsEl);
if (glbAsset || hasExtension) {
const gltfEl = document.createElement("a-entity");
gltfEl.setAttribute("gltf-model-plus", { src: proxiedUrlFor(sceneUrl), useCache: false, inflate: true });
gltfEl.addEventListener("model-loaded", () => environmentScene.emit("bundleloaded"));
environmentScene.appendChild(gltfEl);
} else {
// TODO kill bundles
environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`);
}
remountUI({
hubId: hub.hub_id,
hubName: hub.name,
hubEntryCode: hub.entry_code,
document
.querySelector("#hud-hub-entry-link")
.setAttribute("text", { value: `hub.link/${hub.entry_code}`, width: 1.1, align: "center" });
// Wait for scene objects to load before connecting, so there is no race condition on network state.
objectsEl.addEventListener("model-loaded", async el => {
if (el.target !== objectsEl) return;
scene.setAttribute("networked-scene", {
room: hub.hub_id,
serverURL: process.env.JANUS_SERVER,
debug: !!isDebug
while (!scene.components["networked-scene"] || !scene.components["networked-scene"].data) await nextTick();
scene.components["networked-scene"]
.connect()
.then(() => {
NAF.connection.adapter.reliableTransport = (clientId, dataType, data) => {
const payload = { dataType, data };
if (clientId) {
payload.clientId = clientId;
}
hubChannel.channel.push("naf", payload);
};
})
.catch(connectError => {
// hacky until we get return codes
const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i);
console.error(connectError);
remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" });
entryManager.exitScene();
return;
});
});
async function runBotMode(scene, entryManager) {
const noop = () => {};
scene.renderer = { setAnimationLoop: noop, render: noop };
while (!NAF.connection.isConnected()) await nextTick();
entryManager.enterSceneWhenLoaded(new MediaStream(), false);
}
document.addEventListener("DOMContentLoaded", async () => {
const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0];
console.log(`Hub ID: ${hubId}`);
const subscriptions = new Subscriptions(hubId);
navigator.serviceWorker
.register("/hub.service.js")
.then(() => {
navigator.serviceWorker.ready
.then(registration => subscriptions.setRegistration(registration))
.catch(() => subscriptions.setRegistrationFailed());
})
.catch(() => subscriptions.setRegistrationFailed());
} catch (e) {
subscriptions.setRegistrationFailed();
}
scene.removeAttribute("keyboard-shortcuts"); // Remove F and ESC hotkeys from aframe
const entryManager = new SceneEntryManager(hubChannel);
scene.addEventListener("enter-vr", () => {
document.body.classList.add("vr-mode");
if (!scene.is("entered")) {
// If VR headset is activated, refreshing page will fire vrdisplayactivate
// which puts A-Frame in VR mode, so exit VR mode whenever it is attempted
// to be entered and we haven't entered the room yet.
scene.addEventListener("exit-vr", () => document.body.classList.remove("vr-mode"));
remountUI({
hubChannel,
linkChannel,
subscriptions,
enterScene: entryManager.enterScene,
exitScene: entryManager.exitScene,
initialIsSubscribed: subscriptions.isSubscribed()
});
scene.addEventListener("action_focus_chat", () => document.querySelector(".chat-focus-target").focus());
pollForSupportAvailability(isSupportAvailable => remountUI({ isSupportAvailable }));
const platformUnsupportedReason = getPlatformUnsupportedReason();
if (platformUnsupportedReason) {
entryManager.exitScene();
if (qs.get("required_version") && process.env.BUILD_VERSION) {
const buildNumber = process.env.BUILD_VERSION.split(" ", 1)[0]; // e.g. "123 (abcd5678)"
if (qs.get("required_version") !== buildNumber) {
remountUI({ roomUnavailableReason: "version_mismatch" });
setTimeout(() => document.location.reload(), 5000);
entryManager.exitScene();
const availableVREntryTypes = await getAvailableVREntryTypes();
if (availableVREntryTypes.isInHMD) {
remountUI({ availableVREntryTypes, forcedVREntryType: "vr" });
if (/Oculus/.test(navigator.userAgent)) {
// HACK - The polyfill reports Cardboard as the primary VR display on startup out ahead of Oculus Go on Oculus Browser 5.5.0 beta. This display is cached by A-Frame,
// so we need to resolve that and get the real VRDisplay before entering as well.
const displays = await navigator.getVRDisplays();
const vrDisplay = displays.length && displays[0];
AFRAME.utils.device.getVRDisplay = () => vrDisplay;
}
} else {
remountUI({ availableVREntryTypes });
}
const environmentScene = document.querySelector("#environment-scene");
environmentScene.addEventListener("bundleloaded", () => {
for (const modelEl of environmentScene.children) {
addAnimationComponents(modelEl);
}
// Replace renderer with a noop renderer to reduce bot resource usage.
if (isBotMode) {
const socket = connectToReticulum(isDebug);
remountUI({ sessionId: socket.params().session_id });
const context = {
mobile: isMobile,
hmd: availableVREntryTypes.isInHMD
};
// Reticulum global channel
const retPhxChannel = socket.channel(`ret`, { hub_id: hubId });
retPhxChannel
.join()
.receive("ok", async data => subscriptions.setVapidPublicKey(data.vapid_public_key))
.receive("error", res => {
subscriptions.setVapidPublicKey(null);
console.error(res);
});
Greg Fodor
committed
const pushSubscriptionEndpoint = await subscriptions.getCurrentEndpoint();
const joinPayload = { profile: store.state.profile, push_subscription_endpoint: pushSubscriptionEndpoint, context };
const hubPhxChannel = socket.channel(`hub:${hubId}`, joinPayload);
const presenceLogEntries = [];
const addToPresenceLog = entry => {
entry.key = Date.now().toString();
presenceLogEntries.push(entry);
remountUI({ presenceLogEntries });
// Fade out and then remove
setTimeout(() => {
entry.expired = true;
remountUI({ presenceLogEntries });
setTimeout(() => {
presenceLogEntries.splice(presenceLogEntries.indexOf(entry), 1);
remountUI({ presenceLogEntries });
}, 5000);
const messageDispatch = new MessageDispatch(scene, entryManager, hubChannel, addToPresenceLog, remountUI);
hubPhxChannel
.join()
.receive("ok", async data => {
hubChannel.setPhoenixChannel(hubPhxChannel);
subscriptions.setHubChannel(hubChannel);
subscriptions.setSubscribed(data.subscriptions.web_push);
remountUI({ initialIsSubscribed: subscriptions.isSubscribed() });
await handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data);
})
.receive("error", res => {
if (res.reason === "closed") {
entryManager.exitScene();
remountUI({ roomUnavailableReason: "closed" });
}
console.error(res);
});
const hubPhxPresence = new Presence(hubPhxChannel);
const vrHudPresenceCount = document.querySelector("#hud-presence-count");
remountUI({ presences: hubPhxPresence.state });
const occupantCount = Object.entries(hubPhxPresence.state).length;
vrHudPresenceCount.setAttribute("text", "value", occupantCount.toString());
if (!isInitialSync) return;
// Wire up join/leave event handlers after initial sync.
isInitialSync = false;
hubPhxPresence.onJoin((sessionId, current, info) => {
const meta = info.metas[info.metas.length - 1];
if (current) {
// Change to existing presence
const isSelf = sessionId === socket.params().session_id;
const currentMeta = current.metas[0];
if (!isSelf && currentMeta.presence !== meta.presence && meta.profile.displayName) {
type: "entered",
presence: meta.presence,
name: meta.profile.displayName
});
}
if (currentMeta.profile && meta.profile && currentMeta.profile.displayName !== meta.profile.displayName) {
addToPresenceLog({
type: "display_name_changed",
oldName: currentMeta.profile.displayName,
newName: meta.profile.displayName
});
}
} else {
// New presence
if (meta.presence && meta.profile.displayName) {
type: "join",
presence: meta.presence,
}
});
hubPhxPresence.onLeave((sessionId, current, info) => {
if (current && current.metas.length > 0) return;
const meta = info.metas[0];
if (meta.profile.displayName) {
addToPresenceLog({
type: "leave",
name: meta.profile.displayName
});
}
});
if (!NAF.connection.adapter) return;
NAF.connection.adapter.onData(data);
hubPhxChannel.on("message", ({ session_id, type, body }) => {
const userInfo = hubPhxPresence.state[session_id];
const incomingMessage = { name: userInfo.metas[0].profile.displayName, type, body, maySpawn };
if (scene.is("vr-mode")) {
createInWorldLogMessage(incomingMessage);
}