diff --git a/README.md b/README.md
index 08006517e6..faf0a0d32b 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,12 @@ Then visit https://hubs.local:8080 (note: HTTPS is required, you'll need to acce
> Note: When running the Hubs client locally, you will still connect to the development versions of the [reticulum](https://github.com/Hubs-Foundation/reticulum) server. This server does not allow being accessed outside of localhost. If you want to host your own Hubs servers, please check out [Hubs Community Edition](https://github.com/Hubs-Foundation/hubs-cloud/tree/master/community-edition).
+## Add-ons
+
+Hubs client add-ons are pluggable libraries that are installed as part of the client and loaded at runtime. Add-ons allow functionality to the Hubs core and allow easy Hubs client extensibility while maintaining the Hubs client core lean and minimal.
+
+You can read more about add-ons installation and development [here](doc/add-ons.md).
+
## Contributing
Read our [contributor guide](./CONTRIBUTING.md) to learn how you can submit bug reports, feature requests, and pull requests.
@@ -49,11 +55,10 @@ Contributors are expected to abide by the project's [Code of Conduct](./CODE_OF_
## Additional Resources
-* [Reticulum](https://github.com/Hubs-Foundation/reticulum) - Phoenix-based backend for managing state and presence.
-* [Networked A-Frame](https://github.com/Hubs-Foundation/networked-aframe).
-* [Hubs-Ops](https://github.com/Hubs-Foundation/hubs-ops) - Infrastructure as code + management tools for running necessary backend services on AWS.
+- [Reticulum](https://github.com/Hubs-Foundation/reticulum) - Phoenix-based backend for managing state and presence.
+- [Networked A-Frame](https://github.com/Hubs-Foundation/networked-aframe).
+- [Hubs-Ops](https://github.com/Hubs-Foundation/hubs-ops) - Infrastructure as code + management tools for running necessary backend services on AWS.
## License
Hubs is licensed with the [Mozilla Public License 2.0](./LICENSE)
-
diff --git a/addons.json b/addons.json
new file mode 100644
index 0000000000..702f72fd45
--- /dev/null
+++ b/addons.json
@@ -0,0 +1,8 @@
+{
+ "addons": [
+ "hubs-duck-addon",
+ "hubs-portals-addon",
+ "hubs-behavior-graphs-addon",
+ "hubs-postprocessing-addon"
+ ]
+}
\ No newline at end of file
diff --git a/admin/package-lock.json b/admin/package-lock.json
index 29904c666b..53c02fbde5 100644
--- a/admin/package-lock.json
+++ b/admin/package-lock.json
@@ -11,15 +11,15 @@
"dependencies": {
"@iarna/toml": "^2.2.3",
"@mozilla/lilypad-ui": "^1.8.2",
- "aframe": "github:hubs-foundation/aframe#hubs/master",
- "bitecs": "github:hubs-foundation/bitECS#hubs-patches",
+ "aframe": "github:Hubs-Foundation/aframe#hubs/master",
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
"classnames": "^2.2.5",
"hubs": "file:..",
"react": "^16.1.1",
"react-admin": "^2.6.3",
"react-dom": "^16.1.1",
"react-intl": "^2.4.0",
- "three": "github:hubs-foundation/three.js#hubs-patches-141"
+ "three": "github:Hubs-Foundation/three.js#hubs-patches-141"
},
"devDependencies": {
"@babel/core": "^7.18.9",
@@ -78,14 +78,14 @@
"@fortawesome/fontawesome-svg-core": "^1.2.2",
"@fortawesome/free-solid-svg-icons": "^5.2.0",
"@fortawesome/react-fontawesome": "^0.1.0",
- "@mozilla/lilypad-ui": "^1.8.3",
+ "@mozilla/lilypad-ui": "1.8.6",
"@mozillareality/easing-functions": "^0.1.1",
"@popperjs/core": "^2.4.4",
- "aframe": "github:hubs-foundation/aframe#hubs/master",
+ "aframe": "github:Hubs-Foundation/aframe#hubs/master",
"ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer",
- "ammo.js": "github:hubs-foundation/ammo.js#hubs/master",
- "animejs": "github:hubs-foundation/anime#hubs/master",
- "bitecs": "github:hubs-foundation/bitECS#hubs-patches",
+ "ammo.js": "github:Hubs-Foundation/ammo.js#hubs/master",
+ "animejs": "github:Hubs-Foundation/anime#hubs/master",
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
"buffered-interpolation": "github:Infinitelee/buffered-interpolation",
"classnames": "^2.2.5",
"color": "^3.1.2",
@@ -96,22 +96,24 @@
"detect-browser": "^3.0.1",
"downshift": "^6.0.5",
"draft-js": "^0.11.7",
- "emoji-mart": "^5.5.2",
+ "emoji-picker-react": "^4.4.9",
"event-target-shim": "^3.0.1",
"form-data": "^3.0.0",
"form-urlencoded": "^2.0.4",
"history": "^4.7.2",
"hls.js": "^0.14.6",
"html2canvas": "^1.0.0-rc.7",
+ "hubs-duck-addon": "github:Hubs-Foundation/hubs-duck-addon",
+ "hubs-portals-addon": "github:Hubs-Foundation/hubs-portals-addon",
"js-cookie": "^2.2.0",
"jsonschema": "^1.2.2",
"jwt-decode": "^2.2.0",
- "lib-hubs": "github:hubs-foundation/lib-hubs#master",
+ "lib-hubs": "github:Hubs-Foundation/lib-hubs#master",
"linkify-it": "^2.0.3",
"markdown-it": "^12.3.2",
"moving-average": "^1.0.0",
- "networked-aframe": "github:hubs-foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
- "nipplejs": "github:hubs-foundation/nipplejs#mr-social-client/master",
+ "networked-aframe": "github:Hubs-Foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
+ "nipplejs": "github:Hubs-Foundation/nipplejs#mr-social-client/master",
"node-ensure": "0.0.0",
"normalize.css": "^8.0.1",
"pdfjs-dist": "^2.14.305",
@@ -135,13 +137,15 @@
"screenfull": "^4.0.1",
"sdp-transform": "^2.14.1",
"semver": "^7.3.2",
- "three": "github:hubs-foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
- "three-ammo": "github:hubs-foundation/three-ammo",
+ "stream-browserify": "^3.0.0",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
+ "three-ammo": "github:Hubs-Foundation/three-ammo",
"three-gltf-extensions": "^0.0.14",
"three-mesh-bvh": "^0.3.7",
"three-pathfinding": "^1.1.0",
"three-to-ammo": "github:infinitelee/three-to-ammo",
"troika-three-text": "^0.45.0",
+ "url": "^0.11.1",
"use-clipboard-copy": "^0.2.0",
"uuid": "^3.2.1",
"webrtc-adapter": "^7.7.0",
@@ -162,13 +166,12 @@
"@formatjs/cli": "^5.0.6",
"@formatjs/cli-lib": "^5.1.0",
"@iarna/toml": "^2.2.5",
- "@storybook/addon-actions": "^6.5.9",
- "@storybook/addon-essentials": "^6.5.9",
- "@storybook/addon-links": "^6.5.9",
- "@storybook/builder-webpack5": "^6.5.9",
- "@storybook/manager-webpack5": "^6.5.9",
- "@storybook/react": "^6.5.9",
- "@storybook/storybook-deployer": "^2.8.12",
+ "@storybook/addon-actions": "^7.0.20",
+ "@storybook/addon-essentials": "^7.0.20",
+ "@storybook/addon-links": "^7.0.20",
+ "@storybook/react": "^7.0.20",
+ "@storybook/react-webpack5": "^7.0.20",
+ "@storybook/storybook-deployer": "^2.8.16",
"@svgr/webpack": "^6.3.1",
"@types/three": "^0.141.0",
"@types/webxr": "^0.5.0",
@@ -185,7 +188,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
- "eslint-plugin-storybook": "^0.6.1",
+ "eslint-plugin-storybook": "^0.6.12",
"esm": "^3.2.25",
"fast-plural-rules": "1.0.2",
"file-loader": "^6.2.0",
@@ -215,7 +218,8 @@
"sass-loader": "^13.0.2",
"selfsigned": "^2.0.1",
"shelljs": "^0.8.5",
- "spritesheet-js": "github:hubs-foundation/spritesheet.js#hubs/master",
+ "spritesheet-js": "github:Hubs-Foundation/spritesheet.js#hubs/master",
+ "storybook": "^7.0.20",
"style-loader": "^3.3.1",
"stylelint": "^14.9.1",
"stylelint-config-recommended-scss": "^7.0.0",
@@ -2978,7 +2982,7 @@
},
"node_modules/aframe": {
"version": "1.0.3",
- "resolved": "git+ssh://git@github.com/hubs-foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79",
"license": "MIT",
"dependencies": {
"custom-event-polyfill": "^1.0.6",
@@ -3466,7 +3470,7 @@
},
"node_modules/bitecs": {
"version": "0.3.38",
- "resolved": "git+ssh://git@github.com/hubs-foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e",
"license": "MPL-2.0"
},
"node_modules/body-parser": {
@@ -9749,7 +9753,7 @@
},
"node_modules/three": {
"version": "0.141.0",
- "resolved": "git+ssh://git@github.com/hubs-foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777",
"license": "MIT"
},
"node_modules/thunky": {
@@ -12740,8 +12744,8 @@
"requires": {}
},
"aframe": {
- "version": "git+ssh://git@github.com/hubs-foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79",
- "from": "aframe@github:hubs-foundation/aframe#hubs/master",
+ "version": "git+ssh://git@github.com/Hubs-Foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79",
+ "from": "aframe@github:Hubs-Foundation/aframe#hubs/master",
"requires": {
"custom-event-polyfill": "^1.0.6",
"debug": "ngokevin/debug#noTimestamp",
@@ -13107,8 +13111,8 @@
"dev": true
},
"bitecs": {
- "version": "git+ssh://git@github.com/hubs-foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e",
- "from": "bitecs@github:hubs-foundation/bitECS#hubs-patches"
+ "version": "git+ssh://git@github.com/Hubs-Foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e",
+ "from": "bitecs@github:Hubs-Foundation/bitECS#hubs-patches"
},
"body-parser": {
"version": "1.20.1",
@@ -15117,28 +15121,27 @@
"@fortawesome/free-solid-svg-icons": "^5.2.0",
"@fortawesome/react-fontawesome": "^0.1.0",
"@iarna/toml": "^2.2.5",
- "@mozilla/lilypad-ui": "^1.8.3",
+ "@mozilla/lilypad-ui": "1.8.6",
"@mozillareality/easing-functions": "^0.1.1",
"@popperjs/core": "^2.4.4",
- "@storybook/addon-actions": "^6.5.9",
- "@storybook/addon-essentials": "^6.5.9",
- "@storybook/addon-links": "^6.5.9",
- "@storybook/builder-webpack5": "^6.5.9",
- "@storybook/manager-webpack5": "^6.5.9",
- "@storybook/react": "^6.5.9",
- "@storybook/storybook-deployer": "^2.8.12",
+ "@storybook/addon-actions": "^7.0.20",
+ "@storybook/addon-essentials": "^7.0.20",
+ "@storybook/addon-links": "^7.0.20",
+ "@storybook/react": "^7.0.20",
+ "@storybook/react-webpack5": "^7.0.20",
+ "@storybook/storybook-deployer": "^2.8.16",
"@svgr/webpack": "^6.3.1",
"@types/three": "^0.141.0",
"@types/webxr": "^0.5.0",
"acorn": "^8.8.0",
- "aframe": "github:hubs-foundation/aframe#hubs/master",
+ "aframe": "github:Hubs-Foundation/aframe#hubs/master",
"ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer",
- "ammo.js": "github:hubs-foundation/ammo.js#hubs/master",
- "animejs": "github:hubs-foundation/anime#hubs/master",
+ "ammo.js": "github:Hubs-Foundation/ammo.js#hubs/master",
+ "animejs": "github:Hubs-Foundation/anime#hubs/master",
"ava": "^4.3.1",
"babel-loader": "^8.2.5",
"babel-plugin-react-intl": "^8.2.21",
- "bitecs": "github:hubs-foundation/bitECS#hubs-patches",
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
"buffered-interpolation": "github:Infinitelee/buffered-interpolation",
"classnames": "^2.2.5",
"color": "^3.1.2",
@@ -15153,13 +15156,13 @@
"dotenv": "^16.0.1",
"downshift": "^6.0.5",
"draft-js": "^0.11.7",
- "emoji-mart": "^5.5.2",
+ "emoji-picker-react": "^4.4.9",
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
- "eslint-plugin-storybook": "^0.6.1",
+ "eslint-plugin-storybook": "^0.6.12",
"esm": "^3.2.25",
"event-target-shim": "^3.0.1",
"fast-plural-rules": "1.0.2",
@@ -15176,20 +15179,22 @@
"html-webpack-plugin": "^5.5.0",
"html2canvas": "^1.0.0-rc.7",
"htmlhint": "^1.1.4",
+ "hubs-duck-addon": "github:Hubs-Foundation/hubs-duck-addon",
+ "hubs-portals-addon": "github:Hubs-Foundation/hubs-portals-addon",
"internal-ip": "^7.0.0",
"js-cookie": "^2.2.0",
"jsdom": "^20.0.0",
"jsonschema": "^1.2.2",
"jwt-decode": "^2.2.0",
- "lib-hubs": "github:hubs-foundation/lib-hubs#master",
+ "lib-hubs": "github:Hubs-Foundation/lib-hubs#master",
"linkify-it": "^2.0.3",
"localstorage-memory": "^1.0.3",
"markdown-it": "^12.3.2",
"mediasoup-client": "^3.6.54",
"mini-css-extract-plugin": "^2.6.1",
"moving-average": "^1.0.0",
- "networked-aframe": "github:hubs-foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
- "nipplejs": "github:hubs-foundation/nipplejs#mr-social-client/master",
+ "networked-aframe": "github:Hubs-Foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
+ "nipplejs": "github:Hubs-Foundation/nipplejs#mr-social-client/master",
"node-ensure": "0.0.0",
"node-fetch": "^2.6.7",
"normalize.css": "^8.0.1",
@@ -15228,14 +15233,16 @@
"selfsigned": "^2.0.1",
"semver": "^7.3.2",
"shelljs": "^0.8.5",
- "spritesheet-js": "github:hubs-foundation/spritesheet.js#hubs/master",
+ "spritesheet-js": "github:Hubs-Foundation/spritesheet.js#hubs/master",
+ "storybook": "^7.0.20",
+ "stream-browserify": "^3.0.0",
"style-loader": "^3.3.1",
"stylelint": "^14.9.1",
"stylelint-config-recommended-scss": "^7.0.0",
"stylelint-scss": "^4.3.0",
"tar": "^6.1.11",
- "three": "github:hubs-foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
- "three-ammo": "github:hubs-foundation/three-ammo",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
+ "three-ammo": "github:Hubs-Foundation/three-ammo",
"three-gltf-extensions": "^0.0.14",
"three-mesh-bvh": "^0.3.7",
"three-pathfinding": "^1.1.0",
@@ -15243,6 +15250,7 @@
"troika-three-text": "^0.45.0",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
+ "url": "^0.11.1",
"url-loader": "^4.1.1",
"use-clipboard-copy": "^0.2.0",
"uuid": "^3.2.1",
@@ -17985,8 +17993,8 @@
}
},
"three": {
- "version": "git+ssh://git@github.com/hubs-foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777",
- "from": "three@github:hubs-foundation/three.js#hubs-patches-141"
+ "version": "git+ssh://git@github.com/Hubs-Foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777",
+ "from": "three@github:Hubs-Foundation/three.js#hubs-patches-141"
},
"thunky": {
"version": "1.1.0",
diff --git a/admin/package.json b/admin/package.json
index 5055a08d7f..d3b250a6b1 100644
--- a/admin/package.json
+++ b/admin/package.json
@@ -22,15 +22,15 @@
"dependencies": {
"@iarna/toml": "^2.2.3",
"@mozilla/lilypad-ui": "^1.8.2",
- "aframe": "github:hubs-foundation/aframe#hubs/master",
- "bitecs": "github:hubs-foundation/bitECS#hubs-patches",
+ "aframe": "github:Hubs-Foundation/aframe#hubs/master",
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
"classnames": "^2.2.5",
"hubs": "file:..",
"react": "^16.1.1",
"react-admin": "^2.6.3",
"react-dom": "^16.1.1",
"react-intl": "^2.4.0",
- "three": "github:hubs-foundation/three.js#hubs-patches-141"
+ "three": "github:Hubs-Foundation/three.js#hubs-patches-141"
},
"devDependencies": {
"@babel/core": "^7.18.9",
diff --git a/admin/src/admin.js b/admin/src/admin.js
index 2be64b9e77..289b2b0dd8 100644
--- a/admin/src/admin.js
+++ b/admin/src/admin.js
@@ -36,11 +36,11 @@ import { AutoEndSessionDialog } from "./react-components/auto-end-session-dialog
import registerTelemetry from "hubs/src/telemetry";
import { createMuiTheme, withStyles } from "@material-ui/core/styles";
import { UnauthorizedPage } from "./react-components/unauthorized";
-import { store } from "hubs/src/utils/store-instance";
+import { getStore } from "hubs/src/utils/store-instance";
const qs = new URLSearchParams(location.hash.split("?")[1]);
-window.APP = { store };
+window.APP = { store: getStore() };
registerTelemetry("/admin", "Hubs Admin");
@@ -185,6 +185,7 @@ const mountUI = async (retPhxChannel, customRoutes, layout) => {
let permsTokenRefreshInterval;
+ const store = APP.store;
if (configs.POSTGREST_SERVER) {
dataProvider = postgrestClient(configs.POSTGREST_SERVER);
authProvider = postgrestAuthenticatior.createAuthProvider(retPhxChannel);
@@ -234,6 +235,7 @@ const HiddenAppBar = withStyles({
document.addEventListener("DOMContentLoaded", async () => {
const socket = await connectToReticulum();
+ const store = APP.store;
if (store.state && store.state.credentials && store.state.credentials.token) {
setItaAuthToken(store.state.credentials.token);
try {
diff --git a/admin/src/react-components/service-editor.js b/admin/src/react-components/service-editor.js
index 4ff7a3b19f..24b1e5927c 100644
--- a/admin/src/react-components/service-editor.js
+++ b/admin/src/react-components/service-editor.js
@@ -24,7 +24,7 @@ import LinearProgress from "@material-ui/core/LinearProgress";
import clsx from "classnames";
import { Title } from "react-admin";
import theme from "../utils/sample-theme";
-import { store } from "hubs/src/utils/store-instance";
+import { getStore } from "hubs/src/utils/store-instance";
import withCommonStyles from "../utils/with-common-styles";
import {
getEditableConfig,
@@ -649,6 +649,7 @@ const AppConfigEditor = withStyles(styles)(
class AppConfigEditor extends ConfigurationEditor {
constructor(props) {
super(props);
+ const store = getStore();
if (store.state && store.state.credentials && store.state.credentials.token) {
AppConfigUtils.setAuthToken(store.state.credentials.token);
}
diff --git a/doc/add-ons.md b/doc/add-ons.md
new file mode 100644
index 0000000000..10bcaffc34
--- /dev/null
+++ b/doc/add-ons.md
@@ -0,0 +1,104 @@
+# Hubs client Add-ons
+
+Hubs client add-ons are pluggable libraries that are installed as part of the client and loaded at runtime. Add-ons allow functionality to the Hubs core and allow easy Hubs client extensibility while maintaining the Hubs client core lean and minimal.
+
+## Installation
+
+Installing an addon in your Hubs client is a two step process:
+
+1. You'll need to install the add-on package in your client. You can do that using the `npm install` as with any other npm package.
+ For example if we would want to install the portals add-on we would need to do:
+
+```
+npm i https://github.com/MozillaReality/hubs-portals-addon.git
+```
+
+2. After the add-on is installed you'll need to add it to the addons array inside the `addons.json` file at the root of the Hubs client source.
+
+```
+{
+ "addons": [
+ ...
+ "hubs-portals-addon",
+ ...
+ ]
+}
+```
+
+Now you can build your client and run it as usual.
+
+## Configuration
+
+To configure your add-ons you need to update the add-on configuration JSON in the Admin console. Open your admin console and go to `App Settings -> Features`.
+
+**Important Note**: Add-on require the bitECS based loader so you'll need to enable it for add-ons to work.
+
+Update the add-ons JSON config with your add-ons configuration:
+
+
+
+
+
+There are currently two properties supported in the add-ons configuration:
+
+- **enabled**: Determines if the add-on is enabled by default in the instance rooms.
+- **config**: A free form JSON containing the add-on configuration. This JSON will be passed as is to the add-on during the add-on initialization. See each add-on docs to know what JSON properties the add-on supports if any.
+
+Once your configuration is saved, you can create or open a room in your instance and you should see add-ons running for that room.
+
+### Room add-ons configuration
+
+Add-ons can also be enabled/disabled per room independently from the instance configuration. To override the instance configuration you can go to your room settings and change the add-ons enabled/disabled configuration there. The room add-ons configuration will take precedence over the instance configuration if it has been changed at least once.
+
+
+
+
+
+## Add-on development
+
+You can develop Hubs add-ons using Javascript or Typescript.
+
+If you are developing using Typescript you can use the add-on template as a starting point. You can get the Add-on template from [here](https://github.com/MozillaReality/hubs-template-addon).
+
+The add-on template has the basic dependencies already configured to get started with development as fast as possible.
+
+The easiest way of iterating over an add-on development is by linking the add-on package source from the client.
+
+1. Create a global link for the add-on. Go to the add-ons source folder and do:
+
+ `npm link`
+
+ Other related commands:
+
+ - To see all the global package links:
+
+ `npm ls --link --global`
+
+ - To remove a global linked package:
+
+ `npm unlink -g`
+
+2. Go to the Hubs client root and add a link to the add-on package:
+
+ `npm link [package-id]`
+
+ Other related commands:
+
+ - You can see the currently linked packages with:
+
+ `npm ls --link`
+
+ - To remove a linked package:
+
+ `npm unlink --no-save [package-id]`
+
+Now you can build the client and the add-on should be bundled as part of the client code.
+
+There is a typings library available for Typescript development: [Hubs Client TS Types](https://github.com/MozillaReality/hubs-ts-types)
+
+## Currently available add-ons
+
+- [Template add-on](https://github.com/MozillaReality/hubs-template-addon). This add-on serves as a foundation for add-on development.
+- [Duck add-on](https://github.com/MozillaReality/hubs-duck-addon): Replaces the existing `/duck` chat command and refactors it into a Hubs add-on.
+- [Portals add-on](https://github.com/MozillaReality/hubs-portals-addon): Simple portals implementation as an add-on that lets you spawn portals using the `/portal`. It also show how to add key bindings.
+- [Behavior Graphs add-on](https://github.com/MozillaReality/hubs-behavior-graphs-addon/): Initial Behavior Graphs implementation as an add-on
diff --git a/doc/img/addons-admin-config.png b/doc/img/addons-admin-config.png
new file mode 100644
index 0000000000..5642a73f4a
Binary files /dev/null and b/doc/img/addons-admin-config.png differ
diff --git a/doc/img/addons-room-config.png b/doc/img/addons-room-config.png
new file mode 100644
index 0000000000..949f36b3c2
Binary files /dev/null and b/doc/img/addons-room-config.png differ
diff --git a/package-lock.json b/package-lock.json
index 9fdd0440c2..7889e7bb88 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,11 +24,11 @@
"@mozilla/lilypad-ui": "1.8.6",
"@mozillareality/easing-functions": "^0.1.1",
"@popperjs/core": "^2.4.4",
- "aframe": "github:hubs-foundation/aframe#hubs/master",
+ "aframe": "github:Hubs-Foundation/aframe#hubs/master",
"ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer",
- "ammo.js": "github:hubs-foundation/ammo.js#hubs/master",
- "animejs": "github:hubs-foundation/anime#hubs/master",
- "bitecs": "github:hubs-foundation/bitECS#hubs-patches",
+ "ammo.js": "github:Hubs-Foundation/ammo.js#hubs/master",
+ "animejs": "github:Hubs-Foundation/anime#hubs/master",
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
"buffered-interpolation": "github:Infinitelee/buffered-interpolation",
"classnames": "^2.2.5",
"color": "^3.1.2",
@@ -46,15 +46,19 @@
"history": "^4.7.2",
"hls.js": "^0.14.6",
"html2canvas": "^1.0.0-rc.7",
+ "hubs-behavior-graphs-addon": "github:Hubs-Foundation/hubs-behavior-graphs-addon#main",
+ "hubs-duck-addon": "github:Hubs-Foundation/hubs-duck-addon#main",
+ "hubs-portals-addon": "github:Hubs-Foundation/hubs-portals-addon#main",
+ "hubs-postprocessing-addon": "github:Hubs-Foundation/hubs-postprocessing-addon#main",
"js-cookie": "^2.2.0",
"jsonschema": "^1.2.2",
"jwt-decode": "^2.2.0",
- "lib-hubs": "github:hubs-foundation/lib-hubs#master",
+ "lib-hubs": "github:Hubs-Foundation/lib-hubs#master",
"linkify-it": "^2.0.3",
"markdown-it": "^12.3.2",
"moving-average": "^1.0.0",
- "networked-aframe": "github:hubs-foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
- "nipplejs": "github:hubs-foundation/nipplejs#mr-social-client/master",
+ "networked-aframe": "github:Hubs-Foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
+ "nipplejs": "github:Hubs-Foundation/nipplejs#mr-social-client/master",
"node-ensure": "0.0.0",
"normalize.css": "^8.0.1",
"pdfjs-dist": "^2.14.305",
@@ -79,8 +83,8 @@
"sdp-transform": "^2.14.1",
"semver": "^7.3.2",
"stream-browserify": "^3.0.0",
- "three": "github:hubs-foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
- "three-ammo": "github:hubs-foundation/three-ammo",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
+ "three-ammo": "github:Hubs-Foundation/three-ammo",
"three-gltf-extensions": "^0.0.14",
"three-mesh-bvh": "^0.3.7",
"three-pathfinding": "^1.1.0",
@@ -159,7 +163,7 @@
"sass-loader": "^13.0.2",
"selfsigned": "^2.0.1",
"shelljs": "^0.8.5",
- "spritesheet-js": "github:hubs-foundation/spritesheet.js#hubs/master",
+ "spritesheet-js": "github:Hubs-Foundation/spritesheet.js#hubs/master",
"storybook": "^7.0.20",
"style-loader": "^3.3.1",
"stylelint": "^14.9.1",
@@ -179,6 +183,23 @@
"fsevents": "^2.2.1"
}
},
+ "../../../hubs-postprocessing-addon": {
+ "version": "1.0.0",
+ "extraneous": true,
+ "license": "MPL-2.0",
+ "devDependencies": {
+ "@types/hubs": "github:Hubs-Foundation/hubs-ts-types#d9f56d541f3cf6b8fd360ae96652bf47941f8894",
+ "@types/node": "^18.15.0",
+ "@types/three": "^0.141.0",
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
+ "postprocessing": "~6.28.7",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
+ "ts-loader": "^9.5.1",
+ "typescript": "^5.3.3",
+ "webpack": "^5.91.0",
+ "webpack-cli": "^5.1.4"
+ }
+ },
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@@ -3744,6 +3765,14 @@
"node": ">= 8"
}
},
+ "node_modules/@oveddan-behave-graph/core": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/@oveddan-behave-graph/core/-/core-0.11.1.tgz",
+ "integrity": "sha512-HH6hVYHIjAs4KgrOhg4UoMeXwqiu+DRSU9UI/lQsN4HyGky0cLrzsHMtQxZKLTBl9dGdZIXjlrIBFAepYcMivQ==",
+ "dependencies": {
+ "three-stdlib": "^2.21.5"
+ }
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz",
@@ -9251,6 +9280,11 @@
"integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==",
"dev": true
},
+ "node_modules/@types/draco3d": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw=="
+ },
"node_modules/@types/ejs": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.2.tgz",
@@ -9476,6 +9510,11 @@
"integrity": "sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==",
"dev": true
},
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.3",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="
+ },
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -9598,8 +9637,7 @@
"node_modules/@types/webxr": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz",
- "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==",
- "dev": true
+ "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw=="
},
"node_modules/@types/ws": {
"version": "8.5.4",
@@ -10039,7 +10077,7 @@
},
"node_modules/aframe": {
"version": "1.0.3",
- "resolved": "git+ssh://git@github.com/hubs-foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/aframe.git#0fa14187b8f3e90726385904591eba799380df79",
"license": "MIT",
"dependencies": {
"custom-event-polyfill": "^1.0.6",
@@ -10167,11 +10205,11 @@
},
"node_modules/ammo.js": {
"version": "0.0.2",
- "resolved": "git+ssh://git@github.com/hubs-foundation/ammo.js.git#a38109e87e300c820da14c9615be379caf03d77a"
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/ammo.js.git#a38109e87e300c820da14c9615be379caf03d77a"
},
"node_modules/animejs": {
"version": "3.0.1",
- "resolved": "git+ssh://git@github.com/hubs-foundation/anime.git#883d028420c7eb8c2934290e5d10827be4640318",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/anime.git#883d028420c7eb8c2934290e5d10827be4640318",
"license": "MIT"
},
"node_modules/ansi-align": {
@@ -11069,7 +11107,7 @@
},
"node_modules/bitecs": {
"version": "0.3.38",
- "resolved": "git+ssh://git@github.com/hubs-foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/bitECS.git#913b4ae261684eee205251e41a3250d1c1a2817e",
"license": "MPL-2.0"
},
"node_modules/bl": {
@@ -12615,40 +12653,6 @@
"postcss": "^8.1.0"
}
},
- "node_modules/css-loader/node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true
- },
- "node_modules/css-loader/node_modules/postcss": {
- "version": "8.4.23",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
- "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "nanoid": "^3.3.6",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
"node_modules/css-loader/node_modules/postcss-modules-extract-imports": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
@@ -13399,6 +13403,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "node_modules/draco3d": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="
+ },
"node_modules/draft-js": {
"version": "0.11.7",
"resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz",
@@ -14787,6 +14796,11 @@
"integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==",
"dev": true
},
+ "node_modules/fflate": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="
+ },
"node_modules/figures": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/figures/-/figures-4.0.1.tgz",
@@ -16460,6 +16474,53 @@
"node": ">= 6"
}
},
+ "node_modules/hubs-behavior-graphs-addon": {
+ "version": "0.0.1",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/hubs-behavior-graphs-addon.git#main",
+ "integrity": "sha512-ZNQJzTSuJihiGAONdi8KAFoVi8qZxkG4RLAwS3IUJi2sP6N0qTBCtItBr37TygN3AgLP4KY32sFkP3jc9MMAkA==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "@oveddan-behave-graph/core": "^0.11.1"
+ },
+ "peerDependencies": {
+ "aframe": "github:Hubs-Foundation/aframe#hubs/master",
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
+ "react": "^18.2.0",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
+ "troika-three-text": "^0.45.0"
+ }
+ },
+ "node_modules/hubs-duck-addon": {
+ "version": "1.0.0",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/hubs-duck-addon.git#main",
+ "integrity": "sha512-1mAnkCPudr+jG9w4qG7zKPl7mA5PvhQ7/IZ1bUMt0SCDz6n7ZR7hOvZ0ewQtqXz4NLSU+RGgw2BlVFo3JLQTWg==",
+ "license": "MPL-2.0",
+ "peerDependencies": {
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777"
+ }
+ },
+ "node_modules/hubs-portals-addon": {
+ "version": "1.0.1",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/hubs-portals-addon.git#main",
+ "integrity": "sha512-Pwse1rxVOw+uw1+uS2lNXak4c7b1yA6ucDFhD/d4EiX5lnWPSOkSq+Ljs6Cv02bhFO5eWvOrNKlo6REZLXU1LQ==",
+ "license": "MPL-2.0",
+ "peerDependencies": {
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777"
+ }
+ },
+ "node_modules/hubs-postprocessing-addon": {
+ "version": "1.0.2",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/hubs-postprocessing-addon.git#main",
+ "integrity": "sha512-4qBFYY6eW0nLKH8NLxefSORbRcGdHrei6d3BQYknd4o6Nnelu74CfGaKil8k2Y8fLBq4TBqK9ZD85lpsTEc/iA==",
+ "license": "MPL-2.0",
+ "peerDependencies": {
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
+ "postprocessing": "~6.28.7",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777"
+ }
+ },
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -18143,12 +18204,6 @@
"shell-quote": "^1.7.3"
}
},
- "node_modules/launch-editor/node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true
- },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -18173,7 +18228,7 @@
},
"node_modules/lib-hubs": {
"version": "0.0.1",
- "resolved": "git+ssh://git@github.com/hubs-foundation/lib-hubs.git#9430db2a06cc63c6f91319c47393fea1dca42d43",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/lib-hubs.git#9430db2a06cc63c6f91319c47393fea1dca42d43",
"workspaces": [
"packages/*"
]
@@ -19152,9 +19207,9 @@
"dev": true
},
"node_modules/nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
@@ -19192,7 +19247,7 @@
},
"node_modules/networked-aframe": {
"version": "0.6.1",
- "resolved": "git+ssh://git@github.com/hubs-foundation/networked-aframe.git#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/networked-aframe.git#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
"integrity": "sha512-T8wzMgtLYuFqWRx2cTxadNzDECir2x2gdJlV4TQTAwpZALo0PKyPie5VX3rjEIL9l4/5N3yK+dCtRW70NuUfhg==",
"license": "MIT",
"dependencies": {
@@ -19214,7 +19269,7 @@
},
"node_modules/nipplejs": {
"version": "0.6.8",
- "resolved": "git+ssh://git@github.com/hubs-foundation/nipplejs.git#7b5f953f75df28d42689e96c6a8342ab0a3cb595",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/nipplejs.git#7b5f953f75df28d42689e96c6a8342ab0a3cb595",
"license": "MIT"
},
"node_modules/no-case": {
@@ -20117,6 +20172,12 @@
"websocket": "^1.0.24"
}
},
+ "node_modules/picocolors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+ "devOptional": true
+ },
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -20284,6 +20345,34 @@
"node": ">=10"
}
},
+ "node_modules/postcss": {
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/postcss-media-query-parser": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
@@ -20316,16 +20405,21 @@
"dev": true
},
"node_modules/postprocessing": {
- "version": "6.31.0",
- "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.31.0.tgz",
- "integrity": "sha512-h1g2KDVrTS6QB4AHP55opp8FYzq66jJHh4JIFCptaj283RUX1y/tPkv8FBB2oK4WYrdPgqvElnKrXZwgiLWeHQ==",
+ "version": "6.28.7",
+ "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.28.7.tgz",
+ "integrity": "sha512-norsBlNIhDoNG2R0nki9C0bNEARIunE/s5W/4qH/Ozlg3m0dOOmBoY8JZhnmPild/+Jvq/UFEeHQRBvTc11WUg==",
"engines": {
"node": ">= 0.13.2"
},
"peerDependencies": {
- "three": ">= 0.138.0 < 0.153.0"
+ "three": ">= 0.125.0 < 0.145.0"
}
},
+ "node_modules/potpack": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="
+ },
"node_modules/prefix-style": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz",
@@ -22446,9 +22540,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -22544,7 +22638,7 @@
},
"node_modules/spritesheet-js": {
"version": "1.2.6",
- "resolved": "git+ssh://git@github.com/hubs-foundation/spritesheet.js.git#edce7f45d9f1f5997850fba0d0034a44fceda8c5",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/spritesheet.js.git#edce7f45d9f1f5997850fba0d0034a44fceda8c5",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -23026,42 +23120,6 @@
"stylelint": "^14.4.0"
}
},
- "node_modules/stylelint-config-recommended-scss/node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true,
- "peer": true
- },
- "node_modules/stylelint-config-recommended-scss/node_modules/postcss": {
- "version": "8.4.23",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
- "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "peer": true,
- "dependencies": {
- "nanoid": "^3.3.6",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
"node_modules/stylelint-config-recommended-scss/node_modules/postcss-scss": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz",
@@ -23249,40 +23307,6 @@
"node": ">=10"
}
},
- "node_modules/stylelint/node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true
- },
- "node_modules/stylelint/node_modules/postcss": {
- "version": "8.4.23",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
- "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "nanoid": "^3.3.6",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
"node_modules/stylelint/node_modules/postcss-safe-parser": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
@@ -23480,12 +23504,6 @@
"node": ">= 10"
}
},
- "node_modules/svgo/node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true
- },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -23947,13 +23965,13 @@
},
"node_modules/three": {
"version": "0.141.0",
- "resolved": "git+ssh://git@github.com/hubs-foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/three.js.git#65b5105908f5f135cad25fed07e25f15f3876777",
"integrity": "sha512-oqsJ2XGLH1ki2w/pjyt5nmmpQVw71s1Quo9CK4U3eYVQfe/a12tYKy8rcgUC8BU58eO8lP4ng1B50s4O9p5q6A==",
"license": "MIT"
},
"node_modules/three-ammo": {
"version": "1.0.12",
- "resolved": "git+ssh://git@github.com/hubs-foundation/three-ammo.git#26d445f13a63690271cdf385157ac91e269a744e",
+ "resolved": "git+ssh://git@github.com/Hubs-Foundation/three-ammo.git#26d445f13a63690271cdf385157ac91e269a744e",
"license": "MPL-2.0",
"peerDependencies": {
"ammo.js": "*",
@@ -23981,6 +23999,22 @@
"three": "0.x.x"
}
},
+ "node_modules/three-stdlib": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.30.0.tgz",
+ "integrity": "sha512-ALL7rn57jq/MovDRk5hGjeWCvOeZlZhFCWIdpbBAQNudCO3nMwxEba5ZulsMgiI1ymQMzUzTMcxhLTCVlUaKDw==",
+ "dependencies": {
+ "@types/draco3d": "^1.4.0",
+ "@types/offscreencanvas": "^2019.6.4",
+ "@types/webxr": "^0.5.2",
+ "draco3d": "^1.4.1",
+ "fflate": "^0.6.9",
+ "potpack": "^1.0.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.128.0"
+ }
+ },
"node_modules/three-to-ammo": {
"version": "1.0.1",
"resolved": "git+ssh://git@github.com/infinitelee/three-to-ammo.git#2364f7ce5e4b6b622f29a8a4f8b467b83aa36293",
@@ -24664,12 +24698,6 @@
"browserslist": ">= 4.21.0"
}
},
- "node_modules/update-browserslist-db/node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "devOptional": true
- },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/package.json b/package.json
index 829ad83314..1bb0c3260a 100644
--- a/package.json
+++ b/package.json
@@ -80,11 +80,11 @@
"@mozilla/lilypad-ui": "1.8.6",
"@mozillareality/easing-functions": "^0.1.1",
"@popperjs/core": "^2.4.4",
- "aframe": "github:hubs-foundation/aframe#hubs/master",
+ "aframe": "github:Hubs-Foundation/aframe#hubs/master",
"ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer",
- "ammo.js": "github:hubs-foundation/ammo.js#hubs/master",
- "animejs": "github:hubs-foundation/anime#hubs/master",
- "bitecs": "github:hubs-foundation/bitECS#hubs-patches",
+ "ammo.js": "github:Hubs-Foundation/ammo.js#hubs/master",
+ "animejs": "github:Hubs-Foundation/anime#hubs/master",
+ "bitecs": "github:Hubs-Foundation/bitECS#hubs-patches",
"buffered-interpolation": "github:Infinitelee/buffered-interpolation",
"classnames": "^2.2.5",
"color": "^3.1.2",
@@ -102,15 +102,19 @@
"history": "^4.7.2",
"hls.js": "^0.14.6",
"html2canvas": "^1.0.0-rc.7",
+ "hubs-behavior-graphs-addon": "github:Hubs-Foundation/hubs-behavior-graphs-addon#main",
+ "hubs-duck-addon": "github:Hubs-Foundation/hubs-duck-addon#main",
+ "hubs-portals-addon": "github:Hubs-Foundation/hubs-portals-addon#main",
+ "hubs-postprocessing-addon": "github:Hubs-Foundation/hubs-postprocessing-addon#main",
"js-cookie": "^2.2.0",
"jsonschema": "^1.2.2",
"jwt-decode": "^2.2.0",
- "lib-hubs": "github:hubs-foundation/lib-hubs#master",
+ "lib-hubs": "github:Hubs-Foundation/lib-hubs#master",
"linkify-it": "^2.0.3",
"markdown-it": "^12.3.2",
"moving-average": "^1.0.0",
- "networked-aframe": "github:hubs-foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
- "nipplejs": "github:hubs-foundation/nipplejs#mr-social-client/master",
+ "networked-aframe": "github:Hubs-Foundation/networked-aframe#6093c3a0b2867a9e141cd5c19f7d13dfa7c38479",
+ "nipplejs": "github:Hubs-Foundation/nipplejs#mr-social-client/master",
"node-ensure": "0.0.0",
"normalize.css": "^8.0.1",
"pdfjs-dist": "^2.14.305",
@@ -135,8 +139,8 @@
"sdp-transform": "^2.14.1",
"semver": "^7.3.2",
"stream-browserify": "^3.0.0",
- "three": "github:hubs-foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
- "three-ammo": "github:hubs-foundation/three-ammo",
+ "three": "github:Hubs-Foundation/three.js#65b5105908f5f135cad25fed07e25f15f3876777",
+ "three-ammo": "github:Hubs-Foundation/three-ammo",
"three-gltf-extensions": "^0.0.14",
"three-mesh-bvh": "^0.3.7",
"three-pathfinding": "^1.1.0",
@@ -215,7 +219,7 @@
"sass-loader": "^13.0.2",
"selfsigned": "^2.0.1",
"shelljs": "^0.8.5",
- "spritesheet-js": "github:hubs-foundation/spritesheet.js#hubs/master",
+ "spritesheet-js": "github:Hubs-Foundation/spritesheet.js#hubs/master",
"storybook": "^7.0.20",
"style-loader": "^3.3.1",
"stylelint": "^14.9.1",
diff --git a/src/addons.ts b/src/addons.ts
new file mode 100644
index 0000000000..85d1eaa3ab
--- /dev/null
+++ b/src/addons.ts
@@ -0,0 +1,381 @@
+import { GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader";
+import { App, HubsWorld } from "./app";
+import { prefabs } from "./prefabs/prefabs";
+
+import {
+ InflatorConfigT,
+ SystemConfigT,
+ SystemOrderE,
+ PrefabConfigT,
+ NetworkSchemaConfigT,
+ ChatCommandConfigT,
+ PreferenceConfigT,
+ PreferencePrefsScreenItemT,
+ PreferencePrefsScreenCategory,
+ PreferenceScreenLabelT,
+ PreferenceScreenDefT,
+ PreferenceDefConfigT,
+ PostProcessOrderE
+} from "./types";
+import configs from "./utils/configs";
+import { commonInflators, gltfInflators, jsxInflators } from "./utils/jsx-entity";
+import { networkableComponents, schemas } from "./utils/network-schemas";
+import { gltfPluginsExtra } from "./components/gltf-model-plus";
+import { GLTFLinkResolverFn, gltfLinkResolvers } from "./inflators/model";
+import { Object3D } from "three";
+import { extraSections } from "./react-components/debug-panel/ECSSidebar";
+import { shouldUseNewLoader } from "./hubs";
+import { SCHEMA } from "./storage/store";
+import { Pass } from "postprocessing";
+
+function getNextIdx(slot: Array, system: SystemConfigT) {
+ return slot.findIndex(item => {
+ item.order > system.order;
+ });
+}
+
+function registerSystem(system: SystemConfigT) {
+ let slot = APP.addon_systems.prePhysics;
+ if (system.order < SystemOrderE.PrePhysics) {
+ slot = APP.addon_systems.setup;
+ } else if (system.order < SystemOrderE.PostPhysics) {
+ slot = APP.addon_systems.prePhysics;
+ } else if (system.order < SystemOrderE.BeforeMatricesUpdate) {
+ slot = APP.addon_systems.postPhysics;
+ } else if (system.order < SystemOrderE.BeforeRender) {
+ slot = APP.addon_systems.postPhysics;
+ } else if (system.order < SystemOrderE.AfterRender) {
+ slot = APP.addon_systems.beforeRender;
+ } else {
+ slot = APP.addon_systems.afterRender;
+ }
+ const nextIdx = getNextIdx(slot, system);
+ slot.splice(nextIdx, 0, system);
+}
+
+function registerInflator(inflator: InflatorConfigT) {
+ if (inflator.common) {
+ commonInflators[inflator.common.id] = inflator.common.inflator;
+ } else {
+ if (inflator.jsx) {
+ jsxInflators[inflator.jsx.id] = inflator.jsx.inflator;
+ }
+ if (inflator.gltf) {
+ gltfInflators[inflator.gltf.id] = inflator.gltf.inflator;
+ }
+ }
+}
+
+function registerPrefab(prefab: PrefabConfigT) {
+ if (prefabs.has(prefab.id)) {
+ throw Error(`Error registering prefab ${name}: prefab already registered`);
+ }
+ prefabs.set(prefab.id, prefab.config);
+}
+
+function registerNetworkSchema(schemaConfig: NetworkSchemaConfigT) {
+ if (schemas.has(schemaConfig.component)) {
+ throw Error(
+ `Error registering network schema ${schemaConfig.schema.componentName}: network schema already registered`
+ );
+ }
+ schemas.set(schemaConfig.component, schemaConfig.schema);
+ networkableComponents.push(schemaConfig.component);
+}
+
+function registerChatCommand(command: ChatCommandConfigT) {
+ APP.messageDispatch.registerChatCommand(command.id, command.command);
+}
+
+export type AddonIdT = string;
+export type AddonNameT = string;
+export type AddonDescriptionT = string;
+export type AddonOnLoadedFn = () => void;
+export type AddonOnReadyFn = (app: App, config?: JSON) => void;
+
+export interface InternalAddonConfigT {
+ name: AddonNameT;
+ description?: AddonDescriptionT;
+ onLoaded?: AddonOnLoadedFn;
+ onReady?: AddonOnReadyFn;
+ system?: SystemConfigT | SystemConfigT[];
+ inflator?: InflatorConfigT | InflatorConfigT[];
+ prefab?: PrefabConfigT | PrefabConfigT[];
+ networkSchema?: NetworkSchemaConfigT | NetworkSchemaConfigT[];
+ chatCommand?: ChatCommandConfigT | ChatCommandConfigT[];
+ preference?: PreferenceConfigT | PreferenceConfigT[];
+ enabled?: boolean;
+ config?: JSON | undefined;
+}
+type AddonConfigT = Omit;
+export type AdminAddonConfig = {
+ enabled: boolean;
+ config: JSON;
+};
+
+const pendingAddons = new Map();
+export const addons = new Map();
+export type AddonRegisterCallbackT = (app: App) => void;
+export function registerAddon(id: AddonIdT, config: AddonConfigT) {
+ console.log(`Add-on ${id} registered`);
+ pendingAddons.set(id, config);
+ registerPreferences(id, config);
+ if (config.onLoaded) {
+ config.onLoaded();
+ }
+}
+
+export type GLTFParserCallbackFn = (parser: GLTFParser) => GLTFLoaderPlugin;
+export function registerGLTFLoaderPlugin(callback: GLTFParserCallbackFn): void {
+ gltfPluginsExtra.push(callback);
+}
+export function registerGLTFLinkResolver(resolver: GLTFLinkResolverFn): void {
+ gltfLinkResolvers.push(resolver);
+}
+export function registerECSSidebarSection(section: (world: HubsWorld, selectedObj: Object3D) => React.JSX.Element) {
+ extraSections.push(section);
+}
+
+const screenPreferencesDefs = new Map();
+export function getAddonsPreferencesDefs(): PreferenceScreenDefT {
+ return screenPreferencesDefs;
+}
+
+const screenPreferencesLabels = new Map();
+export function getAddonsPreferencesLabels(): PreferenceScreenLabelT {
+ return screenPreferencesLabels;
+}
+
+let xFormedScreenPreferencesCategories: Map;
+const screenPreferencesCategories = new Map();
+export function getAddonsPreferencesCategories(app: App): PreferencePrefsScreenCategory {
+ // We need to transform on the spot as when the preferences are added we don't yet have the hub user_data
+ // to know what addons are enabled in the room
+ if (!xFormedScreenPreferencesCategories) {
+ xFormedScreenPreferencesCategories = new Map();
+ screenPreferencesCategories.forEach((categories, addonId) => {
+ if (isAddonActive(app, addonId)) {
+ const config = addons.get(addonId);
+ xFormedScreenPreferencesCategories.set(config?.name || addonId, categories);
+ }
+ });
+ return xFormedScreenPreferencesCategories;
+ } else {
+ return xFormedScreenPreferencesCategories;
+ }
+}
+
+function registerPreferences(addonId: string, addonConfig: AddonConfigT) {
+ const prefSchema = SCHEMA.definitions.preferences.properties;
+ function register(preference: PreferenceConfigT) {
+ for (const key in preference) {
+ if (!(key in prefSchema)) {
+ const prefDef = preference[key].prefDefinition;
+ if (key in prefSchema) {
+ throw new Error(`Preference ${key} already exists`);
+ }
+ (prefSchema as any)[key] = prefDef;
+ screenPreferencesDefs.set(key, prefDef);
+
+ const prefConfig = preference[key];
+ let categoryPrefs: PreferencePrefsScreenItemT[];
+ if (screenPreferencesCategories.has(addonId)) {
+ categoryPrefs = screenPreferencesCategories.get(addonId)!;
+ } else {
+ categoryPrefs = new Array();
+ screenPreferencesCategories.set(addonId, categoryPrefs);
+ }
+ categoryPrefs.push({ key, ...prefConfig.prefConfig });
+ screenPreferencesLabels.set(key, prefConfig.prefConfig.description);
+ } else {
+ throw new Error("Preference already exists");
+ }
+ }
+ }
+
+ if (addonConfig.preference) {
+ if (Array.isArray(addonConfig.preference)) {
+ addonConfig.preference.forEach(preference => {
+ register(preference);
+ });
+ } else {
+ register(addonConfig.preference);
+ }
+ }
+}
+
+const fxOrder2Passes = {
+ [PostProcessOrderE.AfterScene]: 0,
+ [PostProcessOrderE.AfterBloom]: 0,
+ [PostProcessOrderE.AfterUI]: 0,
+ [PostProcessOrderE.AfterAA]: 0
+};
+const fx2Order = new Map();
+function afterSceneIdx() {
+ return 2 + fxOrder2Passes[PostProcessOrderE.AfterScene];
+}
+function afterBloomIdx(app: App) {
+ let idx = afterSceneIdx();
+ idx += fxOrder2Passes[PostProcessOrderE.AfterBloom];
+ if (app.fx.bloomAndTonemapPass) {
+ idx++;
+ }
+ return idx;
+}
+function afterUIIdx(app: App) {
+ let idx = afterBloomIdx(app);
+ idx += fxOrder2Passes[PostProcessOrderE.AfterUI];
+ return idx;
+}
+function afterAAIdx(app: App) {
+ let idx = afterUIIdx(app);
+ idx += fxOrder2Passes[PostProcessOrderE.AfterAA];
+ return idx;
+}
+function getPassIdx(app: App, order: PostProcessOrderE) {
+ switch (order) {
+ case PostProcessOrderE.AfterScene:
+ return afterSceneIdx();
+ case PostProcessOrderE.AfterBloom:
+ return afterBloomIdx(app);
+ case PostProcessOrderE.AfterUI:
+ return afterUIIdx(app);
+ case PostProcessOrderE.AfterAA:
+ return afterAAIdx(app);
+ }
+}
+export function registerPass(app: App, pass: Pass | Pass[], order: PostProcessOrderE) {
+ function register(pass: Pass) {
+ const nextIdx = getPassIdx(app, order) + 1;
+ fx2Order.set(pass, order);
+ fxOrder2Passes[order]++;
+ app.fx.composer?.addPass(pass, nextIdx);
+ }
+
+ if (Array.isArray(pass)) {
+ pass.every(pass => register(pass));
+ } else {
+ register(pass);
+ }
+}
+
+export function unregisterPass(app: App, pass: Pass | Pass[]) {
+ function unregister(pass: Pass) {
+ if (fx2Order.has(pass)) {
+ const order = fx2Order.get(pass)!;
+ fxOrder2Passes[order]--;
+ app.fx.composer?.removePass(pass);
+ fx2Order.delete(pass);
+ }
+ }
+
+ if (Array.isArray(pass)) {
+ pass.every(pass => unregister(pass));
+ } else {
+ unregister(pass);
+ }
+}
+
+export function getAddonConfig(id: string): AdminAddonConfig {
+ const adminAddonsConfig = configs.feature("addons_config");
+ let adminAddonConfig = {
+ enabled: false,
+ config: {} as JSON
+ };
+ if (adminAddonsConfig && id in adminAddonsConfig) {
+ adminAddonConfig = adminAddonsConfig[id];
+ }
+ return adminAddonConfig;
+}
+
+export function isAddonEnabled(app: App, id: string): boolean {
+ let enabled = false;
+ if (app.hub?.user_data && "addons" in app.hub?.user_data && id in app.hub.user_data["addons"]) {
+ enabled = app.hub.user_data.addons[id];
+ } else {
+ const adminAddonsConfig = getAddonConfig(id);
+ if (adminAddonsConfig) {
+ enabled = adminAddonsConfig.enabled;
+ }
+ }
+ return enabled;
+}
+
+function isAddonActive(app: App, id: string): boolean {
+ if (shouldUseNewLoader()) {
+ return isAddonEnabled(app, id);
+ }
+ return false;
+}
+
+export function onAddonsInit(app: App) {
+ app.scene?.addEventListener("hub_updated", () => {
+ for (const [id, addon] of pendingAddons) {
+ if (addons.has(id)) {
+ throw Error(`Addon ${id} already registered`);
+ } else {
+ addons.set(id, addon);
+ }
+
+ if (!isAddonActive(app, id)) {
+ continue;
+ }
+
+ if (addon.prefab) {
+ if (Array.isArray(addon.prefab)) {
+ addon.prefab.forEach(prefab => {
+ registerPrefab(prefab);
+ });
+ } else {
+ registerPrefab(addon.prefab);
+ }
+ }
+
+ if (addon.networkSchema) {
+ if (Array.isArray(addon.networkSchema)) {
+ addon.networkSchema.forEach(networkSchema => {
+ registerNetworkSchema(networkSchema);
+ });
+ } else {
+ registerNetworkSchema(addon.networkSchema);
+ }
+ }
+
+ if (addon.inflator) {
+ if (Array.isArray(addon.inflator)) {
+ addon.inflator.forEach(inflator => {
+ registerInflator(inflator);
+ });
+ } else {
+ registerInflator(addon.inflator);
+ }
+ }
+
+ if (addon.system) {
+ if (Array.isArray(addon.system)) {
+ addon.system.forEach(system => {
+ registerSystem(system);
+ });
+ } else {
+ registerSystem(addon.system);
+ }
+ }
+
+ if (addon.chatCommand) {
+ if (Array.isArray(addon.chatCommand)) {
+ addon.chatCommand.forEach(chatCommand => {
+ registerChatCommand(chatCommand);
+ });
+ } else {
+ registerChatCommand(addon.chatCommand);
+ }
+ }
+
+ if (addon.onReady) {
+ const adminAddonConfig = getAddonConfig(id);
+ addon.onReady(app, adminAddonConfig.config);
+ }
+ }
+ pendingAddons.clear();
+ });
+}
diff --git a/src/app.ts b/src/app.ts
index 16a03c475c..f238dcb685 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -2,7 +2,6 @@ import { addEntity, createWorld, IWorld } from "bitecs";
import "./aframe-to-bit-components";
import { AEntity, Networked, Object3DTag, Owned } from "./bit-components";
import MediaSearchStore from "./storage/media-search-store";
-import Store from "./storage/store";
import qsTruthy from "./utils/qs_truthy";
import type { AComponent, AScene } from "aframe";
@@ -19,6 +18,7 @@ import {
PositionalAudio,
Scene,
sRGBEncoding,
+ Texture,
WebGLRenderer
} from "three";
import { AudioSettings, SourceType } from "./components/audio-params";
@@ -27,9 +27,11 @@ import { DialogAdapter } from "./naf-dialog-adapter";
import { mainTick } from "./systems/hubs-systems";
import { waitForPreloads } from "./utils/preload";
import SceneEntryManager from "./scene-entry-manager";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
import { addObject3DComponent } from "./utils/jsx-entity";
import { ElOrEid } from "./utils/bit-utils";
+import { onAddonsInit } from "./addons";
+import { CoreSystemKeyT, HubsSystemKeyT, SystemConfigT, SystemKeyT, SystemT } from "./types";
declare global {
interface Window {
@@ -52,6 +54,7 @@ export interface HubsWorld extends IWorld {
nid2eid: Map;
eid2obj: Map;
eid2mat: Map;
+ eid2tex: Map;
time: { delta: number; elapsed: number; tick: number };
}
@@ -63,7 +66,7 @@ export function getScene() {
return promiseToScene;
}
-interface HubDescription {
+export interface HubDescription {
hub_id: string;
user_data?: any;
}
@@ -75,7 +78,6 @@ export class App {
mediaDevicesManager?: MediaDevicesManager;
entryManager?: SceneEntryManager;
messageDispatch?: any;
- store: Store;
componentRegistry: { [key: string]: AComponent[] };
mediaSearchStore = new MediaSearchStore();
@@ -104,6 +106,15 @@ export class App {
dialog = new DialogAdapter();
+ addon_systems = {
+ setup: new Array<{ order: number; system: SystemT }>(),
+ prePhysics: new Array<{ order: number; system: SystemT }>(),
+ postPhysics: new Array<{ order: number; system: SystemT }>(),
+ beforeMatricesUpdate: new Array<{ order: number; system: SystemT }>(),
+ beforeRender: new Array<{ order: number; system: SystemT }>(),
+ afterRender: new Array<{ order: number; system: SystemT }>()
+ };
+
RENDER_ORDER = {
HUD_BACKGROUND: 1,
HUD_ICONS: 2,
@@ -117,10 +128,10 @@ export class App {
} = {};
constructor() {
- this.store = store;
// TODO: Create accessor / update methods for these maps / set
this.world.eid2obj = new Map();
this.world.eid2mat = new Map();
+ this.world.eid2tex = new Map();
this.world.nid2eid = new Map();
this.world.deletedNids = new Set();
@@ -159,6 +170,23 @@ export class App {
return this.sid2str.get(sid);
}
+ notifyOnInit() {
+ onAddonsInit(this);
+ }
+
+ get store() {
+ return getStore();
+ }
+
+ getSystem(id: SystemKeyT) {
+ const systems = this.scene?.systems!;
+ if (id in systems) {
+ return systems[id as CoreSystemKeyT];
+ } else {
+ return systems["hubs-systems"][id as HubsSystemKeyT];
+ }
+ }
+
// This gets called by a-scene to setup the renderer, camera, and audio listener
// TODO ideally the contorl flow here would be inverted, and we would setup this stuff,
// initialize aframe, and then run our own RAF loop
diff --git a/src/bit-components.js b/src/bit-components.js
index 1bafeafa25..09d6bb53d8 100644
--- a/src/bit-components.js
+++ b/src/bit-components.js
@@ -21,7 +21,9 @@ export const Owned = defineComponent();
export const EntityStateDirty = defineComponent();
export const NetworkedMediaFrame = defineComponent({
capturedNid: Types.ui32,
- scale: [Types.f32, 3]
+ scale: [Types.f32, 3],
+ flags: Types.ui8,
+ mediaType: Types.ui8
});
NetworkedMediaFrame.capturedNid[$isStringType] = true;
@@ -36,6 +38,43 @@ export const MediaFrame = defineComponent({
previewingNid: Types.eid,
flags: Types.ui8
});
+export const MediaRoot = defineComponent();
+export const NetworkedText = defineComponent({
+ text: Types.ui8,
+ anchorX: Types.ui8,
+ anchorY: Types.ui8,
+ color: Types.ui32,
+ curveRadius: Types.f32,
+ direction: Types.ui8,
+ fillOpacity: Types.f32,
+ fontUrl: Types.ui8,
+ fontSize: Types.f32,
+ letterSpacing: Types.f32,
+ lineHeight: Types.ui8,
+ textAlign: Types.ui8,
+ outlineWidth: Types.ui8,
+ outlineColor: Types.ui32,
+ outlineBlur: Types.ui8,
+ outlineOffsetX: Types.ui8,
+ outlineOffsetY: Types.ui8,
+ outlineOpacity: Types.f32,
+ strokeWidth: Types.ui8,
+ strokeColor: Types.ui32,
+ strokeOpacity: Types.ui32,
+ textIndent: Types.ui32,
+ whiteSpace: Types.ui8,
+ overflowWrap: Types.ui8,
+ opacity: Types.f32,
+ side: Types.ui8,
+ maxWidth: Types.f32
+});
+NetworkedText.text[$isStringType] = true;
+NetworkedText.lineHeight[$isStringType] = true;
+NetworkedText.outlineWidth[$isStringType] = true;
+NetworkedText.outlineBlur[$isStringType] = true;
+NetworkedText.outlineOffsetX[$isStringType] = true;
+NetworkedText.outlineOffsetY[$isStringType] = true;
+NetworkedText.strokeWidth[$isStringType] = true;
export const TextTag = defineComponent();
export const ReflectionProbe = defineComponent();
export const Slice9 = defineComponent({
@@ -59,7 +98,9 @@ export const SpotLightTag = defineComponent();
export const CursorRaycastable = defineComponent();
export const RemoteHoverTarget = defineComponent();
export const NotRemoteHoverTarget = defineComponent();
-export const Holdable = defineComponent();
+export const Holdable = defineComponent({
+ flags: Types.ui8
+});
export const RemoveNetworkedEntityButton = defineComponent();
export const Interacted = defineComponent();
export const HandRight = defineComponent();
@@ -119,7 +160,11 @@ export const Rigidbody = defineComponent({
activationState: Types.ui8,
collisionFilterGroup: Types.ui32,
collisionFilterMask: Types.ui32,
- flags: Types.ui8
+ flags: Types.ui8,
+ initialCollisionFilterMask: Types.ui32
+});
+export const NetworkedRigidBody = defineComponent({
+ prevType: Types.ui8
});
export const PhysicsShape = defineComponent({
bodyId: Types.ui16,
@@ -270,9 +315,12 @@ export const LoopAnimation = defineComponent();
*/
export const LoopAnimationData = new Map();
export const NetworkedVideo = defineComponent({
+ src: Types.ui8,
time: Types.f32,
- flags: Types.ui8
+ flags: Types.ui8,
+ projection: Types.ui8
});
+NetworkedVideo.src[$isStringType] = true;
export const VideoMenuItem = defineComponent();
export const VideoMenu = defineComponent({
videoRef: Types.eid,
@@ -387,6 +435,7 @@ export const Billboard = defineComponent({
onlyY: Types.ui8
});
export const MaterialTag = defineComponent();
+export const TextureTag = defineComponent();
export const UVScroll = defineComponent({
speed: [Types.f32, 2],
increment: [Types.f32, 2],
@@ -441,7 +490,6 @@ export const LinearScale = defineComponent({
targetY: Types.f32,
targetZ: Types.f32
});
-export const Quack = defineComponent();
export const TrimeshTag = defineComponent();
export const HeightFieldTag = defineComponent();
export const LocalAvatar = defineComponent();
diff --git a/src/bit-systems/audio-emitter-system.ts b/src/bit-systems/audio-emitter-system.ts
index a525fc3e02..43f8cd7dd3 100644
--- a/src/bit-systems/audio-emitter-system.ts
+++ b/src/bit-systems/audio-emitter-system.ts
@@ -6,6 +6,7 @@ import { AudioType, SourceType } from "../components/audio-params";
import { AudioSystem } from "../systems/audio-system";
import { applySettings, getCurrentAudioSettings, updateAudioSettings } from "../update-audio-settings";
import { addObject3DComponent, swapObject3DComponent } from "../utils/jsx-entity";
+import { EntityID } from "../utils/networking-types";
export type AudioObject3D = StereoAudio | PositionalAudio;
type AudioConstructor = new (listener: ThreeAudioListener) => T;
@@ -55,6 +56,13 @@ function swapAudioType(
swapObject3DComponent(world, eid, newAudio);
}
+export function swapAudioSrc(world: HubsWorld, videoEid: EntityID, audioEid: EntityID) {
+ const audio = world.eid2obj.get(audioEid)! as AudioObject3D;
+ const video = MediaVideoData.get(videoEid)!;
+ audio.setMediaElementSource(video);
+ video.volume = 1;
+}
+
export function makeAudioEntity(world: HubsWorld, source: number, sourceType: SourceType, audioSystem: AudioSystem) {
const eid = addEntity(world);
APP.sourceType.set(eid, sourceType);
diff --git a/src/bit-systems/camera-tool.js b/src/bit-systems/camera-tool.js
index 0ce7cf8411..f403300cf4 100644
--- a/src/bit-systems/camera-tool.js
+++ b/src/bit-systems/camera-tool.js
@@ -211,7 +211,7 @@ function rotateWithRightClick(world, camera) {
userinput.get(paths.device.mouse.buttonRight)
) {
const rightCursor = anyEntityWith(world, RemoteRight);
- physicsSystem.updateRigidBodyOptions(camera, { type: "kinematic" });
+ physicsSystem.updateRigidBody(camera, { type: "kinematic" });
transformSystem.startTransform(world.eid2obj.get(camera), world.eid2obj.get(rightCursor), {
mode: "cursor"
});
diff --git a/src/bit-systems/interactable-system.ts b/src/bit-systems/interactable-system.ts
new file mode 100644
index 0000000000..d7aa7e3442
--- /dev/null
+++ b/src/bit-systems/interactable-system.ts
@@ -0,0 +1,41 @@
+import { addComponent, defineQuery, enterQuery } from "bitecs";
+import { HubsWorld } from "../app";
+import { Holdable, MediaContentBounds, Networked, Rigidbody } from "../bit-components";
+import { getBox } from "../utils/auto-box-collider";
+import { Mesh, Vector3 } from "three";
+import { takeSoftOwnership } from "../utils/take-soft-ownership";
+import { EntityID } from "../utils/networking-types";
+import { COLLISION_LAYERS } from "../constants";
+
+const tmpVector = new Vector3();
+
+const interactableQuery = defineQuery([Holdable, Rigidbody, Networked]);
+const interactableEnterQuery = enterQuery(interactableQuery);
+export function interactableSystem(world: HubsWorld) {
+ interactableEnterQuery(world).forEach((eid: EntityID) => {
+ // Somebody must own a scene grabbable otherwise the networked transform will fight with physics.
+ if (Networked.creator[eid] === APP.getSid("scene") && Networked.owner[eid] === APP.getSid("reticulum")) {
+ takeSoftOwnership(world, eid);
+ }
+
+ const obj = world.eid2obj.get(eid);
+ let hasMesh = false;
+ obj?.traverse(child => {
+ if ((child as Mesh).isMesh) {
+ hasMesh = true;
+ }
+ });
+
+ // If it has media frame collision mask, it needs to have content bounds
+ if (hasMesh && Rigidbody.collisionFilterMask[eid] & COLLISION_LAYERS.MEDIA_FRAMES) {
+ const box = getBox(obj, obj);
+ if (!box.isEmpty()) {
+ box.getSize(tmpVector);
+ addComponent(world, MediaContentBounds, eid);
+ MediaContentBounds.bounds[eid].set(tmpVector.toArray());
+ } else {
+ console.error(`Couldn't create content bounds for entity ${eid}. It seems to be empty or have negative scale.`);
+ }
+ }
+ });
+}
diff --git a/src/bit-systems/loop-animation.ts b/src/bit-systems/loop-animation.ts
index 4bc7844ccd..bd791ddad8 100644
--- a/src/bit-systems/loop-animation.ts
+++ b/src/bit-systems/loop-animation.ts
@@ -1,5 +1,5 @@
import { addComponent, defineQuery, enterQuery, exitQuery, hasComponent, removeComponent } from "bitecs";
-import { AnimationAction, AnimationClip, AnimationMixer, LoopRepeat } from "three";
+import { AnimationClip, LoopRepeat } from "three";
import {
MixerAnimatable,
MixerAnimatableData,
@@ -47,12 +47,16 @@ const getActiveClips = (
export function loopAnimationSystem(world: HubsWorld): void {
loopAnimationInitializeEnterQuery(world).forEach((eid: number): void => {
+ const params = LoopAnimationInitializeData.get(eid)!;
+ if (!params.length) {
+ return;
+ }
+
const object = world.eid2obj.get(eid)!;
const mixer = MixerAnimatableData.get(eid)!;
addComponent(world, LoopAnimation, eid);
- const params = LoopAnimationInitializeData.get(eid)!;
const activeAnimations = [];
for (let i = 0; i < params.length; i++) {
diff --git a/src/bit-systems/media-loading.ts b/src/bit-systems/media-loading.ts
index 98832a80dd..a31dcd9051 100644
--- a/src/bit-systems/media-loading.ts
+++ b/src/bit-systems/media-loading.ts
@@ -30,7 +30,8 @@ import {
Rigidbody,
MediaLoaderOffset,
MediaVideo,
- NetworkedTransform
+ NetworkedTransform,
+ MediaRoot
} from "../bit-components";
import { inflatePhysicsShape, Shape } from "../inflators/physics-shape";
import { ErrorObject } from "../prefabs/error-object";
@@ -204,7 +205,7 @@ class UnsupportedMediaTypeError extends Error {
}
}
-type MediaInfo = {
+export type MediaInfo = {
accessibleUrl: string;
canonicalUrl: string;
canonicalAudioUrl: string | null;
@@ -283,6 +284,7 @@ function* loadMedia(world: HubsWorld, eid: EntityID) {
try {
const urlData = (yield resolveMediaInfo(src)) as MediaInfo;
media = yield* loadByMediaType(world, eid, urlData);
+ addComponent(world, MediaRoot, media);
addComponent(world, MediaLoaded, media);
addComponent(world, MediaInfo, media);
MediaInfo.accessibleUrl[media] = APP.getSid(urlData.accessibleUrl);
@@ -359,7 +361,7 @@ export function mediaLoadingSystem(world: HubsWorld) {
jobs.add(eid, clearRollbacks => loadAndAnimateMedia(world, eid, clearRollbacks));
});
- mediaLoadingExitQuery(world).forEach(function (eid) {
+ mediaLoadingExitQuery(world).forEach(function (eid: EntityID) {
jobs.stop(eid);
if (MediaImageLoaderData.has(eid)) {
@@ -406,7 +408,7 @@ export function mediaLoadingSystem(world: HubsWorld) {
}
});
- mediaLoadingQuery(world).forEach(eid => {
+ mediaLoadingQuery(world).forEach((eid: EntityID) => {
const mediaLoaderObj = world.eid2obj.get(eid)!;
transformPosition.fromArray(NetworkedTransform.position[eid]);
if (mediaLoaderObj.position.near(transformPosition, 0.001)) {
@@ -417,7 +419,7 @@ export function mediaLoadingSystem(world: HubsWorld) {
mediaLoadedEnterQuery(world).forEach(() => APP.scene?.emit("listed_media_changed"));
mediaLoadedExitQuery(world).forEach(() => APP.scene?.emit("listed_media_changed"));
- mediaRefreshEnterQuery(world).forEach(eid => {
+ mediaRefreshEnterQuery(world).forEach((eid: EntityID) => {
if (!jobs.has(eid)) {
jobs.add(eid, clearRollbacks => refreshMedia(world, eid, clearRollbacks));
}
diff --git a/src/bit-systems/object-menu.ts b/src/bit-systems/object-menu.ts
index 51c6702a73..e7545c9936 100644
--- a/src/bit-systems/object-menu.ts
+++ b/src/bit-systems/object-menu.ts
@@ -105,7 +105,7 @@ function startRotation(world: HubsWorld, menuEid: EntityID, targetEid: EntityID)
}
const transformSystem = APP.scene!.systems["transform-selected-object"];
const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem;
- physicsSystem.updateRigidBodyOptions(Rigidbody.bodyId[targetEid], { type: "kinematic" });
+ physicsSystem.updateRigidBody(Rigidbody.bodyId[targetEid], { type: "kinematic" });
const rightCursorEid = anyEntityWith(world, RemoteRight)!;
transformSystem.startTransform(world.eid2obj.get(targetEid)!, world.eid2obj.get(rightCursorEid)!, {
mode: TRANSFORM_MODE.CURSOR
@@ -137,7 +137,7 @@ function startScaling(world: HubsWorld, menuEid: EntityID, targetEid: EntityID)
// TODO: Remove the dependency with AFRAME
const transformSystem = (AFRAME as any).scenes[0].systems["transform-selected-object"];
const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem;
- physicsSystem.updateRigidBodyOptions(Rigidbody.bodyId[targetEid], { type: "kinematic" });
+ physicsSystem.updateRigidBody(Rigidbody.bodyId[targetEid], { type: "kinematic" });
const rightCursorEid = anyEntityWith(world, RemoteRight)!;
scalingHandler = new ScalingHandler(world.eid2obj.get(targetEid), transformSystem);
scalingHandler!.objectToScale = world.eid2obj.get(targetEid);
diff --git a/src/bit-systems/quack.ts b/src/bit-systems/quack.ts
deleted file mode 100644
index e66a4bb5c4..0000000000
--- a/src/bit-systems/quack.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { HubsWorld } from "../app";
-import { Held, Quack } from "../bit-components";
-import { defineQuery, enterQuery } from "bitecs";
-import { SOUND_QUACK, SOUND_SPECIAL_QUACK } from "../systems/sound-effects-system";
-
-const heldQuackQuery = defineQuery([Quack, Held]);
-const heldQuackEnterQuery = enterQuery(heldQuackQuery);
-
-export function quackSystem(world: HubsWorld) {
- heldQuackEnterQuery(world).forEach(() => {
- const rand = Math.random();
- if (rand < 0.01) {
- APP.scene?.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_SPECIAL_QUACK);
- } else {
- APP.scene?.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_QUACK);
- }
- });
-}
diff --git a/src/bit-systems/scene-loading.ts b/src/bit-systems/scene-loading.ts
index c782c001b6..5f5a176d91 100644
--- a/src/bit-systems/scene-loading.ts
+++ b/src/bit-systems/scene-loading.ts
@@ -7,7 +7,6 @@ import {
HeightFieldTag,
NavMesh,
Networked,
- PhysicsShape,
SceneLoader,
ScenePreviewCamera,
SceneRoot,
diff --git a/src/bit-systems/text.ts b/src/bit-systems/text.ts
index 5f6a93a7e3..717c2fc49e 100644
--- a/src/bit-systems/text.ts
+++ b/src/bit-systems/text.ts
@@ -1,9 +1,23 @@
import { defineQuery } from "bitecs";
import { Text as TroikaText } from "troika-three-text";
import { HubsWorld } from "../app";
-import { TextTag } from "../bit-components";
+import { NetworkedText, TextTag } from "../bit-components";
+import {
+ NumberOrNormalT,
+ NumberOrPctT,
+ THREE_SIDES,
+ flagToAnchorX,
+ flagToAnchorY,
+ flagToDirection,
+ flagToOverflowWrap,
+ flagToSide,
+ flagToTextAlign,
+ flagToWhiteSpace,
+ stringToNumberOrString
+} from "../inflators/text";
const textQuery = defineQuery([TextTag]);
+const networkedTextQuery = defineQuery([TextTag, NetworkedText]);
export function textSystem(world: HubsWorld) {
textQuery(world).forEach(eid => {
@@ -30,4 +44,100 @@ export function textSystem(world: HubsWorld) {
// because TroikaText properly handles
text.sync();
});
+ networkedTextQuery(world).forEach(eid => {
+ const text = world.eid2obj.get(eid)! as TroikaText;
+ const newText = APP.getString(NetworkedText.text[eid]);
+ if (text.text !== newText) {
+ text.text = newText!;
+ }
+ if (text.fontSize !== NetworkedText.fontSize[eid]) {
+ text.fontSize = NetworkedText.fontSize[eid];
+ }
+ const textAlign = flagToTextAlign(NetworkedText.textAlign[eid]);
+ if (text.textAlign !== textAlign) {
+ text.textAlign = textAlign;
+ }
+ const anchorX = flagToAnchorX(NetworkedText.anchorX[eid]);
+ if (text.anchorX !== anchorX) {
+ text.anchorX = anchorX;
+ }
+ const anchorY = flagToAnchorY(NetworkedText.anchorY[eid]);
+ if (text.anchorY !== anchorY) {
+ text.anchorY = anchorY;
+ }
+ if (text.color !== NetworkedText.color[eid]) {
+ text.color = NetworkedText.color[eid];
+ }
+ if (text.letterSpacing !== NetworkedText.letterSpacing[eid]) {
+ text.letterSpacing = NetworkedText.letterSpacing[eid];
+ }
+ const lineHeight = stringToNumberOrString(APP.getString(NetworkedText.lineHeight[eid])!) as NumberOrNormalT;
+ if (text.lineHeight !== lineHeight) {
+ text.lineHeight = lineHeight;
+ }
+ const outlineWidth = stringToNumberOrString(APP.getString(NetworkedText.outlineWidth[eid])!) as NumberOrPctT;
+ if (text.outlineWidth !== outlineWidth) {
+ text.outlineWidth = outlineWidth;
+ }
+ if (text.outlineColor !== NetworkedText.outlineColor[eid]) {
+ text.outlineColor = NetworkedText.outlineColor[eid];
+ }
+ const outlineBlur = stringToNumberOrString(APP.getString(NetworkedText.outlineBlur[eid])!) as NumberOrPctT;
+ if (text.outlineBlur !== outlineBlur) {
+ text.outlineBlur = outlineBlur;
+ }
+ const outlineOffsetX = stringToNumberOrString(APP.getString(NetworkedText.outlineOffsetX[eid])!) as NumberOrPctT;
+ if (text.outlineOffsetX !== outlineOffsetX) {
+ text.outlineOffsetX = outlineOffsetX;
+ }
+ const outlineOffsetY = stringToNumberOrString(APP.getString(NetworkedText.outlineOffsetY[eid])!) as NumberOrPctT;
+ if (text.outlineOffsetY !== outlineOffsetY) {
+ text.outlineOffsetY = outlineOffsetY;
+ }
+ if (text.outlineOpacity !== NetworkedText.outlineOpacity[eid]) {
+ text.outlineOpacity = NetworkedText.outlineOpacity[eid];
+ }
+ if (text.fillOpacity !== NetworkedText.fillOpacity[eid]) {
+ text.fillOpacity = NetworkedText.fillOpacity[eid];
+ }
+ const strokeWidth = stringToNumberOrString(APP.getString(NetworkedText.strokeWidth[eid])!) as NumberOrPctT;
+ if (text.strokeWidth !== strokeWidth) {
+ text.strokeWidth = strokeWidth;
+ }
+ if (text.strokeColor !== NetworkedText.strokeColor[eid]) {
+ text.strokeColor = NetworkedText.strokeColor[eid];
+ }
+ if (text.strokeOpacity !== NetworkedText.strokeOpacity[eid]) {
+ text.strokeOpacity = NetworkedText.strokeOpacity[eid];
+ }
+ if (text.textIndent !== NetworkedText.textIndent[eid]) {
+ text.textIndent = NetworkedText.textIndent[eid];
+ }
+ const whiteSpace = flagToWhiteSpace(NetworkedText.whiteSpace[eid]);
+ if (text.whiteSpace !== whiteSpace) {
+ text.whiteSpace = whiteSpace;
+ }
+ const overflowWrap = flagToOverflowWrap(NetworkedText.overflowWrap[eid]);
+ if (text.overflowWrap !== overflowWrap) {
+ text.overflowWrap = overflowWrap;
+ }
+ if (text.material!.opacity !== NetworkedText.opacity[eid]) {
+ text.material!.opacity = NetworkedText.opacity[eid];
+ }
+ const side = THREE_SIDES[flagToSide(NetworkedText.side[eid])];
+ if (text.material!.side !== side) {
+ text.material!.side = side;
+ }
+ if (text.maxWidth !== NetworkedText.maxWidth[eid]) {
+ text.maxWidth = NetworkedText.maxWidth[eid];
+ }
+ if (text.curveRadius !== NetworkedText.curveRadius[eid]) {
+ text.curveRadius = NetworkedText.curveRadius[eid];
+ }
+ const direction = flagToDirection(NetworkedText.direction[eid]);
+ if (text.direction !== direction) {
+ text.direction = direction;
+ }
+ text.sync();
+ });
}
diff --git a/src/bit-systems/video-system.ts b/src/bit-systems/video-system.ts
index 6fe23202c4..95dc5b2a9e 100644
--- a/src/bit-systems/video-system.ts
+++ b/src/bit-systems/video-system.ts
@@ -1,10 +1,22 @@
-import { addComponent, defineQuery, enterQuery, entityExists, exitQuery, hasComponent, removeComponent } from "bitecs";
+import {
+ addComponent,
+ defineComponent,
+ defineQuery,
+ enterQuery,
+ entityExists,
+ exitQuery,
+ hasComponent,
+ removeComponent
+} from "bitecs";
import { Mesh } from "three";
import { HubsWorld } from "../app";
import {
+ AudioEmitter,
AudioParams,
AudioSettingsChanged,
+ MediaInfo,
MediaLoaded,
+ MediaRoot,
MediaVideo,
MediaVideoData,
MediaVideoUpdated,
@@ -14,25 +26,102 @@ import {
} from "../bit-components";
import { SourceType } from "../components/audio-params";
import { AudioSystem } from "../systems/audio-system";
-import { findAncestorWithComponent } from "../utils/bit-utils";
-import { Emitter2Audio, Emitter2Params, makeAudioEntity } from "./audio-emitter-system";
+import { findAncestorWithComponent, findChildWithComponent } from "../utils/bit-utils";
+import { Emitter2Audio, Emitter2Params, makeAudioEntity, swapAudioSrc } from "./audio-emitter-system";
import { takeSoftOwnership } from "../utils/take-soft-ownership";
import { crNextFrame } from "../utils/coroutine";
+import { ClearFunction, JobRunner, swapObject3DComponent } from "../hubs";
+import { VIDEO_FLAGS } from "../inflators/video";
+import { HubsVideoTexture } from "../textures/HubsVideoTexture";
+import { create360ImageMesh, createImageMesh } from "../utils/create-image-mesh";
+import { loadAudioTexture } from "../utils/load-audio-texture";
+import { loadVideoTexture } from "../utils/load-video-texture";
+import { resolveMediaInfo, MediaType } from "../utils/media-utils";
+import { EntityID } from "../utils/networking-types";
+import { ProjectionModeName, getProjectionNameFromProjection } from "../utils/projection-mode";
+import { disposeNode } from "../utils/three-utils";
+import { MediaInfo as MediaInfoT } from "./media-loading";
+
+export const MediaVideoUpdateSrcEvent = defineComponent();
+
+function* loadSrc(
+ world: HubsWorld,
+ eid: EntityID,
+ src: string,
+ oldVideo: HTMLVideoElement,
+ clearRollbacks: ClearFunction
+) {
+ const projection = getProjectionNameFromProjection(NetworkedVideo.projection[eid]);
+ const autoPlay = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false;
+ const loop = NetworkedVideo.flags[eid] & VIDEO_FLAGS.LOOP ? true : false;
+ const { accessibleUrl, contentType, mediaType } = (yield resolveMediaInfo(src)) as MediaInfoT;
+ let data: any;
+ if (mediaType === MediaType.VIDEO) {
+ data = (yield loadVideoTexture(accessibleUrl, contentType, loop, autoPlay)) as unknown;
+ } else if (mediaType === MediaType.AUDIO) {
+ data = (yield loadAudioTexture(accessibleUrl, loop, autoPlay)) as unknown;
+ } else {
+ return;
+ }
+
+ const { texture, ratio, video }: { texture: HubsVideoTexture; ratio: number; video: HTMLVideoElement } = data;
+
+ clearRollbacks(); // After this point, normal entity cleanup will take care of things
+
+ let videoObj;
+ if (projection === ProjectionModeName.SPHERE_EQUIRECTANGULAR) {
+ videoObj = create360ImageMesh(texture, ratio);
+ } else {
+ videoObj = createImageMesh(texture, ratio);
+ }
+ MediaVideo.ratio[eid] = ratio;
+ MediaVideoData.set(eid, video);
+ oldVideo.pause();
+ const mediaRoot = findAncestorWithComponent(world, MediaRoot, eid)!;
+ const mediaRootObj = world.eid2obj.get(mediaRoot)!;
+ mediaRootObj.add(videoObj);
+
+ const audioEmitter = findChildWithComponent(world, AudioEmitter, eid)!;
+ swapAudioSrc(world, eid, audioEmitter);
+ const audioObj = APP.world.eid2obj.get(audioEmitter)!;
+ videoObj.add(audioObj);
+
+ const oldVideoObj = APP.world.eid2obj.get(eid)! as Mesh;
+ mediaRootObj.remove(oldVideoObj);
+ disposeNode(oldVideoObj);
+
+ swapObject3DComponent(world, eid, videoObj);
+
+ if ((NetworkedVideo.flags[eid] & VIDEO_FLAGS.PAUSED) === 0 || autoPlay) {
+ video.play();
+ }
+
+ removeComponent(world, MediaVideoUpdateSrcEvent, eid);
+}
enum Flags {
PAUSED = 1 << 0
}
+export function updateVideoSrc(world: HubsWorld, eid: EntityID, src: string, video: HTMLVideoElement) {
+ addComponent(world, MediaVideoUpdateSrcEvent, eid);
+
+ jobs.stop(eid);
+ jobs.add(eid, clearRollbacks => loadSrc(world, eid, src, video, clearRollbacks));
+}
+
+const jobs = new JobRunner();
export const OUT_OF_SYNC_SEC = 5;
const networkedVideoQuery = defineQuery([Networked, NetworkedVideo]);
const networkedVideoEnterQuery = enterQuery(networkedVideoQuery);
const mediaVideoQuery = defineQuery([MediaVideo]);
const mediaVideoEnterQuery = enterQuery(mediaVideoQuery);
+const networkedVideoExitQuery = exitQuery(networkedVideoQuery);
const mediaVideoExitQuery = exitQuery(mediaVideoQuery);
const mediaLoadStatusQuery = defineQuery([MediaVideo, MediaLoaded]);
const mediaLoadedQuery = enterQuery(mediaLoadStatusQuery);
export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) {
- mediaVideoEnterQuery(world).forEach(function (videoEid) {
+ mediaVideoEnterQuery(world).forEach(function (videoEid: EntityID) {
const videoObj = world.eid2obj.get(videoEid) as Mesh;
const video = MediaVideoData.get(videoEid)!;
if (video.autoplay) {
@@ -48,7 +137,7 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) {
// Note in media-video we call updateMatrixWorld here to force PositionalAudio's updateMatrixWorld to run even
// if it has an invisible parent. We don't want to have invisible parents now.
});
- mediaLoadedQuery(world).forEach(videoEid => {
+ mediaLoadedQuery(world).forEach((videoEid: EntityID) => {
const audioParamsEid = findAncestorWithComponent(world, AudioParams, videoEid);
if (audioParamsEid) {
const audioSettings = APP.audioOverrides.get(audioParamsEid)!;
@@ -58,7 +147,7 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) {
addComponent(world, AudioSettingsChanged, audioEid);
}
});
- mediaVideoExitQuery(world).forEach(videoEid => {
+ mediaVideoExitQuery(world).forEach((videoEid: EntityID) => {
const audioParamsEid = Emitter2Params.get(videoEid);
audioParamsEid && APP.audioOverrides.delete(audioParamsEid);
Emitter2Params.delete(videoEid);
@@ -66,13 +155,16 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) {
MediaVideoData.delete(videoEid);
});
- networkedVideoEnterQuery(world).forEach(function (eid) {
+ networkedVideoEnterQuery(world).forEach(function (eid: EntityID) {
if (Networked.owner[eid] === APP.getSid("reticulum")) {
takeSoftOwnership(world, eid);
}
});
+ networkedVideoExitQuery(world).forEach((eid: EntityID) => {
+ jobs.stop(eid);
+ });
- networkedVideoQuery(world).forEach(function (eid) {
+ networkedVideoQuery(world).forEach(function (eid: EntityID) {
const video = MediaVideoData.get(eid)!;
if (hasComponent(world, Owned, eid)) {
const now = performance.now();
@@ -80,18 +172,49 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) {
NetworkedVideo.time[eid] = video.currentTime;
MediaVideo.lastUpdate[eid] = now;
}
- let flags = 0;
- flags |= video.paused ? Flags.PAUSED : 0;
+ let flags = MediaVideo.flags[eid];
+ if (video.paused) {
+ flags |= VIDEO_FLAGS.PAUSED;
+ } else {
+ flags &= ~VIDEO_FLAGS.PAUSED;
+ }
+ if (video.loop) {
+ flags |= VIDEO_FLAGS.LOOP;
+ } else {
+ flags &= ~VIDEO_FLAGS.LOOP;
+ }
+ if (video.autoplay) {
+ flags |= VIDEO_FLAGS.AUTO_PLAY;
+ } else {
+ flags &= ~VIDEO_FLAGS.AUTO_PLAY;
+ }
NetworkedVideo.flags[eid] = flags;
+ NetworkedVideo.src[eid] = MediaInfo.accessibleUrl[eid];
} else {
- const networkedPauseState = !!(NetworkedVideo.flags[eid] & Flags.PAUSED);
+ let shouldUpdateVideo = false;
+ const autoPlay = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false;
+ const loop = NetworkedVideo.flags[eid] & VIDEO_FLAGS.AUTO_PLAY ? true : false;
+ if (MediaVideo.flags[eid] !== NetworkedVideo.flags[eid]) {
+ MediaVideo.flags[eid] = NetworkedVideo.flags[eid];
+ }
+ if (MediaVideo.projection[eid] !== NetworkedVideo.projection[eid]) {
+ MediaVideo.projection[eid] = NetworkedVideo.projection[eid];
+ shouldUpdateVideo ||= true;
+ }
+ const src = APP.getString(NetworkedVideo.src[eid])!;
+ const currentSrc = APP.getString(MediaInfo.accessibleUrl[eid]);
+ shouldUpdateVideo ||= src !== currentSrc || autoPlay !== video.autoplay || loop !== video.loop;
+ if (shouldUpdateVideo && !hasComponent(world, MediaVideoUpdateSrcEvent, eid)) {
+ updateVideoSrc(world, eid, src, video);
+ }
+ const networkedPauseState = !!(NetworkedVideo.flags[eid] & VIDEO_FLAGS.PAUSED);
if (networkedPauseState !== video.paused) {
- video.paused
- ? video.play().catch(() => {
+ networkedPauseState
+ ? video.pause()
+ : video.play().catch(() => {
// Need to deal with the fact play() may fail if user has not interacted with browser yet.
console.error("Error playing video.");
- })
- : video.pause();
+ });
addComponent(world, MediaVideoUpdated, eid);
}
if (networkedPauseState || Math.abs(NetworkedVideo.time[eid] - video.currentTime) > OUT_OF_SYNC_SEC) {
@@ -100,7 +223,7 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) {
}
}
});
- mediaVideoQuery(world).forEach(eid => {
+ mediaVideoQuery(world).forEach((eid: EntityID) => {
// We need to delay this a frame to give a chance to other services to process this event
crNextFrame().then(() => {
if (entityExists(world, eid)) {
@@ -108,4 +231,6 @@ export function videoSystem(world: HubsWorld, audioSystem: AudioSystem) {
}
});
});
+
+ jobs.tick();
}
diff --git a/src/cloud.js b/src/cloud.js
index e6e4576f86..a99adde6f5 100644
--- a/src/cloud.js
+++ b/src/cloud.js
@@ -9,7 +9,7 @@ import { PageContainer } from "./react-components/layout/PageContainer";
import { AuthContextProvider } from "./react-components/auth/AuthContext";
import { Container } from "./react-components/layout/Container";
import { Button } from "./react-components/input/Button";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
import registerTelemetry from "./telemetry";
import { FormattedMessage } from "react-intl";
@@ -117,6 +117,7 @@ function HubsCloudPage() {
);
}
+const store = getStore();
window.APP = { store };
function CloudRoot() {
diff --git a/src/components/body-helper.js b/src/components/body-helper.js
index e7b17d63d7..96496da1e0 100644
--- a/src/components/body-helper.js
+++ b/src/components/body-helper.js
@@ -1,6 +1,7 @@
import { addComponent, removeComponent } from "bitecs";
import { CONSTANTS } from "three-ammo";
import { Rigidbody } from "../bit-components";
+import { updateBodyParams } from "../inflators/rigid-body";
const ACTIVATION_STATE = CONSTANTS.ACTIVATION_STATE,
TYPE = CONSTANTS.TYPE;
@@ -41,6 +42,7 @@ AFRAME.registerComponent("body-helper", {
this.uuid = this.system.addBody(this.el.object3D, this.data);
const eid = this.el.object3D.eid;
addComponent(APP.world, Rigidbody, eid);
+ updateBodyParams(eid, this.data);
Rigidbody.bodyId[eid] = this.uuid; //uuid is a lie, it's actually an int
},
diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js
index 13f5e87a19..c668615aa4 100644
--- a/src/components/gltf-model-plus.js
+++ b/src/components/gltf-model-plus.js
@@ -508,6 +508,18 @@ class GLTFHubsPlugin {
}
}
}
+ const materials = parser.json.materials;
+ if (materials) {
+ for (let i = 0; i < materials.length; i++) {
+ const mat = materials[i];
+
+ if (!mat.extras) {
+ mat.extras = {};
+ }
+
+ mat.extras.gltfIndex = i;
+ }
+ }
}
afterRoot(gltf) {
@@ -858,6 +870,7 @@ class GLTFHubsLoopAnimationComponent {
}
}
+export const gltfPluginsExtra = [];
export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) {
let gltfUrl = src;
let fileMap;
@@ -933,6 +946,7 @@ export async function loadGLTF(src, contentType, onProgress, jsonPreprocessor) {
}
})
);
+ gltfPluginsExtra.forEach(ext => gltfLoader.register(parser => ext(parser)));
// TODO some models are loaded before the renderer exists. This is likely things like the camera tool and loading cube.
// They don't currently use KTX textures but if they did this would be an issue. Fixing this is hard but is part of
diff --git a/src/components/media-loader.js b/src/components/media-loader.js
index 9c89bbddcf..94961d74f6 100644
--- a/src/components/media-loader.js
+++ b/src/components/media-loader.js
@@ -284,8 +284,10 @@ AFRAME.registerComponent("media-loader", {
// TODO this does duplicate work in some cases, but finish() is the only consistent place to do it
const contentBounds = getBox(this.el.object3D, this.el.getObject3D("mesh")).getSize(new THREE.Vector3());
- addComponent(APP.world, MediaContentBounds, el.eid);
- MediaContentBounds.bounds[el.eid].set(contentBounds.toArray());
+ if (el.eid) {
+ addComponent(APP.world, MediaContentBounds, el.eid);
+ MediaContentBounds.bounds[el.eid].set(contentBounds.toArray());
+ }
el.emit("media-loaded");
};
diff --git a/src/constants.ts b/src/constants.ts
index 64c1c988e7..189993a663 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -6,12 +6,13 @@ export enum COLLISION_LAYERS {
AVATAR = 1 << 2,
HANDS = 1 << 3,
MEDIA_FRAMES = 1 << 4,
+ TRIGGERS = 1 << 5,
// @TODO we should split these "sets" off into something other than COLLISION_LAYERS or at least name
// them differently to indicate they are a combination of multiple bits
- DEFAULT_INTERACTABLE = INTERACTABLES | ENVIRONMENT | AVATAR | HANDS | MEDIA_FRAMES,
+ DEFAULT_INTERACTABLE = INTERACTABLES | ENVIRONMENT | AVATAR | HANDS | MEDIA_FRAMES | TRIGGERS,
UNOWNED_INTERACTABLE = INTERACTABLES | HANDS | MEDIA_FRAMES,
DEFAULT_SPAWNER = INTERACTABLES | HANDS
-};
+}
export enum AAModes {
NONE = "NONE",
diff --git a/src/discord.js b/src/discord.js
index 5a0efc53c0..1148b9e30f 100644
--- a/src/discord.js
+++ b/src/discord.js
@@ -9,7 +9,7 @@ import discordBotLogo from "./assets/images/discord-bot-logo.png";
import registerTelemetry from "./telemetry";
import { ThemeProvider } from "./react-components/styles/theme";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
registerTelemetry("/discord", "Discord Landing Page");
@@ -19,6 +19,7 @@ class DiscordPage extends Component {
componentDidMount() {}
render() {
+ const store = getStore();
return (
diff --git a/src/hub.html b/src/hub.html
index 623027ad19..a6d733bd6f 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -211,7 +211,7 @@
@@ -821,7 +821,8 @@
-
+
diff --git a/src/hub.js b/src/hub.js
index fb609a6b7e..bd2eb1a554 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -183,6 +183,7 @@ import "./systems/audio-debug-system";
import "./systems/audio-gain-system";
import "./gltf-component-mappings";
+import { addons } from "./addons";
import { App, getScene } from "./app";
import MediaDevicesManager from "./utils/media-devices-manager";
import PinningHelper from "./utils/pinning-helper";
@@ -219,8 +220,6 @@ preload(
})
);
-const store = window.APP.store;
-store.update({ preferences: { shouldPromptForRefresh: false } }); // Clear flag that prompts for refresh from preference screen
const mediaSearchStore = window.APP.mediaSearchStore;
const OAUTH_FLOW_PERMS_TOKEN_KEY = "ret-oauth-flow-perms-token";
const NOISY_OCCUPANT_COUNT = 30; // Above this # of occupants, we stop posting join/leaves/renames
@@ -273,6 +272,7 @@ import { exposeBitECSDebugHelpers } from "./bitecs-debug-helpers";
import { loadLegacyRoomObjects } from "./utils/load-legacy-room-objects";
import { loadSavedEntityStates } from "./utils/entity-state-utils";
import { shouldUseNewLoader } from "./utils/bit-utils";
+import { getStore } from "./utils/store-instance";
const PHOENIX_RELIABLE_NAF = "phx-reliable";
NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF;
@@ -353,6 +353,7 @@ function mountUI(props = {}) {
qsTruthy("allow_idle") || (process.env.NODE_ENV === "development" && !qs.get("idle_timeout"));
const forcedVREntryType = qsVREntryType;
+ const store = getStore();
root.render(
@@ -543,8 +544,18 @@ export async function updateEnvironmentForHub(hub, entryManager) {
}
}
-export async function updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt = false) {
- remountUI({ hub, entryDisallowed: !hubChannel.canEnterRoom(hub), showBitECSBasedClientRefreshPrompt });
+export async function updateUIForHub(
+ hub,
+ hubChannel,
+ showBitECSBasedClientRefreshPrompt = false,
+ showAddonRefreshPrompt = false
+) {
+ remountUI({
+ hub,
+ entryDisallowed: !hubChannel.canEnterRoom(hub),
+ showBitECSBasedClientRefreshPrompt,
+ showAddonRefreshPrompt
+ });
}
function onConnectionError(entryManager, connectError) {
@@ -596,7 +607,7 @@ function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data)
onSendMessage: messageDispatch.dispatch,
onLoaded: () => {
audioSystem.setMediaGainOverride(1);
- store.executeOnLoadActions(scene);
+ getStore().executeOnLoadActions(scene);
},
onMediaSearchResultEntrySelected: (entry, selectAction) =>
scene.emit("action_selected_media_result_entry", { entry, selectAction }),
@@ -728,6 +739,9 @@ async function runBotMode(scene, entryManager) {
}
document.addEventListener("DOMContentLoaded", async () => {
+ const store = getStore();
+ store.update({ preferences: { shouldPromptForRefresh: false } }); // Clear flag that prompts for refresh from preference screen
+
if (!root) {
const container = document.getElementById("ui-root");
root = createRoot(container);
@@ -1388,16 +1402,26 @@ document.addEventListener("DOMContentLoaded", async () => {
const displayName = (userInfo && userInfo.metas[0].profile.displayName) || "API";
let showBitECSBasedClientRefreshPrompt = false;
-
- if (!!hub.user_data?.hubs_use_bitecs_based_client !== !!window.APP.hub.user_data?.hubs_use_bitecs_based_client) {
+ if (!!hub.user_data?.hubs_use_bitecs_based_client !== !!APP.hub.user_data?.hubs_use_bitecs_based_client) {
showBitECSBasedClientRefreshPrompt = true;
setTimeout(() => {
document.location.reload();
}, 5000);
}
+ let showAddonRefreshPrompt = false;
+ [...addons.keys()].map(id => {
+ const oldAddonState = !!APP.hub.user_data && "addons" in APP.hub.user_data && APP.hub.user_data.addons[id];
+ const newAddonState = !!hub.user_data && "addons" in hub.user_data && hub.user_data.addons[id];
+ if (newAddonState !== oldAddonState) {
+ showAddonRefreshPrompt = true;
+ setTimeout(() => {
+ document.location.reload();
+ }, 5000);
+ }
+ });
window.APP.hub = hub;
- updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt);
+ updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt, showAddonRefreshPrompt);
if (
stale_fields.includes("scene") ||
@@ -1484,4 +1508,6 @@ document.addEventListener("DOMContentLoaded", async () => {
authChannel.setSocket(socket);
linkChannel.setSocket(socket);
+
+ APP.notifyOnInit();
});
diff --git a/src/hubs.js b/src/hubs.js
new file mode 100644
index 0000000000..48dc23a122
--- /dev/null
+++ b/src/hubs.js
@@ -0,0 +1,43 @@
+export * from "./bit-components";
+export * as bitComponents from "./bit-components";
+export * from "./addons";
+export * from "./types";
+export * from "./camera-layers";
+export * from "./constants";
+export * from "./change-hub";
+export * from "./utils/bit-utils";
+export * from "./utils/jsx-entity";
+export * from "./utils/media-url-utils";
+export * from "./utils/bit-pinning-helper";
+export * from "./utils/create-networked-entity";
+export * from "./utils/material-utils";
+export * from "./utils/network-schemas";
+export * from "./utils/define-network-schema";
+export * from "./utils/animate";
+export * from "./utils/easing";
+export * from "./utils/coroutine";
+export * from "./utils/coroutine-utils";
+export * from "./utils/take-ownership";
+export * from "./utils/take-soft-ownership";
+export * from "./utils/component-utils";
+export * from "./utils/projection-mode";
+export * from "./utils/assign-network-ids";
+export * from "./utils/store-instance";
+export * from "./components/gltf-model-plus";
+export * from "./inflators/model";
+export * from "./inflators/physics-shape";
+export * from "./inflators/media-frame";
+export * from "./inflators/rigid-body";
+export * from "./inflators/text";
+export * from "./inflators/video";
+export * from "./bit-systems/delete-entity-system";
+export * from "./bit-systems/video-system";
+export * from "./systems/floaty-object-system";
+export * from "./systems/userinput/paths";
+export * from "./systems/userinput/sets";
+export * from "./systems/userinput/userinput";
+export * from "./systems/userinput/bindings/xforms";
+export * from "./systems/userinput/bindings/keyboard-mouse-user";
+export * from "./systems/userinput/devices/keyboard";
+export * from "./systems/bit-physics";
+export * from "./react-components/debug-panel/ECSSidebar";
diff --git a/src/index.js b/src/index.js
index 896c491f7a..414578772c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,10 +7,11 @@ import { HomePage } from "./react-components/home/HomePage";
import { AuthContextProvider } from "./react-components/auth/AuthContext";
import "./react-components/styles/global.scss";
import { ThemeProvider } from "./react-components/styles/theme";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
registerTelemetry("/home", "Hubs Home Page");
+const store = getStore();
window.APP = { store };
function HomeRoot() {
diff --git a/src/inflators/grabbable.ts b/src/inflators/grabbable.ts
index 2c8f89405e..4aa29e6f4f 100644
--- a/src/inflators/grabbable.ts
+++ b/src/inflators/grabbable.ts
@@ -3,11 +3,11 @@ import { HubsWorld } from "../app";
import {
CursorRaycastable,
HandCollisionTarget,
- Holdable,
OffersHandConstraint,
OffersRemoteConstraint,
RemoteHoverTarget
} from "../bit-components";
+import { inflateHoldable } from "./holdable";
export type GrabbableParams = { cursor: boolean; hand: boolean };
const defaults: GrabbableParams = { cursor: true, hand: true };
@@ -22,5 +22,5 @@ export function inflateGrabbable(world: HubsWorld, eid: number, props: Grabbable
addComponent(world, RemoteHoverTarget, eid);
addComponent(world, OffersRemoteConstraint, eid);
}
- addComponent(world, Holdable, eid);
+ inflateHoldable(world, eid);
}
diff --git a/src/inflators/holdable.ts b/src/inflators/holdable.ts
new file mode 100644
index 0000000000..d76c099823
--- /dev/null
+++ b/src/inflators/holdable.ts
@@ -0,0 +1,20 @@
+import { addComponent } from "bitecs";
+import { HubsWorld } from "../app";
+import { Holdable } from "../bit-components";
+
+export const HOLDABLE_FLAGS = {
+ ENABLED: 1 << 0
+};
+
+export type HoldableParams = { enabled: boolean };
+const defaults: HoldableParams = { enabled: true };
+export function inflateHoldable(world: HubsWorld, eid: number, props?: HoldableParams) {
+ props = Object.assign({}, defaults, props);
+
+ addComponent(world, Holdable, eid);
+ if (props.enabled !== false) {
+ Holdable.flags[eid] |= HOLDABLE_FLAGS.ENABLED;
+ } else {
+ Holdable.flags[eid] &= ~HOLDABLE_FLAGS.ENABLED;
+ }
+}
diff --git a/src/inflators/loop-animation.ts b/src/inflators/loop-animation.ts
index d21665bec7..f032e5fd4a 100644
--- a/src/inflators/loop-animation.ts
+++ b/src/inflators/loop-animation.ts
@@ -20,7 +20,7 @@ type ElementParams = {
export type LoopAnimationParams = ElementParams[];
-const ELEMENT_DEFAULTS: Required = {
+export const LOOP_ANIMATION_DEFAULTS: Required = {
activeClipIndex: 0,
clip: "",
activeClipIndices: [],
@@ -29,20 +29,14 @@ const ELEMENT_DEFAULTS: Required = {
timeScale: 1.0
};
-const DEFAULTS: Required = [ELEMENT_DEFAULTS];
-
export function inflateLoopAnimationInitialize(
world: HubsWorld,
eid: number,
params: LoopAnimationParams = []
): number {
- if (params.length === 0) {
- params = DEFAULTS;
- }
-
const componentParams = [];
for (let i = 0; i < params.length; i++) {
- const requiredParams = Object.assign({}, ELEMENT_DEFAULTS, params[i]) as Required;
+ const requiredParams = Object.assign({}, LOOP_ANIMATION_DEFAULTS, params[i]) as Required;
const activeClipIndices =
requiredParams.activeClipIndices.length > 0 ? requiredParams.activeClipIndices : [requiredParams.activeClipIndex];
componentParams.push({
diff --git a/src/inflators/media-frame.js b/src/inflators/media-frame.js
index 84db6250ef..d1dccaaace 100644
--- a/src/inflators/media-frame.js
+++ b/src/inflators/media-frame.js
@@ -15,14 +15,28 @@ export const AxisAlignType = {
};
export const MEDIA_FRAME_FLAGS = {
- SCALE_TO_BOUNDS: 1 << 0
+ SCALE_TO_BOUNDS: 1 << 0,
+ ACTIVE: 1 << 1,
+ SNAP_TO_CENTER: 1 << 2,
+ LOCKED: 1 << 3
+};
+
+export const MediaTypes = {
+ all: MediaType.ALL,
+ "all-2d": MediaType.ALL_2D,
+ model: MediaType.MODEL,
+ image: MediaType.IMAGE,
+ video: MediaType.VIDEO,
+ pdf: MediaType.PDF
};
const DEFAULTS = {
bounds: { x: 1, y: 1, z: 1 },
mediaType: "all",
scaleToBounds: true,
- align: { x: "center", y: "center", z: "center" }
+ align: { x: "center", y: "center", z: "center" },
+ active: true,
+ locked: false
};
export function inflateMediaFrame(world, eid, componentProps) {
componentProps = Object.assign({}, DEFAULTS, componentProps);
@@ -68,17 +82,16 @@ export function inflateMediaFrame(world, eid, componentProps) {
addComponent(world, MediaFrame, eid, true);
addComponent(world, NetworkedMediaFrame, eid, true);
+ NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.ACTIVE;
+ if (componentProps.snapToCenter) {
+ NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.SNAP_TO_CENTER;
+ }
+
if (!hasComponent(world, Networked, eid)) addComponent(world, Networked, eid);
// Media types accepted
- MediaFrame.mediaType[eid] = {
- all: MediaType.ALL,
- "all-2d": MediaType.ALL_2D,
- model: MediaType.MODEL,
- image: MediaType.IMAGE,
- video: MediaType.VIDEO,
- pdf: MediaType.PDF
- }[componentProps.mediaType];
+ MediaFrame.mediaType[eid] = MediaTypes[componentProps.mediaType];
+ NetworkedMediaFrame.mediaType[eid] = MediaFrame.mediaType[eid];
// Bounds
MediaFrame.bounds[eid].set([componentProps.bounds.x, componentProps.bounds.y, componentProps.bounds.z]);
// Axis alignment
@@ -101,6 +114,15 @@ export function inflateMediaFrame(world, eid, componentProps) {
if (componentProps.scaleToBounds) flags |= MEDIA_FRAME_FLAGS.SCALE_TO_BOUNDS;
MediaFrame.flags[eid] = flags;
+ if (componentProps.active) {
+ NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.ACTIVE;
+ MediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.ACTIVE;
+ }
+ if (componentProps.locked) {
+ NetworkedMediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.LOCKED;
+ MediaFrame.flags[eid] |= MEDIA_FRAME_FLAGS.LOCKED;
+ }
+
inflateRigidBody(world, eid, {
type: Type.KINEMATIC,
collisionGroup: COLLISION_LAYERS.MEDIA_FRAMES,
diff --git a/src/inflators/model.tsx b/src/inflators/model.tsx
index 42875bd551..7d03ff2ca9 100644
--- a/src/inflators/model.tsx
+++ b/src/inflators/model.tsx
@@ -7,24 +7,23 @@ import { mapMaterials } from "../utils/material-utils";
import { EntityID } from "../utils/networking-types";
import { inflateLoopAnimationInitialize, LoopAnimationParams } from "./loop-animation";
-function camelCase(s: string) {
+export function camelCase(s: string) {
return s.replace(/-(\w)/g, (_, m) => m.toUpperCase());
}
export type ModelParams = { model: Object3D };
+export type GLTFLinkResolverFn = (
+ world: HubsWorld,
+ model: Object3D,
+ rootEid: EntityID,
+ idx2eid: Map
+) => void;
+
// These components are all handled in some special way, not through inflators
-const ignoredComponents = [
- "visible",
- "frustum",
- "frustrum",
- "shadow",
- "networked",
- "animation-mixer",
- "loop-animation"
-];
-
-function inflateComponents(
+const ignoredComponents = ["visible", "frustum", "frustrum", "shadow", "animation-mixer", "loop-animation"];
+
+export function inflateComponents(
world: HubsWorld,
eid: number,
components: { [componentName: string]: any },
@@ -59,6 +58,7 @@ function inflateComponents(
});
}
+export const gltfLinkResolvers = new Array();
export function inflateModel(world: HubsWorld, rootEid: number, { model }: ModelParams) {
const swap: [old: Object3D, replacement: Object3D][] = [];
const idx2eid = new Map();
@@ -166,5 +166,7 @@ export function inflateModel(world: HubsWorld, rootEid: number, { model }: Model
inflateLoopAnimationInitialize(world, rootEid, loopAnimationParams);
}
+ gltfLinkResolvers.forEach(resolved => resolved(world, model, rootEid, idx2eid));
+
addComponent(world, GLTFModel, rootEid);
}
diff --git a/src/inflators/rigid-body.ts b/src/inflators/rigid-body.ts
index 54f2cf55e5..fee7717ffe 100644
--- a/src/inflators/rigid-body.ts
+++ b/src/inflators/rigid-body.ts
@@ -1,7 +1,8 @@
import { addComponent } from "bitecs";
-import { Rigidbody } from "../bit-components";
+import { NetworkedRigidBody, Rigidbody } from "../bit-components";
import { HubsWorld } from "../app";
import { CONSTANTS } from "three-ammo";
+import { COLLISION_LAYERS } from "../constants";
export enum Type {
STATIC = 0,
@@ -9,6 +10,13 @@ export enum Type {
KINEMATIC
}
+export enum CollisionGroup {
+ OBJECTS = "objects",
+ ENVIRONMENT = "environment",
+ TRIGGERS = "triggers",
+ AVATARS = "avatars"
+}
+
export enum ActivationState {
ACTIVE_TAG = 0,
ISLAND_SLEEPING = 1,
@@ -17,6 +25,23 @@ export enum ActivationState {
DISABLE_SIMULATION = 4
}
+export type BodyParams = {
+ type: string;
+ mass: number;
+ gravity: { x: number; y: number; z: number };
+ linearDamping: number;
+ angularDamping: number;
+ linearSleepingThreshold: number;
+ angularSleepingThreshold: number;
+ angularFactor: { x: number; y: number; z: number };
+ activationState: string;
+ emitCollisionEvents: boolean;
+ disableCollision: boolean;
+ collisionFilterGroup: number;
+ collisionFilterMask: number;
+ scaleAutoUpdate: boolean;
+};
+
export type RigidBodyParams = {
type: Type;
mass: number;
@@ -61,10 +86,50 @@ export const getTypeString = (eid: number) => {
return Object.values(CONSTANTS.TYPE)[Rigidbody.type[eid]];
};
-export const getActivationStateString = (eid: number) => {
+export const getStringFromActivationState = (eid: number) => {
return Object.values(CONSTANTS.ACTIVATION_STATE)[Rigidbody.activationState[eid]];
};
+export const getActivationStateFromString = (activationState: string) => {
+ switch (activationState) {
+ case CONSTANTS.ACTIVATION_STATE.ACTIVE_TAG:
+ return ActivationState.ACTIVE_TAG;
+ case CONSTANTS.ACTIVATION_STATE.DISABLE_DEACTIVATION:
+ return ActivationState.DISABLE_DEACTIVATION;
+ case CONSTANTS.ACTIVATION_STATE.DISABLE_SIMULATION:
+ return ActivationState.DISABLE_SIMULATION;
+ case CONSTANTS.ACTIVATION_STATE.ISLAND_SLEEPING:
+ return ActivationState.ISLAND_SLEEPING;
+ case CONSTANTS.ACTIVATION_STATE.WANTS_DEACTIVATION:
+ return ActivationState.WANTS_DEACTIVATION;
+ }
+ return ActivationState.ACTIVE_TAG;
+};
+
+export const getTypeFromBodyType = (type: string) => {
+ switch (type) {
+ case "static":
+ return Type.STATIC;
+ case "dynamic":
+ return Type.DYNAMIC;
+ case "kinematic":
+ return Type.KINEMATIC;
+ }
+ return Type.KINEMATIC;
+};
+
+export const getBodyTypeFromType = (type: Type) => {
+ switch (type) {
+ case Type.STATIC:
+ return "static";
+ case Type.DYNAMIC:
+ return "dynamic";
+ case Type.KINEMATIC:
+ return "kinematic";
+ }
+ return "kinematic";
+};
+
export const getBodyFromRigidBody = (eid: number) => {
return {
mass: Rigidbody.mass[eid],
@@ -78,44 +143,156 @@ export const getBodyFromRigidBody = (eid: number) => {
y: Rigidbody.angularFactor[eid][1],
z: Rigidbody.angularFactor[eid][2]
},
- activationState: getActivationStateString(eid),
- emitCollisionEvents: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS,
- scaleAutoUpdate: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE,
+ activationState: getStringFromActivationState(eid),
+ emitCollisionEvents: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS) !== 0,
+ scaleAutoUpdate: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE) !== 0,
type: getTypeString(eid),
- disableCollision: Rigidbody.flags[eid] & RIGID_BODY_FLAGS.DISABLE_COLLISION,
+ disableCollision: (Rigidbody.flags[eid] & RIGID_BODY_FLAGS.DISABLE_COLLISION) !== 0,
collisionFilterGroup: Rigidbody.collisionFilterGroup[eid],
collisionFilterMask: Rigidbody.collisionFilterMask[eid]
};
};
-const updateRigidBody = (eid: number, params: RigidBodyParams) => {
- Rigidbody.type[eid] = params.type;
- Rigidbody.mass[eid] = params.mass;
- Rigidbody.gravity[eid].set(params.gravity);
- Rigidbody.linearDamping[eid] = params.linearDamping;
- Rigidbody.angularDamping[eid] = params.angularDamping;
- Rigidbody.linearSleepingThreshold[eid] = params.linearSleepingThreshold;
- Rigidbody.angularSleepingThreshold[eid] = params.angularSleepingThreshold;
- Rigidbody.angularFactor[eid].set(params.angularFactor);
- Rigidbody.activationState[eid] = params.activationState;
- params.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS);
- params.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION);
- Rigidbody.collisionFilterGroup[eid] = params.collisionGroup;
- Rigidbody.collisionFilterMask[eid] = params.collisionMask;
- params.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE);
+export const updateBodyParams = (eid: number, params: Partial) => {
+ const currentParams = getBodyFromRigidBody(eid);
+ const bodyParams = Object.assign({}, currentParams, params) as BodyParams;
+
+ Rigidbody.type[eid] = getTypeFromBodyType(bodyParams.type);
+ Rigidbody.mass[eid] = bodyParams.mass;
+ Rigidbody.gravity[eid].set([bodyParams.gravity.x, bodyParams.gravity.y, bodyParams.gravity.z]);
+ Rigidbody.linearDamping[eid] = bodyParams.linearDamping;
+ Rigidbody.angularDamping[eid] = bodyParams.angularDamping;
+ Rigidbody.linearSleepingThreshold[eid] = bodyParams.linearSleepingThreshold;
+ Rigidbody.angularSleepingThreshold[eid] = bodyParams.angularSleepingThreshold;
+ Rigidbody.angularFactor[eid].set([
+ bodyParams.angularFactor.x,
+ bodyParams.angularFactor.y,
+ bodyParams.angularFactor.z
+ ]);
+ Rigidbody.activationState[eid] = getActivationStateFromString(params.activationState!);
+ bodyParams.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS);
+ bodyParams.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION);
+ Rigidbody.collisionFilterGroup[eid] = bodyParams.collisionFilterGroup;
+ Rigidbody.collisionFilterMask[eid] = bodyParams.collisionFilterMask;
+ bodyParams.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE);
};
-export const updateRigiBodyParams = (eid: number, params: Partial) => {
- const currentParams = getBodyFromRigidBody(eid);
- const bodyParams = Object.assign({}, currentParams, params);
- updateRigidBody(eid, bodyParams);
+export const updateRigidBodyParams = (eid: number, params: Partial) => {
+ if (params.type !== undefined) {
+ Rigidbody.type[eid] = params.type;
+ }
+ if (params.mass !== undefined) {
+ Rigidbody.mass[eid] = params.mass;
+ }
+ if (params.gravity !== undefined) {
+ Rigidbody.gravity[eid].set(params.gravity);
+ }
+ if (params.linearDamping !== undefined) {
+ Rigidbody.linearDamping[eid] = params.linearDamping;
+ }
+ if (params.angularDamping !== undefined) {
+ Rigidbody.angularDamping[eid] = params.angularDamping;
+ }
+ if (params.linearSleepingThreshold !== undefined) {
+ Rigidbody.linearSleepingThreshold[eid] = params.linearSleepingThreshold;
+ }
+ if (params.angularSleepingThreshold !== undefined) {
+ Rigidbody.angularSleepingThreshold[eid] = params.angularSleepingThreshold;
+ }
+ if (params.angularFactor !== undefined) {
+ Rigidbody.angularFactor[eid].set(params.angularFactor);
+ }
+ if (params.activationState !== undefined) {
+ Rigidbody.activationState[eid] = params.activationState;
+ }
+ if (params.emitCollisionEvents !== undefined) {
+ Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS;
+ }
+ if (params.disableCollision !== undefined) {
+ Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION;
+ }
+ if (params.scaleAutoUpdate !== undefined) {
+ Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE;
+ }
+ if (params.collisionGroup !== undefined) {
+ Rigidbody.collisionFilterGroup[eid] = params.collisionGroup;
+ }
+ if (params.collisionMask !== undefined) {
+ Rigidbody.collisionFilterMask[eid] = params.collisionMask;
+ }
};
export function inflateRigidBody(world: HubsWorld, eid: number, params: Partial) {
- const bodyParams = Object.assign({}, DEFAULTS, params);
+ const bodyParams = Object.assign({}, DEFAULTS, params) as RigidBodyParams;
addComponent(world, Rigidbody, eid);
- updateRigidBody(eid, bodyParams);
+ addComponent(world, NetworkedRigidBody, eid);
+
+ Rigidbody.type[eid] = bodyParams.type;
+ Rigidbody.mass[eid] = bodyParams.mass;
+ Rigidbody.gravity[eid].set(bodyParams.gravity);
+ Rigidbody.linearDamping[eid] = bodyParams.linearDamping;
+ Rigidbody.angularDamping[eid] = bodyParams.angularDamping;
+ Rigidbody.linearSleepingThreshold[eid] = bodyParams.linearSleepingThreshold;
+ Rigidbody.angularSleepingThreshold[eid] = bodyParams.angularSleepingThreshold;
+ Rigidbody.angularFactor[eid].set(bodyParams.angularFactor);
+ Rigidbody.activationState[eid] = bodyParams.activationState;
+ bodyParams.emitCollisionEvents && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.EMIT_COLLISION_EVENTS);
+ bodyParams.disableCollision && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.DISABLE_COLLISION);
+ Rigidbody.collisionFilterGroup[eid] = bodyParams.collisionGroup;
+ Rigidbody.collisionFilterMask[eid] = bodyParams.collisionMask;
+ Rigidbody.initialCollisionFilterMask[eid] = bodyParams.collisionMask;
+ bodyParams.scaleAutoUpdate && (Rigidbody.flags[eid] |= RIGID_BODY_FLAGS.SCALE_AUTO_UPDATE);
+ NetworkedRigidBody.prevType[eid] = bodyParams.type;
+
+ return eid;
+}
+
+export enum GLTFRigidBodyType {
+ STATIC = "static",
+ DYNAMIC = "dynamic",
+ KINEMATIC = "kinematic"
+}
+
+export enum GLTFRigidBodyCollisionGroup {
+ OBJECTS = "objects",
+ ENVIRONMENT = "environment",
+ TRIGGERS = "triggers",
+ AVATARS = "avatars",
+ MEDIA_FRAMES = "media-frames"
+}
+
+const GLTF_DEFAULTS = {
+ ...DEFAULTS,
+ type: GLTFRigidBodyType.DYNAMIC,
+ collisionGroup: GLTFRigidBodyCollisionGroup.OBJECTS,
+ collisionMask: [GLTFRigidBodyCollisionGroup.AVATARS]
+};
+
+const gltfGroupToLayer = {
+ [GLTFRigidBodyCollisionGroup.OBJECTS]: COLLISION_LAYERS.INTERACTABLES,
+ [GLTFRigidBodyCollisionGroup.ENVIRONMENT]: COLLISION_LAYERS.ENVIRONMENT,
+ [GLTFRigidBodyCollisionGroup.TRIGGERS]: COLLISION_LAYERS.TRIGGERS,
+ [GLTFRigidBodyCollisionGroup.AVATARS]: COLLISION_LAYERS.AVATAR,
+ [GLTFRigidBodyCollisionGroup.MEDIA_FRAMES]: COLLISION_LAYERS.MEDIA_FRAMES
+} as const;
+
+export interface GLTFRigidBodyParams
+ extends Partial> {
+ type?: GLTFRigidBodyType;
+ collisionGroup?: GLTFRigidBodyCollisionGroup;
+ collisionMask?: GLTFRigidBodyCollisionGroup[];
+}
+
+export function inflateGLTFRigidBody(world: HubsWorld, eid: number, params: GLTFRigidBodyParams) {
+ const bodyParams = Object.assign({}, GLTF_DEFAULTS, params);
+
+ inflateRigidBody(world, eid, {
+ ...bodyParams,
+ type: Object.values(GLTFRigidBodyType).indexOf(bodyParams.type),
+ collisionGroup: gltfGroupToLayer[bodyParams.collisionGroup],
+ collisionMask: bodyParams.collisionMask.reduce((acc, m) => acc | gltfGroupToLayer[m], 0)
+ });
return eid;
}
diff --git a/src/inflators/spawner.ts b/src/inflators/spawner.ts
index 78c6be0310..1587fadb39 100644
--- a/src/inflators/spawner.ts
+++ b/src/inflators/spawner.ts
@@ -42,7 +42,6 @@ export function inflateSpawner(world: HubsWorld, eid: number, props: SpawnerPara
inflateRigidBody(world, eid, {
mass: 0,
- type: Type.STATIC,
collisionGroup: COLLISION_LAYERS.INTERACTABLES,
collisionMask: COLLISION_LAYERS.DEFAULT_SPAWNER
});
diff --git a/src/inflators/text.ts b/src/inflators/text.ts
index 418d7a0e43..cc7f38480c 100644
--- a/src/inflators/text.ts
+++ b/src/inflators/text.ts
@@ -1,10 +1,255 @@
import { addComponent } from "bitecs";
-import { BackSide, DoubleSide, FrontSide } from "three";
+import { BackSide, Color, DoubleSide, FrontSide, Side } from "three";
import { Text as TroikaText } from "troika-three-text";
import { HubsWorld } from "../app";
-import { TextTag } from "../bit-components";
+import { Networked, NetworkedText, TextTag } from "../bit-components";
import { addObject3DComponent } from "../utils/jsx-entity";
+export const ANCHOR_X = {
+ LEFT: 1 << 0,
+ CENTER: 1 << 1,
+ RIGHT: 1 << 2
+};
+
+export function anchorXToFlag(anchorX: string) {
+ switch (anchorX) {
+ case "center":
+ return ANCHOR_X.CENTER;
+ case "left":
+ return ANCHOR_X.LEFT;
+ case "right":
+ return ANCHOR_X.RIGHT;
+ }
+ return ANCHOR_X.CENTER;
+}
+
+export function flagToAnchorX(flag: number) {
+ switch (flag) {
+ case ANCHOR_X.CENTER:
+ return "center";
+ case ANCHOR_X.LEFT:
+ return "left";
+ case ANCHOR_X.RIGHT:
+ return "right";
+ }
+ return "center";
+}
+
+export const ANCHOR_Y = {
+ TOP: 1 << 0,
+ TOP_BASELINE: 1 << 1,
+ TOP_CAP: 1 << 2,
+ TOP_EX: 1 << 3,
+ MIDDLE: 1 << 4,
+ BOTTOM_BASELINE: 1 << 5,
+ BOTTOM: 1 << 6
+};
+
+export function anchorYToFlag(anchorY: string) {
+ switch (anchorY) {
+ case "top":
+ return ANCHOR_Y.TOP;
+ case "top-baseline":
+ return ANCHOR_Y.TOP_BASELINE;
+ case "top-cap":
+ return ANCHOR_Y.TOP_CAP;
+ case "top-ex":
+ return ANCHOR_Y.TOP_EX;
+ case "middle":
+ return ANCHOR_Y.MIDDLE;
+ case "bottom-baseline":
+ return ANCHOR_Y.BOTTOM_BASELINE;
+ case "bottom":
+ return ANCHOR_Y.BOTTOM;
+ }
+ return ANCHOR_Y.MIDDLE;
+}
+
+export function flagToAnchorY(flag: number) {
+ switch (flag) {
+ case ANCHOR_Y.TOP:
+ return "top";
+ case ANCHOR_Y.TOP_BASELINE:
+ return "top-baseline";
+ case ANCHOR_Y.TOP_CAP:
+ return "top-cap";
+ case ANCHOR_Y.TOP_EX:
+ return "top-ex";
+ case ANCHOR_Y.MIDDLE:
+ return "middle";
+ case ANCHOR_Y.BOTTOM_BASELINE:
+ return "bottom-baseline";
+ case ANCHOR_Y.BOTTOM:
+ return "bottom";
+ }
+ return "middle";
+}
+
+export const DIRECTION = {
+ AUTO: 1 << 0,
+ LTR: 1 << 1,
+ RTL: 1 << 2
+};
+
+export function directionToFlag(direction: string) {
+ switch (direction) {
+ case "auto":
+ return DIRECTION.AUTO;
+ case "ltr":
+ return DIRECTION.LTR;
+ case "rtl":
+ return DIRECTION.RTL;
+ }
+ return DIRECTION.AUTO;
+}
+
+export function flagToDirection(flag: number) {
+ switch (flag) {
+ case DIRECTION.AUTO:
+ return "auto";
+ case DIRECTION.LTR:
+ return "ltr";
+ case DIRECTION.RTL:
+ return "rtl";
+ }
+ return "auto";
+}
+
+export const OVERFLOW_WRAP = {
+ NORMAL: 1 << 0,
+ BREAK_WORD: 1 << 1
+};
+
+export function overflowWrapToFlag(overflowWrap: string) {
+ switch (overflowWrap) {
+ case "normal":
+ return OVERFLOW_WRAP.NORMAL;
+ case "break-word":
+ return OVERFLOW_WRAP.BREAK_WORD;
+ }
+ return OVERFLOW_WRAP.NORMAL;
+}
+
+export function flagToOverflowWrap(flag: number) {
+ switch (flag) {
+ case OVERFLOW_WRAP.NORMAL:
+ return "normal";
+ case OVERFLOW_WRAP.BREAK_WORD:
+ return "break-word";
+ }
+ return "normal";
+}
+
+export const SIDE = {
+ FRONT: 1 << 0,
+ BACK: 1 << 1,
+ DOUBLE: 1 << 2
+};
+
+export function sideToFlag(side: string) {
+ switch (side) {
+ case "front":
+ return SIDE.FRONT;
+ case "back":
+ return SIDE.BACK;
+ case "double":
+ return SIDE.DOUBLE;
+ }
+ return SIDE.FRONT;
+}
+
+export function flagToSide(flag: number) {
+ switch (flag) {
+ case SIDE.FRONT:
+ return "front";
+ case SIDE.BACK:
+ return "back";
+ case SIDE.DOUBLE:
+ return "double";
+ }
+ return "front";
+}
+
+export const TEXT_ALIGN = {
+ LEFT: 1 << 0,
+ RIGHT: 1 << 1,
+ CENTER: 1 << 2,
+ JUSTIFY: 1 << 2
+};
+
+export function textAlignToFlag(textAlign: string) {
+ switch (textAlign) {
+ case "left":
+ return TEXT_ALIGN.LEFT;
+ case "right":
+ return TEXT_ALIGN.RIGHT;
+ case "center":
+ return TEXT_ALIGN.CENTER;
+ case "justify":
+ return TEXT_ALIGN.JUSTIFY;
+ }
+ return TEXT_ALIGN.CENTER;
+}
+
+export function flagToTextAlign(flag: number) {
+ switch (flag) {
+ case TEXT_ALIGN.LEFT:
+ return "left";
+ case TEXT_ALIGN.RIGHT:
+ return "right";
+ case TEXT_ALIGN.CENTER:
+ return "center";
+ case TEXT_ALIGN.JUSTIFY:
+ return "justify";
+ }
+ return "center";
+}
+
+export const WHITESPACE = {
+ NORMAL: 1 << 0,
+ NO_WRAP: 1 << 1
+};
+
+export function whiteSpaceToFlag(whiteSpace: string) {
+ switch (whiteSpace) {
+ case "normal":
+ return WHITESPACE.NORMAL;
+ case "nowrap":
+ return WHITESPACE.NO_WRAP;
+ }
+ return WHITESPACE.NORMAL;
+}
+
+export function flagToWhiteSpace(flag: number) {
+ switch (flag) {
+ case WHITESPACE.NORMAL:
+ return "normal";
+ case WHITESPACE.NO_WRAP:
+ return "nowrap";
+ }
+ return "normal";
+}
+
+export function numberOrStringToString(value: number | string) {
+ if (isNaN(Number(value))) {
+ return APP.getSid(value as string);
+ } else {
+ return APP.getSid(`${value as number}`);
+ }
+}
+
+export function stringToNumberOrString(value: string): number | string {
+ if (!value) return 0;
+ if (value.indexOf("%") !== -1) {
+ return value;
+ } else {
+ return Number(value);
+ }
+}
+
+export type NumberOrNormalT = number | "normal";
+export type NumberOrPctT = number | `${number}%`;
+
export type TextParams = {
value: string;
anchorX?: "left" | "center" | "right";
@@ -13,39 +258,53 @@ export type TextParams = {
color?: string;
curveRadius?: number;
depthOffset?: number;
- direction?: "auto" | "ltr" | "trl";
+ direction?: "auto" | "ltr" | "rtl";
fillOpacity?: number;
fontUrl?: string | null;
fontSize?: number;
glyphGeometryDetail?: number;
gpuAccelerateSDF?: boolean;
letterSpacing?: number;
- lineHeight?: number | "normal";
+ lineHeight?: NumberOrNormalT;
maxWidth?: number;
opacity?: number;
- outlineBlur?: number | `${number}%`;
+ outlineBlur?: NumberOrPctT;
outlineColor?: string;
- outlineOffsetX?: number | `${number}%`;
- outlineOffsetY?: number | `${number}%`;
+ outlineOffsetX?: NumberOrPctT;
+ outlineOffsetY?: NumberOrPctT;
outlineOpacity?: number;
- outlineWidth?: number | `${number}%`;
+ outlineWidth?: NumberOrPctT;
overflowWrap?: "normal" | "break-word";
sdfGlyphSize?: number | null;
side?: "front" | "back" | "double";
strokeColor?: string;
strokeOpacity?: number;
- strokeWidth?: number | `${number}%`;
+ strokeWidth?: NumberOrPctT;
textAlign?: "left" | "right" | "center" | "justify";
textIndent?: number;
whiteSpace?: "normal" | "nowrap";
};
-const THREE_SIDES = {
+export const THREE_SIDES = {
front: FrontSide,
back: BackSide,
double: DoubleSide
};
+export function sideToThree(side: "front" | "back" | "double"): Side {
+ return THREE_SIDES[side];
+}
+
+export const THREE_TO_SIDE = {
+ [FrontSide]: "front",
+ [BackSide]: "back",
+ [DoubleSide]: "double"
+};
+
+export function threeToSide(side: Side) {
+ return THREE_TO_SIDE[side];
+}
+
const DEFAULTS: Required = {
anchorX: "center",
anchorY: "middle",
@@ -92,7 +351,7 @@ const DEFAULTS: Required = {
// glTF. If we notice problems with other parameters, we may add
// casting of other parameters.
// TODO: Add a generic mechanism to cast and validate inflator params.
-const cast = (params: Required): Required => {
+export const cast = (params: Required): Required => {
const keys: Array = [
"curveRadius",
"depthOffset",
@@ -132,8 +391,8 @@ const cast = (params: Required): Required => {
return params;
};
-export function inflateText(world: HubsWorld, eid: number, params: TextParams) {
- const requiredParams = cast(Object.assign({}, DEFAULTS, params) as Required);
+const tmpColor = new Color();
+function createText(requiredParams: Required) {
const text = new TroikaText();
text.material!.toneMapped = false;
@@ -145,7 +404,7 @@ export function inflateText(world: HubsWorld, eid: number, params: TextParams) {
text.anchorX = requiredParams.anchorX;
text.anchorY = requiredParams.anchorY;
text.clipRect = requiredParams.clipRect;
- text.color = requiredParams.color;
+ text.color = tmpColor.set(requiredParams.color).getHex();
text.curveRadius = requiredParams.curveRadius;
text.depthOffset = requiredParams.depthOffset;
text.direction = requiredParams.direction;
@@ -173,6 +432,52 @@ export function inflateText(world: HubsWorld, eid: number, params: TextParams) {
text.sync();
+ return text;
+}
+
+export function inflateText(world: HubsWorld, eid: number, params: TextParams) {
+ const requiredParams = Object.assign({}, DEFAULTS, params) as Required;
+ const text = createText(requiredParams);
+
+ addComponent(world, TextTag, eid);
+ addObject3DComponent(world, eid, text);
+}
+
+export function inflateGLTFText(world: HubsWorld, eid: number, params: TextParams) {
+ const requiredParams = Object.assign({}, DEFAULTS, params) as Required;
+ const text = createText(requiredParams);
+
addComponent(world, TextTag, eid);
+ addComponent(world, Networked, eid);
+ addComponent(world, NetworkedText, eid);
addObject3DComponent(world, eid, text);
+
+ NetworkedText.text[eid] = APP.getSid(requiredParams.value);
+ NetworkedText.fontSize[eid] = requiredParams.fontSize;
+ NetworkedText.textAlign[eid] = textAlignToFlag(requiredParams.textAlign);
+ NetworkedText.anchorX[eid] = anchorXToFlag(requiredParams.anchorX);
+ NetworkedText.anchorY[eid] = anchorYToFlag(requiredParams.anchorY);
+ NetworkedText.color[eid] = tmpColor.set(requiredParams.color).getHex();
+ NetworkedText.letterSpacing[eid] = requiredParams.letterSpacing;
+ NetworkedText.lineHeight[eid] = numberOrStringToString(requiredParams.lineHeight);
+ NetworkedText.outlineWidth[eid] = numberOrStringToString(requiredParams.outlineWidth);
+ NetworkedText.outlineColor[eid] = tmpColor.set(requiredParams.outlineColor).getHex();
+ NetworkedText.outlineBlur[eid] = numberOrStringToString(requiredParams.outlineBlur);
+ NetworkedText.outlineOffsetX[eid] = numberOrStringToString(requiredParams.outlineOffsetX);
+ NetworkedText.outlineOffsetY[eid] = numberOrStringToString(requiredParams.outlineOffsetY);
+ NetworkedText.outlineOpacity[eid] = requiredParams.outlineOpacity;
+ NetworkedText.fillOpacity[eid] = requiredParams.fillOpacity;
+ NetworkedText.strokeWidth[eid] = numberOrStringToString(requiredParams.strokeWidth);
+ NetworkedText.strokeColor[eid] = tmpColor.set(requiredParams.strokeColor).getHex();
+ NetworkedText.strokeOpacity[eid] = requiredParams.strokeOpacity;
+ NetworkedText.textIndent[eid] = requiredParams.textIndent;
+ NetworkedText.whiteSpace[eid] = whiteSpaceToFlag(requiredParams.whiteSpace);
+ NetworkedText.overflowWrap[eid] = overflowWrapToFlag(requiredParams.overflowWrap);
+ NetworkedText.opacity[eid] = requiredParams.opacity;
+ NetworkedText.side[eid] = sideToFlag(requiredParams.side);
+ NetworkedText.maxWidth[eid] = requiredParams.maxWidth;
+ NetworkedText.curveRadius[eid] = requiredParams.curveRadius;
+ NetworkedText.direction[eid] = directionToFlag(requiredParams.direction);
+
+ return eid;
}
diff --git a/src/inflators/video.ts b/src/inflators/video.ts
index 8e087390bb..3bfec5aa50 100644
--- a/src/inflators/video.ts
+++ b/src/inflators/video.ts
@@ -2,13 +2,16 @@ import { create360ImageMesh, createImageMesh } from "../utils/create-image-mesh"
import { addComponent } from "bitecs";
import { addObject3DComponent } from "../utils/jsx-entity";
import { ProjectionMode } from "../utils/projection-mode";
-import { MediaVideo, MediaVideoData } from "../bit-components";
+import { MediaVideo, MediaVideoData, NetworkedVideo } from "../bit-components";
import { HubsWorld } from "../app";
import { EntityID } from "../utils/networking-types";
import { Texture } from "three";
export const VIDEO_FLAGS = {
- CONTROLS: 1 << 0
+ CONTROLS: 1 << 0,
+ AUTO_PLAY: 1 << 1,
+ LOOP: 1 << 2,
+ PAUSED: 1 << 3
};
export interface VideoParams {
diff --git a/src/link.js b/src/link.js
index c6fc89f052..beb641e3f7 100644
--- a/src/link.js
+++ b/src/link.js
@@ -10,10 +10,11 @@ import LinkRoot from "./react-components/link-root";
import LinkChannel from "./utils/link-channel";
import { connectToReticulum } from "./utils/phoenix-utils";
import { ThemeProvider } from "./react-components/styles/theme";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
registerTelemetry("/link", "Hubs Device Link");
+const store = getStore();
const linkChannel = new LinkChannel(store);
(async () => {
diff --git a/src/load-media-on-paste-or-drop.ts b/src/load-media-on-paste-or-drop.ts
index 1d41d0fafe..17b48448fe 100644
--- a/src/load-media-on-paste-or-drop.ts
+++ b/src/load-media-on-paste-or-drop.ts
@@ -107,6 +107,7 @@ function onDrop(e: DragEvent) {
if (qsTruthy("debugLocalScene")) {
URL.revokeObjectURL(lastDebugScene);
if (!e.dataTransfer?.files.length) return;
+ e.preventDefault();
const url = URL.createObjectURL(e.dataTransfer.files[0]);
APP.hubChannel!.updateScene(url);
lastDebugScene = url;
diff --git a/src/message-dispatch.js b/src/message-dispatch.js
index d555db8208..59cf6f884d 100644
--- a/src/message-dispatch.js
+++ b/src/message-dispatch.js
@@ -11,7 +11,6 @@ import { createNetworkedEntity } from "./utils/create-networked-entity";
import { add, testAsset, respawn } from "./utils/chat-commands";
import { isLockedDownDemoRoom } from "./utils/hub-utils";
import { loadState, clearState } from "./utils/entity-state-utils";
-import { shouldUseNewLoader } from "./utils/bit-utils";
let uiRoot;
// Handles user-entered messages
@@ -24,6 +23,15 @@ export default class MessageDispatch extends EventTarget {
this.remountUI = remountUI;
this.mediaSearchStore = mediaSearchStore;
this.presenceLogEntries = [];
+ this.chatCommands = new Map();
+ }
+
+ registerChatCommand(name, callback) {
+ if (!this.chatCommands.has(name)) {
+ this.chatCommands.set(name, callback);
+ } else {
+ throw Error(`Error registering chat command ${name}: command already registered`);
+ }
}
addToPresenceLog(entry) {
@@ -140,22 +148,6 @@ export default class MessageDispatch extends EventTarget {
this.scene.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_QUACK);
}
break;
- case "duck":
- if (shouldUseNewLoader()) {
- const avatarPov = document.querySelector("#avatar-pov-node").object3D;
- const eid = createNetworkedEntity(APP.world, "duck");
- const obj = APP.world.eid2obj.get(eid);
- obj.position.copy(avatarPov.localToWorld(new THREE.Vector3(0, 0, -1.5)));
- obj.lookAt(avatarPov.getWorldPosition(new THREE.Vector3()));
- } else {
- spawnChatMessage(getAbsoluteHref(location.href, ducky));
- }
- if (Math.random() < 0.01) {
- this.scene.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_SPECIAL_QUACK);
- } else {
- this.scene.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_QUACK);
- }
- break;
case "cube": {
const avatarPov = document.querySelector("#avatar-pov-node").object3D;
const eid = createNetworkedEntity(APP.world, "cube");
@@ -269,5 +261,9 @@ export default class MessageDispatch extends EventTarget {
}
break;
}
+
+ if (this.chatCommands.has(command)) {
+ this.chatCommands.get(command)(APP, args);
+ }
};
}
diff --git a/src/prefabs/duck.tsx b/src/prefabs/duck.tsx
deleted file mode 100644
index 380053c7d0..0000000000
--- a/src/prefabs/duck.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/** @jsx createElementEntity */
-import { createElementEntity, EntityDef } from "../utils/jsx-entity";
-import { COLLISION_LAYERS } from "../constants";
-import { FLOATY_OBJECT_FLAGS } from "../systems/floaty-object-system";
-import ducky from "../assets/models/DuckyMesh.glb";
-import { getAbsoluteHref } from "../utils/media-url-utils";
-import { Fit, Shape } from "../inflators/physics-shape";
-
-export function DuckPrefab(): EntityDef {
- return (
-
- );
-}
diff --git a/src/prefabs/loading-object.js b/src/prefabs/loading-object.js
index 68c488725a..673c799535 100644
--- a/src/prefabs/loading-object.js
+++ b/src/prefabs/loading-object.js
@@ -5,6 +5,7 @@ import { createElementEntity } from "../utils/jsx-entity";
import { loadModel } from "../components/gltf-model-plus";
import loadingObjectSrc from "../assets/models/LoadingObject_Atom.glb";
import { cloneObject3D, disposeNode } from "../utils/three-utils";
+import { LOOP_ANIMATION_DEFAULTS } from "../inflators/loop-animation";
// TODO We should have an explicit "preload assets" step
let loadingObject = new Mesh(new BoxGeometry(), new MeshBasicMaterial());
@@ -16,5 +17,12 @@ loadModel(loadingObjectSrc, null, true).then(gltf => {
// TODO: Do we really need to clone the loadingObject every time?
// Should we use a pool?
export function LoadingObject() {
- return ;
+ return (
+
+ );
}
diff --git a/src/prefabs/prefabs.ts b/src/prefabs/prefabs.ts
index 1584653614..915da578f6 100644
--- a/src/prefabs/prefabs.ts
+++ b/src/prefabs/prefabs.ts
@@ -1,14 +1,8 @@
-import { MediaLoaderParams } from "../inflators/media-loader";
import { CameraPrefab, CubeMediaFramePrefab } from "../prefabs/camera-tool";
import { MediaPrefab } from "../prefabs/media";
-import { EntityDef } from "../utils/jsx-entity";
-import { DuckPrefab } from "./duck";
+import { PrefabDefinitionT, PrefabNameT } from "../types";
-type CameraPrefabT = () => EntityDef;
-type CubeMediaPrefabT = () => EntityDef;
-type MediaPrefabT = (params: MediaLoaderParams) => EntityDef;
-
-type Permission =
+export type Permission =
| "spawn_camera"
| "spawn_and_move_media"
| "update_hub"
@@ -22,15 +16,7 @@ type Permission =
| "kick_users"
| "mute_users";
-export type PrefabDefinition = {
- permission: Permission;
- template: CameraPrefabT | CubeMediaPrefabT | MediaPrefabT;
-};
-
-export type PrefabName = "camera" | "cube" | "media" | "duck";
-
-export const prefabs = new Map();
+export const prefabs = new Map();
prefabs.set("camera", { permission: "spawn_camera", template: CameraPrefab });
prefabs.set("cube", { permission: "spawn_and_move_media", template: CubeMediaFramePrefab });
prefabs.set("media", { permission: "spawn_and_move_media", template: MediaPrefab });
-prefabs.set("duck", { permission: "spawn_and_move_media", template: DuckPrefab });
diff --git a/src/react-components/debug-panel/ECSSidebar.js b/src/react-components/debug-panel/ECSSidebar.js
index 1793990ad8..e76246d6e3 100644
--- a/src/react-components/debug-panel/ECSSidebar.js
+++ b/src/react-components/debug-panel/ECSSidebar.js
@@ -70,9 +70,28 @@ function MaterialItem(props) {
);
}
+function TextureItem(props) {
+ const { tex, setSelectedObj } = props;
+ const displayName = formatObjectName(tex);
+ return (
+
+
{
+ e.preventDefault();
+ setSelectedObj(tex);
+ }}
+ >
+ {displayName}
+ {` [${tex.eid}]`}
+
+
+ );
+}
+
export function formatComponentProps(eid, component) {
const formatted = Object.keys(component).reduce((str, k, i, arr) => {
- const val = component[k][eid];
+ const val = component[k] instanceof Map ? component[k].get(eid) : component[k][eid];
const isStr = component[k][bitComponents.$isStringType];
str += ` ${k}: `;
if (ArrayBuffer.isView(val)) {
@@ -144,8 +163,10 @@ function RefreshButton({ onClick }) {
);
}
+export const extraSections = new Array();
const object3dQuery = defineQuery([bitComponents.Object3DTag]);
const materialQuery = defineQuery([bitComponents.MaterialTag]);
+const textureQuery = defineQuery([bitComponents.TextureTag]);
function ECSDebugSidebar({
onClose,
toggleObjExpand,
@@ -159,6 +180,8 @@ function ECSDebugSidebar({
.map(eid => APP.world.eid2obj.get(eid))
.filter(o => !o.parent);
const materials = materialQuery(APP.world).map(eid => APP.world.eid2mat.get(eid));
+ const textures = textureQuery(APP.world).map(eid => APP.world.eid2tex.get(eid));
+ const envRoot = document.getElementById("environment-root").object3D;
return (
+
{orphaned.map(o => (
- {materials.map(m => (
-
- ))}
+
+
+
+ {materials.map(m => m && )}
+
+
+
+
+
+ {textures.map(t => t && )}
+ {extraSections.map(section => section(APP.world, setSelectedObj))}
{selectedObj && }
diff --git a/src/react-components/home/HomePage.js b/src/react-components/home/HomePage.js
index bbe3480de2..691b6e110d 100644
--- a/src/react-components/home/HomePage.js
+++ b/src/react-components/home/HomePage.js
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import classNames from "classnames";
import configs from "../../utils/configs";
@@ -28,12 +28,13 @@ export function HomePage() {
const { results: favoriteRooms } = useFavoriteRooms();
const { results: publicRooms } = usePublicRooms();
+ const qs = useMemo(() => new URLSearchParams(location.search), []);
+ const [newScene] = useState(qs.has("new"));
+
const sortedFavoriteRooms = Array.from(favoriteRooms).sort((a, b) => b.member_count - a.member_count);
const sortedPublicRooms = Array.from(publicRooms).sort((a, b) => b.member_count - a.member_count);
const wrapInBold = chunk => {chunk} ;
useEffect(() => {
- const qs = new URLSearchParams(location.search);
-
// Support legacy sign in urls.
if (qs.has("sign_in")) {
const redirectUrl = new URL("/signin", window.location);
@@ -49,142 +50,144 @@ export function HomePage() {
qs.delete("new");
createAndRedirectToNewHub(null, null, true, qs);
}
- }, []);
+ }, [qs]);
const canCreateRooms = !configs.feature("disable_room_creation") || auth.isAdmin;
const email = auth.email;
return (
-
-
-
- {auth.isSignedIn ? (
-
-
-
-
-
-
-
+ !newScene && (
+
+
+
+ {auth.isSignedIn ? (
+
+ ) : (
+
+ )}
+
- ) : (
-
- )}
-
-
-
{configs.translation("app-description")}
- {canCreateRooms &&
}
-
-
-
-
-
-
-
- {configs.feature("show_feature_panels") && (
-
-
-
-
-
-
-
-
+ {configs.translation("app-description")}
+ {canCreateRooms && }
+
+
+
+
-
-
-
-
-
-
+
+
+
+ {configs.feature("show_feature_panels") && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ {sortedPublicRooms.length > 0 && (
+
+
+
-
-
-
-
-
-
-
-
+
+
+ {sortedPublicRooms.map(room => {
+ return (
+
+ scaledThumbnailUrlFor(entry.images.preview.url, width, height)
+ }
+ />
+ );
+ })}
+
+
+
+ )}
+ {sortedFavoriteRooms.length > 0 && (
+
+
+
-
-
-
+
+
+ {sortedFavoriteRooms.map(room => {
+ return (
+
+ scaledThumbnailUrlFor(entry.images.preview.url, width, height)
+ }
+ />
+ );
+ })}
+
+
+
+ )}
+ {isHmc() ? (
+
+
-
- )}
- {sortedPublicRooms.length > 0 && (
-
-
-
-
-
-
- {sortedPublicRooms.map(room => {
- return (
-
- scaledThumbnailUrlFor(entry.images.preview.url, width, height)
- }
- />
- );
- })}
-
-
-
- )}
- {sortedFavoriteRooms.length > 0 && (
-
-
-
-
-
-
- {sortedFavoriteRooms.map(room => {
- return (
-
- scaledThumbnailUrlFor(entry.images.preview.url, width, height)
- }
- />
- );
- })}
-
-
-
- )}
- {isHmc() ? (
-
-
-
- ) : null}
-
+ ) : null}
+
+ )
);
}
diff --git a/src/react-components/preferences-screen.js b/src/react-components/preferences-screen.js
index 9d577c57c0..507568ce6b 100644
--- a/src/react-components/preferences-screen.js
+++ b/src/react-components/preferences-screen.js
@@ -27,6 +27,7 @@ import { isLockedDownDemoRoom } from "../utils/hub-utils";
import dropdownArrowUrl from "../assets/images/dropdown_arrow.png";
import dropdownArrow2xUrl from "../assets/images/dropdown_arrow@2x.png";
import { PermissionNotification } from "./room/PermissionNotifications";
+import { getAddonsPreferencesCategories, getAddonsPreferencesLabels } from "../addons";
export const CLIPPING_THRESHOLD_MIN = 0.0;
export const CLIPPING_THRESHOLD_MAX = 0.1;
@@ -582,12 +583,26 @@ class PreferenceListItem extends Component {
const isCheckbox = this.props.itemProps.prefType === PREFERENCE_LIST_ITEM_TYPE.CHECK_BOX;
const isCustomComponent = this.props.itemProps.prefType === PREFERENCE_LIST_ITEM_TYPE.CUSTOM_COMPONENT;
const isSmallScreen = window.innerWidth < 600;
- const label = preferenceLabels[this.props.storeKey] && (
+ let labelText;
+ if (preferenceLabels[this.props.storeKey]) {
+ labelText = intl.formatMessage(preferenceLabels[this.props.storeKey]);
+ } else {
+ const addonLabels = getAddonsPreferencesLabels();
+ labelText = addonLabels.get(this.props.storeKey);
+ }
+ let labelTooltip;
+ if (this.props.itemProps.tooltipKey) {
+ labelTooltip = intl.formatMessage(preferenceLabels[this.props.itemProps.tooltipKey]);
+ } else {
+ const addonLabels = getAddonsPreferencesLabels();
+ labelTooltip = addonLabels.get(this.props.storeKey);
+ }
+ const label = (
- {intl.formatMessage(preferenceLabels[this.props.storeKey])}
+ {labelText}
);
const prefSchema = this.props.store.schema.definitions.preferences.properties;
@@ -695,7 +710,8 @@ const CATEGORY_MOVEMENT = 3;
const CATEGORY_TOUCHSCREEN = 4;
const CATEGORY_ACCESSIBILITY = 5;
const CATEGORY_GRAPHICS = 6;
-const TOP_LEVEL_CATEGORIES = [CATEGORY_AUDIO, CATEGORY_CONTROLS, CATEGORY_MISC];
+const CATEGORY_ADDONS = 7;
+const TOP_LEVEL_CATEGORIES = [CATEGORY_AUDIO, CATEGORY_CONTROLS, CATEGORY_MISC, CATEGORY_ADDONS];
const categoryNames = defineMessages({
[CATEGORY_AUDIO]: { id: "preferences-screen.category.audio", defaultMessage: "Audio" },
[CATEGORY_CONTROLS]: { id: "preferences-screen.category.controls", defaultMessage: "Controls" },
@@ -703,7 +719,8 @@ const categoryNames = defineMessages({
[CATEGORY_MOVEMENT]: { id: "preferences-screen.category.movement", defaultMessage: "Movement" },
[CATEGORY_TOUCHSCREEN]: { id: "preferences-screen.category.touchscreen", defaultMessage: "Touchscreen" },
[CATEGORY_ACCESSIBILITY]: { id: "preferences-screen.category.accessibility", defaultMessage: "Accessibility" },
- [CATEGORY_GRAPHICS]: { id: "preferences-screen.category.graphics", defaultMessage: "Graphics" }
+ [CATEGORY_GRAPHICS]: { id: "preferences-screen.category.graphics", defaultMessage: "Graphics" },
+ [CATEGORY_ADDONS]: { id: "preferences-screen.category.add-ons", defaultMessage: "Add-Ons" }
});
function NavItem({ ariaLabel, title, onClick, selected }) {
@@ -1427,6 +1444,11 @@ class PreferencesScreen extends Component {
);
}
+ const addOnCategories = [];
+ getAddonsPreferencesCategories(APP).forEach((prefItems, prefCatName) => {
+ addOnCategories.push({ name: prefCatName, items: prefItems.map(toItem).filter(item => !!item) });
+ });
+
return new Map([
[
CATEGORY_AUDIO,
@@ -1460,7 +1482,8 @@ class PreferencesScreen extends Component {
items: items.get(CATEGORY_GRAPHICS)
}
]
- ]
+ ],
+ [CATEGORY_ADDONS, addOnCategories]
]);
}
diff --git a/src/react-components/room/ChatSidebar.js b/src/react-components/room/ChatSidebar.js
index 8495344336..b75ab7ddb0 100644
--- a/src/react-components/room/ChatSidebar.js
+++ b/src/react-components/room/ChatSidebar.js
@@ -327,6 +327,8 @@ export function formatSystemMessage(entry, intl) {
values={{ hubName: {entry.hubName} }}
/>
);
+ case "script_message":
+ return "script: " + entry.msg;
case "log":
return intl.formatMessage(logMessages[entry.messageType], entry.props);
default:
diff --git a/src/react-components/room/RoomSettingsSidebar.js b/src/react-components/room/RoomSettingsSidebar.js
index 319350dc74..6edc8dd18f 100644
--- a/src/react-components/room/RoomSettingsSidebar.js
+++ b/src/react-components/room/RoomSettingsSidebar.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useCallback, useEffect, useState } from "react";
import PropTypes from "prop-types";
import { useForm } from "react-hook-form";
import styles from "./RoomSettingsSidebar.scss";
@@ -20,6 +20,8 @@ import { canShare, shareInviteUrl } from "../../utils/share";
import { ReactComponent as ShareIcon } from "../icons/Share.svg";
import { Checkbox } from "@mozilla/lilypad-ui";
import configs from "../../utils/configs";
+import { addons, isAddonEnabled } from "../../addons";
+import { shouldUseNewLoader } from "../../hubs";
export function RoomSettingsSidebar({
showBackButton,
@@ -58,6 +60,22 @@ export function RoomSettingsSidebar({
const [isShareInEnglish, setIsShareInEnglish] = useState(false);
+ const handleAddonChange = useCallback(
+ evt => {
+ setValue(`user_data.addons.${evt.target.id}`, evt.target.checked);
+ },
+ [setValue]
+ );
+
+ const [bitECSLoaderEnabled, setBitECSLoaderEnabled] = useState(shouldUseNewLoader());
+ const handleBitECSChange = useCallback(
+ evt => {
+ setValue("user_data.hubs_use_bitecs_based_client", evt.target.checked);
+ setBitECSLoaderEnabled(evt.target.checked);
+ },
+ [setValue, setBitECSLoaderEnabled]
+ );
+
return (
}
@@ -245,15 +263,37 @@ export function RoomSettingsSidebar({
defaultMessage="Enable bitECS based Client"
/>
}
+ defaultChecked={shouldUseNewLoader()}
+ onChange={handleBitECSChange}
description={
}
- {...register("user_data.hubs_use_bitecs_based_client")}
/>
+ } fullWidth>
+ {!bitECSLoaderEnabled && (
+
+
+
+ )}
+ {[...addons.entries()].map(([id, addon]) => (
+
+ ))}
+
diff --git a/src/react-components/room/RoomSettingsSidebar.scss b/src/react-components/room/RoomSettingsSidebar.scss
index 720b13f2ad..ef318f534b 100644
--- a/src/react-components/room/RoomSettingsSidebar.scss
+++ b/src/react-components/room/RoomSettingsSidebar.scss
@@ -1,6 +1,7 @@
@use "../styles/theme.scss";
-:local(.room-permissions), :local(.permissions-group) {
+:local(.room-permissions),
+:local(.permissions-group) {
margin-left: 20px;
& > * {
@@ -15,4 +16,12 @@
:local(.confirm-revoke-button) {
display: inline;
color: theme.$link-color;
-}
\ No newline at end of file
+}
+
+:local(.label) {
+ margin-bottom: 8px;
+ color: theme.$red;
+ align-self: flex-start;
+ font-weight: theme.$font-weight-bold;
+ font-size: theme.$font-size-sm;
+}
diff --git a/src/react-components/room/RoomSidebar.js b/src/react-components/room/RoomSidebar.js
index 98438a0b1c..f722075a8b 100644
--- a/src/react-components/room/RoomSidebar.js
+++ b/src/react-components/room/RoomSidebar.js
@@ -70,12 +70,12 @@ function SceneAttribution({ attribution }) {
}
SceneAttribution.propTypes = {
- attribution: {
+ attribution: PropTypes.shape({
name: PropTypes.string,
title: PropTypes.string,
author: PropTypes.string,
url: PropTypes.string
- }
+ })
};
// To assist with content control, we avoid displaying scene links to users who are not the scene
diff --git a/src/react-components/room/contexts/ChatContext.tsx b/src/react-components/room/contexts/ChatContext.tsx
index bb56c0f22e..6ba664e28b 100644
--- a/src/react-components/room/contexts/ChatContext.tsx
+++ b/src/react-components/room/contexts/ChatContext.tsx
@@ -81,6 +81,7 @@ function updateMessageGroups(messageGroups: any[], newMessage: NewMessageT) {
case "scene_changed":
case "hub_name_changed":
case "hub_changed":
+ case "script_message":
case "log":
return [
...messageGroups,
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index c1dc959dbe..56f64ee280 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -153,6 +153,7 @@ class UIRoot extends Component {
initialIsFavorited: PropTypes.bool,
showSignInDialog: PropTypes.bool,
showBitECSBasedClientRefreshPrompt: PropTypes.bool,
+ showAddonRefreshPrompt: PropTypes.bool,
signInMessage: PropTypes.object,
onContinueAfterSignIn: PropTypes.func,
showSafariMicDialog: PropTypes.bool,
@@ -1705,6 +1706,14 @@ class UIRoot extends Component {
/>
)}
+ {this.props.showAddonRefreshPrompt && (
+
+
+
+ )}
);
diff --git a/src/scene.js b/src/scene.js
index acd784692b..aab334b4e9 100644
--- a/src/scene.js
+++ b/src/scene.js
@@ -10,7 +10,7 @@ import registerTelemetry from "./telemetry";
import { disableiOSZoom } from "./utils/disable-ios-zoom";
import { connectToReticulum, fetchReticulumAuthenticatedWithToken } from "./utils/phoenix-utils";
import "./utils/theme";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
function mountUI(props = {}) {
const container = document.getElementById("ui-root");
@@ -89,6 +89,7 @@ function onReady() {
disableiOSZoom();
+ const store = getStore();
const sceneId = parseSceneId(document.location);
console.log(`Scene ID: ${sceneId}`);
remountUI({ sceneId, store });
diff --git a/src/schema.toml b/src/schema.toml
index f36c360117..495c52c1f9 100644
--- a/src/schema.toml
+++ b/src/schema.toml
@@ -36,6 +36,8 @@ features.disable_room_creation = { category = "rooms", type = "boolean", name =
features.require_account_for_join = { category = "rooms", type = "boolean", name = "Require accounts for room access", description = "Require accounts for accessing rooms." }
features.default_room_size = { category = "rooms", type = "number", name = "Default room size", description = "Default room size for new rooms. This does not include users in the lobby." }
features.max_room_size = { category = "rooms", type = "number", name = "Maximum room size", description = "Maximum room size visitors can set." }
+features.bitecs_loader = { category = "features", type="boolean", name="Use BitECS loader", description="Use the BitECS based loader by default in all rooms" }
+features.addons_config = { category = "features", type="json", name="Add-ons config JSON", description="Add-ons config JSON file" }
features.show_feature_panels = { category = "features", type = "boolean", internal = "true" }
features.show_join_us_dialog = { category = "features", type = "boolean", internal = "true" }
@@ -92,5 +94,5 @@ links.promotion = { category = "links", type = "string", name = "Promotion Info"
links.remixing = { category = "links", type = "string", name = "Remixing Info", description = "Link to info about remixing info and licensing."}
links.model_collection = { category = "links", type = "string", name = "Model Collection", description = "Link to a collection of recommended models."}
-auth.login_subject = { category = "auth", type="string", name="Magic Link Email Subject", description="Customize the email subject line for users logging in" }
+login_subject = { category = "auth", type="string", name="Magic Link Email Subject", description="Customize the email subject line for users logging in" }
auth.login_body = { category = "auth", type="longstring", name="Magic Link Email Body", description="Customize message. Add '{{ link }}' to insert the magic link, otherwise it will be appended at the end." }
diff --git a/src/signin.js b/src/signin.js
index 2337e18c5b..122abed7cc 100644
--- a/src/signin.js
+++ b/src/signin.js
@@ -10,10 +10,11 @@ import "./react-components/styles/global.scss";
import "./assets/stylesheets/globals.scss";
import { Center } from "./react-components/layout/Center";
import { ThemeProvider } from "./react-components/styles/theme";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
registerTelemetry("/signin", "Hubs Sign In Page");
+const store = getStore();
window.APP = { store };
function SignInRoot() {
diff --git a/src/systems/bit-constraints-system.js b/src/systems/bit-constraints-system.js
index 1fa79aea1a..d75ed8a7b1 100644
--- a/src/systems/bit-constraints-system.js
+++ b/src/systems/bit-constraints-system.js
@@ -19,8 +19,10 @@ import {
ConstraintHandLeft,
ConstraintHandRight,
ConstraintRemoteLeft,
- ConstraintRemoteRight
+ ConstraintRemoteRight,
+ NetworkedRigidBody
} from "../bit-components";
+import { Type, getBodyFromRigidBody, getBodyTypeFromType } from "../inflators/rigid-body";
const queryRemoteRight = defineQuery([HeldRemoteRight, OffersRemoteConstraint]);
const queryEnterRemoteRight = enterQuery(queryRemoteRight);
@@ -45,7 +47,7 @@ function add(world, physicsSystem, interactor, constraintComponent, entities) {
for (let i = 0; i < entities.length; i++) {
const eid = findAncestorEntity(world, entities[i], ancestor => hasComponent(world, Rigidbody, ancestor));
if (!entityExists(world, eid)) continue;
- physicsSystem.updateRigidBodyOptions(eid, grabBodyOptions);
+ physicsSystem.updateRigidBody(eid, grabBodyOptions);
physicsSystem.addConstraint(interactor, Rigidbody.bodyId[eid], Rigidbody.bodyId[interactor], {});
addComponent(world, Constraint, eid);
addComponent(world, constraintComponent, eid);
@@ -57,8 +59,16 @@ function remove(world, offersConstraint, constraintComponent, physicsSystem, int
const eid = findAncestorEntity(world, entities[i], ancestor => hasComponent(world, Rigidbody, ancestor));
if (!entityExists(world, eid)) continue;
if (hasComponent(world, offersConstraint, entities[i]) && hasComponent(world, Rigidbody, eid)) {
- physicsSystem.updateRigidBodyOptions(eid, releaseBodyOptions);
+ physicsSystem.updateRigidBody(eid, {
+ type: getBodyTypeFromType(NetworkedRigidBody.prevType[eid]),
+ ...releaseBodyOptions
+ });
physicsSystem.removeConstraint(interactor);
+ if (Rigidbody.type[eid] === Type.DYNAMIC) {
+ physicsSystem.activateBody(Rigidbody.bodyId[eid]);
+ // This shouldn't be necessary but for some reason it doesn't activate the body if we don't update the body afterwards
+ physicsSystem.updateRigidBody(eid, getBodyFromRigidBody(eid));
+ }
removeComponent(world, constraintComponent, eid);
if (
!hasComponent(world, ConstraintHandLeft, eid) &&
diff --git a/src/systems/bit-media-frames.js b/src/systems/bit-media-frames.js
index f56ed3ad79..c5f57b5d18 100644
--- a/src/systems/bit-media-frames.js
+++ b/src/systems/bit-media-frames.js
@@ -19,29 +19,31 @@ import {
MediaContentBounds,
MediaFrame,
MediaImage,
- MediaLoaded,
MediaPDF,
MediaVideo,
Networked,
NetworkedMediaFrame,
Owned,
- Rigidbody
+ Rigidbody,
+ Holdable
} from "../bit-components";
import { MediaType } from "../utils/media-utils";
import { cloneObject3D, disposeNode, setMatrixWorld } from "../utils/three-utils";
import { takeOwnership } from "../utils/take-ownership";
import { takeSoftOwnership } from "../utils/take-soft-ownership";
-import { findAncestorWithComponent, findChildWithComponent } from "../utils/bit-utils";
+import { findAncestorWithComponent, findChildWithComponent, findChildrenWithComponent } from "../utils/bit-utils";
import { addObject3DComponent } from "../utils/jsx-entity";
import { updateMaterials } from "../utils/material-utils";
import { MEDIA_FRAME_FLAGS, AxisAlignType } from "../inflators/media-frame";
import { Matrix4, NormalBlending, Quaternion, RGBAFormat, Vector3 } from "three";
+import { COLLISION_LAYERS } from "../constants";
+import { HOLDABLE_FLAGS } from "../inflators/holdable";
const EMPTY_COLOR = 0x6fc0fd;
const HOVER_COLOR = 0x2f80ed;
const FULL_COLOR = 0x808080;
-const mediaFramesQuery = defineQuery([MediaFrame]);
+const mediaFramesQuery = defineQuery([MediaFrame, NetworkedMediaFrame]);
const enteredMediaFramesQuery = enterQuery(mediaFramesQuery);
const exitedMediaFramesQuery = exitQuery(mediaFramesQuery);
@@ -54,11 +56,19 @@ function mediaTypeMaskFor(world, eid) {
mediaTypeMask |= el.components["media-image"] && MediaType.IMAGE;
mediaTypeMask |= el.components["media-pdf"] && MediaType.PDF;
} else {
- const mediaEid = findChildWithComponent(world, MediaLoaded, eid);
- mediaTypeMask |= hasComponent(world, GLTFModel, mediaEid) && MediaType.MODEL;
- mediaTypeMask |= hasComponent(world, MediaVideo, mediaEid) && MediaType.VIDEO;
- mediaTypeMask |= hasComponent(world, MediaImage, mediaEid) && MediaType.IMAGE;
- mediaTypeMask |= hasComponent(world, MediaPDF, mediaEid) && MediaType.PDF;
+ const rigidBody = findAncestorWithComponent(world, Rigidbody, eid);
+ if (rigidBody && Rigidbody.collisionFilterMask[rigidBody] & COLLISION_LAYERS.MEDIA_FRAMES) {
+ const interactable = findChildWithComponent(world, Holdable, eid);
+ if (interactable) {
+ mediaTypeMask |= hasComponent(world, GLTFModel, interactable) && MediaType.MODEL;
+ mediaTypeMask |= hasComponent(world, MediaVideo, interactable) && MediaType.VIDEO;
+ mediaTypeMask |= hasComponent(world, MediaImage, interactable) && MediaType.IMAGE;
+ mediaTypeMask |= hasComponent(world, MediaPDF, interactable) && MediaType.PDF;
+ if (mediaTypeMask === 0) {
+ mediaTypeMask |= MediaType.MODEL;
+ }
+ }
+ }
}
return mediaTypeMask;
}
@@ -320,40 +330,68 @@ export function mediaFramesSystem(world, physicsSystem) {
const isFrameDeleting = findAncestorWithComponent(world, Deleting, frame);
const isFrameOwned = hasComponent(world, Owned, frame);
- if (capturedEid && isCapturedOwned && !isCapturedHeld && !isFrameDeleting && isCapturedColliding) {
- snapToFrame(world, frame, capturedEid);
- physicsSystem.updateRigidBodyOptions(capturedEid, { type: "kinematic" });
- } else if (
- (isFrameOwned && MediaFrame.capturedNid[frame] && world.deletedNids.has(MediaFrame.capturedNid[frame])) ||
- (capturedEid && isCapturedOwned && !isCapturedColliding) ||
- isFrameDeleting
- ) {
- takeOwnership(world, frame);
- NetworkedMediaFrame.capturedNid[frame] = 0;
- NetworkedMediaFrame.scale[frame].set(zero);
- // TODO BUG: If an entity I do not own is capturedEid by the media frame,
- // and then I take ownership of the entity (by grabbing it),
- // the physics system does not immediately notice the entity isCapturedColliding with the frame,
- // so I immediately think the frame should be emptied.
- } else if (isFrameOwned && MediaFrame.capturedNid[frame] && !capturedEid) {
+ if (!hasComponent(world, Owned, frame)) {
+ if (MediaFrame.flags[frame] !== NetworkedMediaFrame.flags[frame]) {
+ MediaFrame.flags[frame] = NetworkedMediaFrame.flags[frame];
+ }
+ }
+
+ if (!hasComponent(world, Owned, frame)) {
+ if (MediaFrame.mediaType[frame] !== NetworkedMediaFrame.mediaType[frame]) {
+ MediaFrame.mediaType[frame] = NetworkedMediaFrame.mediaType[frame];
+ }
+ }
+
+ if (capturedEid) {
+ const grabbables = findChildrenWithComponent(world, Holdable, capturedEid);
+ if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.LOCKED) {
+ grabbables.forEach(eid => (Holdable.flags[eid] &= ~HOLDABLE_FLAGS.ENABLED));
+ } else {
+ grabbables.forEach(eid => (Holdable.flags[eid] |= HOLDABLE_FLAGS.ENABLED));
+ }
+ }
+
+ if ((MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) === 0) {
NetworkedMediaFrame.capturedNid[frame] = 0;
NetworkedMediaFrame.scale[frame].set(zero);
- } else if (!NetworkedMediaFrame.capturedNid[frame]) {
- const capturable = getCapturableEntity(world, physicsSystem, frame);
- if (
- capturable &&
- (hasComponent(world, Owned, capturable) || (isOwnedByRet(world, capturable) && isFrameOwned)) &&
- !findChildWithComponent(world, Held, capturable) &&
- !inOtherFrame(world, frame, capturable)
+ }
+
+ if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) {
+ if (capturedEid && isCapturedOwned && !isCapturedHeld && !isFrameDeleting && isCapturedColliding) {
+ snapToFrame(world, frame, capturedEid);
+ physicsSystem.updateRigidBody(capturedEid, { type: "kinematic" });
+ } else if (
+ (isFrameOwned && MediaFrame.capturedNid[frame] && world.deletedNids.has(MediaFrame.capturedNid[frame])) ||
+ (capturedEid && isCapturedOwned && !isCapturedColliding) ||
+ isFrameDeleting
) {
takeOwnership(world, frame);
- takeOwnership(world, capturable);
- NetworkedMediaFrame.capturedNid[frame] = Networked.id[capturable];
- const obj = world.eid2obj.get(capturable);
- obj.updateMatrices();
- tmpVec3.setFromMatrixScale(obj.matrixWorld).toArray(NetworkedMediaFrame.scale[frame]);
- snapToFrame(world, frame, capturable);
- physicsSystem.updateRigidBodyOptions(capturable, { type: "kinematic" });
+ NetworkedMediaFrame.capturedNid[frame] = 0;
+ NetworkedMediaFrame.scale[frame].set(zero);
+ // TODO BUG: If an entity I do not own is capturedEid by the media frame,
+ // and then I take ownership of the entity (by grabbing it),
+ // the physics system does not immediately notice the entity isCapturedColliding with the frame,
+ // so I immediately think the frame should be emptied.
+ } else if (isFrameOwned && MediaFrame.capturedNid[frame] && !capturedEid) {
+ NetworkedMediaFrame.capturedNid[frame] = 0;
+ NetworkedMediaFrame.scale[frame].set(zero);
+ } else if (!NetworkedMediaFrame.capturedNid[frame]) {
+ const capturable = getCapturableEntity(world, physicsSystem, frame);
+ if (
+ capturable &&
+ (hasComponent(world, Owned, capturable) || (isOwnedByRet(world, capturable) && isFrameOwned)) &&
+ !findChildWithComponent(world, Held, capturable) &&
+ !inOtherFrame(world, frame, capturable)
+ ) {
+ takeOwnership(world, frame);
+ takeOwnership(world, capturable);
+ NetworkedMediaFrame.capturedNid[frame] = Networked.id[capturable];
+ const obj = world.eid2obj.get(capturable);
+ obj.updateMatrices();
+ tmpVec3.setFromMatrixScale(obj.matrixWorld).toArray(NetworkedMediaFrame.scale[frame]);
+ snapToFrame(world, frame, capturable);
+ physicsSystem.updateRigidBody(capturable, { type: "kinematic" });
+ }
}
}
@@ -366,12 +404,14 @@ export function mediaFramesSystem(world, physicsSystem) {
// TODO: If you are resetting scale because you lost a race for the frame,
// you should probably also move the object away from the frame.
setMatrixScale(world.eid2obj.get(capturedEid), MediaFrame.scale[frame]);
- physicsSystem.updateRigidBodyOptions(capturedEid, { type: "dynamic" });
+ physicsSystem.updateRigidBody(capturedEid, { type: "dynamic" });
}
MediaFrame.capturedNid[frame] = NetworkedMediaFrame.capturedNid[frame];
MediaFrame.scale[frame].set(NetworkedMediaFrame.scale[frame]);
- display(world, physicsSystem, frame, capturedEid, heldMediaTypes);
+ if (MediaFrame.flags[frame] & MEDIA_FRAME_FLAGS.ACTIVE) {
+ display(world, physicsSystem, frame, capturedEid, heldMediaTypes);
+ }
}
}
diff --git a/src/systems/bit-physics.ts b/src/systems/bit-physics.ts
index a07c325956..d9b891d2c4 100644
--- a/src/systems/bit-physics.ts
+++ b/src/systems/bit-physics.ts
@@ -1,10 +1,12 @@
import { defineQuery, enterQuery, entityExists, exitQuery, hasComponent, Not } from "bitecs";
-import { Object3DTag, Rigidbody, PhysicsShape, AEntity } from "../bit-components";
+import { Object3DTag, Rigidbody, PhysicsShape, AEntity, TextTag } from "../bit-components";
import { getShapeFromPhysicsShape } from "../inflators/physics-shape";
-import { findAncestorWithComponent } from "../utils/bit-utils";
+import { findAncestorWithComponent, hasAnyComponent } from "../utils/bit-utils";
import { getBodyFromRigidBody } from "../inflators/rigid-body";
import { HubsWorld } from "../app";
import { PhysicsSystem } from "./physics-system";
+import { EntityID } from "../utils/networking-types";
+import { Object3D } from "three";
const rigidbodyQuery = defineQuery([Rigidbody, Object3DTag, Not(AEntity)]);
const rigidbodyEnteredQuery = enterQuery(rigidbodyQuery);
@@ -13,23 +15,49 @@ const shapeQuery = defineQuery([PhysicsShape]);
const shapeEnterQuery = enterQuery(shapeQuery);
const shapeExitQuery = exitQuery(shapeQuery);
+// We don't want to add physics shape for some child entities
+// ie. If the object already has a physics shape we don't want to create another one.
+// If the object has a text component, we don't want to create a physics shape for it.
+const NO_PHYSICS_COMPONENTS = [Rigidbody, TextTag];
+
function addPhysicsShapes(world: HubsWorld, physicsSystem: PhysicsSystem, eid: number) {
const bodyId = PhysicsShape.bodyId[eid];
const obj = world.eid2obj.get(eid)!;
- const shape = getShapeFromPhysicsShape(eid);
- const shapeId = physicsSystem.addShapes(bodyId, obj, shape);
- PhysicsShape.shapeId[eid] = shapeId;
+
+ // Avoid adding physics shapes for child entities with specific components.
+ if (obj) {
+ const hidden = new Map();
+ obj.traverse((child: Object3D) => {
+ if (child.eid! !== eid && hasAnyComponent(world, NO_PHYSICS_COMPONENTS, child.eid!)) {
+ hidden.set(child.eid!, child.parent!.eid!);
+ }
+ });
+ for (let child of hidden.keys()) {
+ const childObj = world.eid2obj.get(child)!;
+ childObj.removeFromParent();
+ }
+
+ const shape = getShapeFromPhysicsShape(eid);
+ const shapeId = physicsSystem.addShapes(bodyId, obj, shape);
+ PhysicsShape.shapeId[eid] = shapeId;
+
+ hidden.forEach((parent: EntityID, child: EntityID) => {
+ const parentObj = world.eid2obj.get(parent)!;
+ const childObj = world.eid2obj.get(child)!;
+ parentObj.add(childObj);
+ });
+ }
}
export const physicsCompatSystem = (world: HubsWorld, physicsSystem: PhysicsSystem) => {
- rigidbodyEnteredQuery(world).forEach(eid => {
+ rigidbodyEnteredQuery(world).forEach((eid: EntityID) => {
const obj = world.eid2obj.get(eid);
const body = getBodyFromRigidBody(eid);
const bodyId = physicsSystem.addBody(obj, body);
Rigidbody.bodyId[eid] = bodyId;
});
- shapeEnterQuery(world).forEach(eid => {
+ shapeEnterQuery(world).forEach((eid: EntityID) => {
const bodyEid = findAncestorWithComponent(world, Rigidbody, eid);
if (bodyEid) {
PhysicsShape.bodyId[eid] = Rigidbody.bodyId[bodyEid];
@@ -39,9 +67,11 @@ export const physicsCompatSystem = (world: HubsWorld, physicsSystem: PhysicsSyst
}
});
- shapeExitQuery(world).forEach(eid => physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid]));
+ shapeExitQuery(world).forEach((eid: EntityID) =>
+ physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid])
+ );
- rigidbodyExitedQuery(world).forEach(eid => {
+ rigidbodyExitedQuery(world).forEach((eid: EntityID) => {
if (entityExists(world, eid) && hasComponent(world, PhysicsShape, eid)) {
physicsSystem.removeShapes(PhysicsShape.bodyId[eid], PhysicsShape.shapeId[eid]);
// The PhysicsShape is still on this entity!
diff --git a/src/systems/floaty-object-system.js b/src/systems/floaty-object-system.js
index f857fa8e70..82db35efc8 100644
--- a/src/systems/floaty-object-system.js
+++ b/src/systems/floaty-object-system.js
@@ -53,7 +53,7 @@ function makeKinematicOnRelease(world) {
const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem;
makeKinematicOnReleaseExitQuery(world).forEach(eid => {
if (!entityExists(world, eid) || !hasComponent(world, Owned, eid)) return;
- physicsSystem.updateRigidBodyOptions(eid, { type: "kinematic" });
+ physicsSystem.updateRigidBody(eid, { type: "kinematic" });
});
}
@@ -73,17 +73,17 @@ export const floatyObjectSystem = world => {
const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem;
enteredFloatyObjectsQuery(world).forEach(eid => {
- physicsSystem.updateRigidBodyOptions(eid, {
+ physicsSystem.updateRigidBody(eid, {
type: "kinematic",
gravity: { x: 0, y: 0, z: 0 }
});
});
enterHeldFloatyObjectsQuery(world).forEach(eid => {
- physicsSystem.updateRigidBodyOptions(eid, {
+ physicsSystem.updateRigidBody(eid, {
gravity: { x: 0, y: 0, z: 0 },
type: "dynamic",
- collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES
+ collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES | COLLISION_LAYERS.TRIGGERS
});
});
@@ -95,17 +95,17 @@ export const floatyObjectSystem = world => {
const bodyData = physicsSystem.bodyUuidToData.get(bodyId);
if (FloatyObject.flags[eid] & FLOATY_OBJECT_FLAGS.MODIFY_GRAVITY_ON_RELEASE) {
if (bodyData.linearVelocity < 1.85) {
- physicsSystem.updateRigidBodyOptions(eid, {
+ physicsSystem.updateRigidBody(eid, {
gravity: { x: 0, y: 0, z: 0 },
angularDamping: FloatyObject.flags[eid] & FLOATY_OBJECT_FLAGS.REDUCE_ANGULAR_FLOAT ? 0.89 : 0.5,
linearDamping: 0.95,
linearSleepingThreshold: 0.1,
angularSleepingThreshold: 0.1,
- collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES
+ collisionFilterMask: COLLISION_LAYERS.HANDS | COLLISION_LAYERS.MEDIA_FRAMES | COLLISION_LAYERS.TRIGGERS
});
addComponent(world, MakeStaticWhenAtRest, eid);
} else {
- physicsSystem.updateRigidBodyOptions(eid, {
+ physicsSystem.updateRigidBody(eid, {
gravity: { x: 0, y: FloatyObject.releaseGravity[eid], z: 0 },
angularDamping: 0.01,
linearDamping: 0.01,
@@ -131,7 +131,7 @@ export const floatyObjectSystem = world => {
const angle = Math.random() * Math.PI * 2;
const x = Math.cos(angle);
const z = Math.sin(angle);
- physicsSystem.updateRigidBodyOptions(eid, {
+ physicsSystem.updateRigidBody(eid, {
gravity: { x, y: force, z },
angularDamping: 0.01,
linearDamping: 0.01,
@@ -142,7 +142,7 @@ export const floatyObjectSystem = world => {
removeComponent(world, MakeStaticWhenAtRest, eid);
}
} else {
- physicsSystem.updateRigidBodyOptions(eid, {
+ physicsSystem.updateRigidBody(eid, {
collisionFilterMask: COLLISION_LAYERS.DEFAULT_INTERACTABLE,
gravity: { x: 0, y: -9.8, z: 0 }
});
diff --git a/src/systems/hold-system.js b/src/systems/hold-system.js
index 72316e506a..68ff4ae74a 100644
--- a/src/systems/hold-system.js
+++ b/src/systems/hold-system.js
@@ -13,14 +13,14 @@ import {
HeldHandLeft,
AEntity,
Networked,
- MediaLoader,
- Deletable
+ Rigidbody
} from "../bit-components";
import { canMove } from "../utils/permissions-utils";
import { canMove as canMoveEntity } from "../utils/bit-permissions-utils";
import { isPinned } from "../bit-systems/networking";
import { takeOwnership } from "../utils/take-ownership";
import { findAncestorWithComponents } from "../utils/bit-utils";
+import { HOLDABLE_FLAGS } from "../inflators/holdable";
const GRAB_REMOTE_RIGHT = paths.actions.cursor.right.grab;
const DROP_REMOTE_RIGHT = paths.actions.cursor.right.drop;
@@ -75,18 +75,22 @@ export function isAEntityPinned(world, eid) {
// Alternate solution: Simply recognize an entity as pinned if its any
// ancestor is pinned (in hold-system) unless there is a case that
// descendant entity under pinned entity wants to be grabbable.
+//
+// Update: As now we are supporting grabbable scene objects, we look for the
+// root holdable entity with a rigid body as the only rigid body
+// in the hierarchy should be at the root of the grabbable object.
function grab(world, userinput, queryHovered, held, grabPath) {
const hovered = queryHovered(world)[0];
- // Special path for Dropped/Pasted Media with new loader enabled. Check the comment above.
- const mediaRoot = findAncestorWithComponents(world, [Deletable, MediaLoader, Holdable], hovered);
- const target = mediaRoot ? mediaRoot : hovered;
+ const interactable = findAncestorWithComponents(world, [Holdable, Rigidbody], hovered);
+ const target = interactable ? interactable : hovered;
const isEntityPinned = isPinned(target) || isAEntityPinned(world, target);
if (
target &&
userinput.get(grabPath) &&
(!isEntityPinned || AFRAME.scenes[0].is("frozen")) &&
+ Holdable.flags[interactable] & HOLDABLE_FLAGS.ENABLED &&
hasPermissionToGrab(world, target)
) {
if (hasComponent(world, Networked, target)) {
diff --git a/src/systems/hubs-systems.ts b/src/systems/hubs-systems.ts
index 96a6e8c86f..2281f524fb 100644
--- a/src/systems/hubs-systems.ts
+++ b/src/systems/hubs-systems.ts
@@ -76,7 +76,6 @@ import { textSystem } from "../bit-systems/text";
import { audioTargetSystem } from "../bit-systems/audio-target-system";
import { scenePreviewCameraSystem } from "../bit-systems/scene-preview-camera-system";
import { linearTransformSystem } from "../bit-systems/linear-transform";
-import { quackSystem } from "../bit-systems/quack";
import { mixerAnimatableSystem } from "../bit-systems/mixer-animatable";
import { loopAnimationSystem } from "../bit-systems/loop-animation";
import { linkSystem } from "../bit-systems/link-system";
@@ -93,6 +92,8 @@ import { linkedPDFSystem } from "../bit-systems/linked-pdf-system";
import { inspectSystem } from "../bit-systems/inspect-system";
import { snapMediaSystem } from "../bit-systems/snap-media-system";
import { scaleWhenGrabbedSystem } from "../bit-systems/scale-when-grabbed-system";
+import { interactableSystem } from "../bit-systems/interactable-system";
+import { SystemConfigT } from "../types";
declare global {
interface Window {
@@ -198,6 +199,10 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene
aframeSystems[systemNames[i]].tick(t, dt);
}
+ APP.addon_systems.setup.forEach((systemConfig: SystemConfigT) => {
+ systemConfig.system(APP);
+ });
+
networkReceiveSystem(world);
onOwnershipLost(world);
sceneLoadingSystem(world, hubsSystems.environmentSystem, hubsSystems.characterController);
@@ -213,10 +218,13 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene
buttonSystems(world);
sfxButtonSystem(world, aframeSystems["hubs-systems"].soundEffectsSystem);
+ APP.addon_systems.prePhysics.forEach((systemConfig: SystemConfigT) => {
+ systemConfig.system(APP);
+ });
+
physicsCompatSystem(world, hubsSystems.physicsSystem);
hubsSystems.physicsSystem.tick(dt);
constraintsSystem(world, hubsSystems.physicsSystem);
- floatyObjectSystem(world);
hoverableVisualsSystem(world);
@@ -240,6 +248,8 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene
hubsSystems.hoverMenuSystem.tick();
hubsSystems.positionAtBorderSystem.tick();
hubsSystems.twoPointStretchingSystem.tick();
+ interactableSystem(world);
+ floatyObjectSystem(world);
hubsSystems.holdableButtonSystem.tick();
hubsSystems.hoverButtonSystem.tick();
@@ -281,7 +291,6 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene
hubsSystems.nameTagSystem.tick();
simpleWaterSystem(world);
linearTransformSystem(world);
- quackSystem(world);
followInFovSystem(world);
linkedMediaSystem(world);
linkedVideoSystem(world);
@@ -303,6 +312,10 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene
bitPenCompatSystem(world, aframeSystems["pen-tools"]);
snapMediaSystem(world, aframeSystems["hubs-systems"].soundEffectsSystem);
+ APP.addon_systems.postPhysics.forEach((systemConfig: SystemConfigT) => {
+ systemConfig.system(APP);
+ });
+
deleteEntitySystem(world, aframeSystems.userinput);
destroyAtExtremeDistanceSystem(world);
removeNetworkedObjectButtonSystem(world);
@@ -318,15 +331,28 @@ export function mainTick(xrFrame: XRFrame, renderer: WebGLRenderer, scene: Scene
networkDebugSystem(world, scene);
}
+ APP.addon_systems.beforeMatricesUpdate.forEach((systemConfig: SystemConfigT) => {
+ systemConfig.system(APP);
+ });
+
scene.updateMatrixWorld();
renderer.info.reset();
+
+ APP.addon_systems.beforeRender.forEach((systemConfig: SystemConfigT) => {
+ systemConfig.system(APP);
+ });
+
if (APP.fx.composer) {
APP.fx.composer.render();
} else {
renderer.render(scene, camera);
}
+ APP.addon_systems.afterRender.forEach((systemConfig: SystemConfigT) => {
+ systemConfig.system(APP);
+ });
+
// tock()s on components and system will fire here. (As well as any other time render() is called without unbinding onAfterRender)
// TODO inline invoking tocks instead of using onAfterRender registered in a-scene
}
diff --git a/src/systems/networked-transform.js b/src/systems/networked-transform.js
index 9a501c25d7..81ab689a6c 100644
--- a/src/systems/networked-transform.js
+++ b/src/systems/networked-transform.js
@@ -1,12 +1,24 @@
-import { addComponent, defineQuery, hasComponent } from "bitecs";
-import { LinearRotate, LinearScale, LinearTranslate, NetworkedTransform, Owned } from "../bit-components";
+import { addComponent, defineQuery, hasComponent, enterQuery } from "bitecs";
+import { LinearRotate, LinearScale, LinearTranslate, NetworkedTransform, Owned, Networked } from "../bit-components";
import { millisecondsBetweenTicks } from "../bit-systems/networking";
const query = defineQuery([NetworkedTransform]);
const tmpVec = new THREE.Vector3();
const tmpQuat = new THREE.Quaternion();
+
+const networkedTransformEnterQuery = enterQuery(query);
export function networkedTransformSystem(world) {
+ networkedTransformEnterQuery(world).forEach(eid => {
+ // If it's a scene object that has not been owned yet,
+ // we initialize its networked transform to the initial object transform.
+ if (Networked.creator[eid] === APP.getSid("scene") && Networked.owner[eid] === APP.getSid("reticulum")) {
+ const obj = world.eid2obj.get(eid);
+ NetworkedTransform.position[eid].set(obj.position.toArray());
+ NetworkedTransform.rotation[eid].set(obj.quaternion.toArray());
+ NetworkedTransform.scale[eid].set(obj.scale.toArray());
+ }
+ });
const ents = query(world);
for (let i = 0; i < ents.length; i++) {
const eid = ents[i];
diff --git a/src/systems/on-ownership-lost.js b/src/systems/on-ownership-lost.js
index ae055891c6..e392265ebf 100644
--- a/src/systems/on-ownership-lost.js
+++ b/src/systems/on-ownership-lost.js
@@ -1,4 +1,3 @@
-import { COLLISION_LAYERS } from "../constants";
import { exitQuery, defineQuery, removeComponent, hasComponent, entityExists } from "bitecs";
import {
Held,
@@ -13,7 +12,6 @@ import {
// TODO this seems wrong, nothing sets it back unless its a floaty object
const exitOwned = exitQuery(defineQuery([Owned]));
const componentsToRemove = [Held, HeldHandRight, HeldHandLeft, HeldRemoteRight, HeldRemoteLeft];
-const kinematicOptions = { type: "kinematic", collisionFilterMask: COLLISION_LAYERS.UNOWNED_INTERACTABLE };
export function onOwnershipLost(world) {
const physicsSystem = AFRAME.scenes[0].systems["hubs-systems"].physicsSystem;
@@ -27,7 +25,10 @@ export function onOwnershipLost(world) {
}
if (hasComponent(world, Rigidbody, eid)) {
- physicsSystem.updateRigidBodyOptions(eid, kinematicOptions);
+ physicsSystem.updateRigidBody(eid, {
+ type: "kinematic",
+ collisionFilterMask: Rigidbody.initialCollisionFilterMask[eid]
+ });
}
}
}
diff --git a/src/systems/physics-system.js b/src/systems/physics-system.js
index 1489cafad9..20e92fcc06 100644
--- a/src/systems/physics-system.js
+++ b/src/systems/physics-system.js
@@ -3,7 +3,7 @@ import { AmmoDebugConstants, DefaultBufferSize } from "ammo-debug-drawer";
import configs from "../utils/configs";
import ammoWasmUrl from "ammo.js/builds/ammo.wasm.wasm";
import { Rigidbody } from "../bit-components";
-import { updateRigiBodyParams } from "../inflators/rigid-body";
+import { updateBodyParams } from "../inflators/rigid-body";
const MESSAGE_TYPES = CONSTANTS.MESSAGE_TYPES,
TYPE = CONSTANTS.TYPE,
@@ -231,6 +231,7 @@ export class PhysicsSystem {
const bodyId = this.nextBodyUuid;
this.nextBodyUuid += 1;
+ object3D.updateMatrices();
this.workerHelpers.addBody(bodyId, object3D, options);
this.bodyUuidToData.set(bodyId, {
@@ -250,7 +251,7 @@ export class PhysicsSystem {
updateRigidBody(eid, options) {
const bodyId = Rigidbody.bodyId[eid];
- updateRigiBodyParams(eid, options);
+ updateBodyParams(eid, options);
if (this.bodyUuidToData.has(bodyId)) {
this.bodyUuidToData.get(bodyId).options = options;
this.workerHelpers.updateBody(bodyId, options);
@@ -259,18 +260,6 @@ export class PhysicsSystem {
}
}
- updateRigidBodyOptions(eid, options) {
- const bodyId = Rigidbody.bodyId[eid];
- updateRigiBodyParams(eid, options);
- const bodyData = this.bodyUuidToData.get(bodyId);
- if (!bodyData) {
- // TODO: Fix me.
- console.warn("updateBodyOptions called for invalid bodyId");
- return;
- }
- this.workerHelpers.updateBody(bodyId, Object.assign(this.bodyUuidToData.get(bodyId).options, options));
- }
-
removeBody(uuid) {
const bodyData = this.bodyUuidToData.get(uuid);
if (!bodyData) {
@@ -285,8 +274,10 @@ export class PhysicsSystem {
if (bodyData.isInitialized) {
delete this.indexToUuid[bodyData.index];
bodyData.collisions.forEach(otherId => {
- const otherData = this.bodyUuidToData.get(otherId).collisions;
- otherData.splice(otherData.indexOf(uuid), 1);
+ const collisions = this.bodyUuidToData.get(otherId)?.collisions;
+ // This can happen when removing multiple bodies in a frame
+ if (!collisions) return;
+ collisions.splice(collisions.indexOf(uuid), 1);
});
this.bodyUuids.splice(this.bodyUuids.indexOf(uuid), 1);
this.bodyUuidToData.delete(uuid);
diff --git a/src/systems/remove-object3D-system.js b/src/systems/remove-object3D-system.js
index a163834c5f..d7cc9dc04a 100644
--- a/src/systems/remove-object3D-system.js
+++ b/src/systems/remove-object3D-system.js
@@ -5,6 +5,7 @@ import {
GLTFModel,
LightTag,
MaterialTag,
+ TextureTag,
MediaFrame,
MediaImage,
MediaVideo,
@@ -93,6 +94,7 @@ const cleanupAudioDebugSystem = cleanupOnExit(NavMesh, eid => cleanupAudioDebugN
// which means we will remove each descendent from its parent.
const exitedObject3DQuery = exitQuery(defineQuery([Object3DTag]));
const exitedMaterialQuery = exitQuery(defineQuery([MaterialTag]));
+const exitedTextureQuery = exitQuery(defineQuery([TextureTag]));
export function removeObject3DSystem(world) {
function removeObjFromMap(eid) {
const o = world.eid2obj.get(eid);
@@ -104,6 +106,11 @@ export function removeObject3DSystem(world) {
world.eid2mat.delete(eid);
m.eid = 0;
}
+ function removeFromTexMap(eid) {
+ const m = world.eid2tex.get(eid);
+ world.eid2tex.delete(eid);
+ m.eid = 0;
+ }
// TODO write removeObject3DEntity to do this work up-front,
// keeping the scene graph consistent and avoiding the second exitedObject3DQuery in this system.
@@ -145,4 +152,5 @@ export function removeObject3DSystem(world) {
entities.forEach(removeObjFromMap);
exitedObject3DQuery(world).forEach(removeObjFromMap);
exitedMaterialQuery(world).forEach(removeFromMatMap);
+ exitedTextureQuery(world).forEach(removeFromTexMap);
}
diff --git a/src/systems/single-action-button-system.js b/src/systems/single-action-button-system.js
index 850b94eeca..28e389c724 100644
--- a/src/systems/single-action-button-system.js
+++ b/src/systems/single-action-button-system.js
@@ -1,4 +1,4 @@
-import { addComponent, defineQuery, hasComponent, removeComponent } from "bitecs";
+import { addComponent, defineQuery, enterQuery, hasComponent, removeComponent } from "bitecs";
import {
HoverButton,
HoveredHandLeft,
@@ -80,13 +80,16 @@ function applyTheme() {
textHoverColor: new THREE.Color(0xffffff)
};
}
-onThemeChanged(applyTheme);
-applyTheme();
const hoverComponents = [HoveredRemoteRight, HoveredRemoteLeft, HoveredHandRight, HoveredHandLeft];
const hoverButtonsQuery = defineQuery([HoverButton]);
+const hoverButtonsEnterQuery = enterQuery(hoverButtonsQuery);
function hoverButtonSystem(world) {
+ if (hoverButtonsEnterQuery(world).length > 0) {
+ onThemeChanged(applyTheme);
+ applyTheme();
+ }
hoverButtonsQuery(world).forEach(function (eid) {
const obj = world.eid2obj.get(eid);
const isHovered = hasAnyComponent(world, hoverComponents, eid);
diff --git a/src/systems/sound-effects-system.js b/src/systems/sound-effects-system.js
index 906594955d..b615dbb68c 100644
--- a/src/systems/sound-effects-system.js
+++ b/src/systems/sound-effects-system.js
@@ -54,6 +54,17 @@ function decodeAudioData(audioContext, arrayBuffer) {
});
}
+function load(system, url) {
+ let audioBufferPromise = system.loading.get(url);
+ if (!audioBufferPromise) {
+ audioBufferPromise = fetch(url)
+ .then(r => r.arrayBuffer())
+ .then(arrayBuffer => decodeAudioData(system.audioContext, arrayBuffer));
+ system.loading.set(url, audioBufferPromise);
+ }
+ return audioBufferPromise;
+}
+
export class SoundEffectsSystem {
constructor(scene) {
this.pendingAudioSourceNodes = [];
@@ -92,20 +103,10 @@ export class SoundEffectsSystem {
[SOUND_SPAWN_EMOJI, URL_SPAWN_EMOJI],
[SOUND_SPEAKER_TONE, URL_SPEAKER_TONE]
];
- const loading = new Map();
- const load = url => {
- let audioBufferPromise = loading.get(url);
- if (!audioBufferPromise) {
- audioBufferPromise = fetch(url)
- .then(r => r.arrayBuffer())
- .then(arrayBuffer => decodeAudioData(this.audioContext, arrayBuffer));
- loading.set(url, audioBufferPromise);
- }
- return audioBufferPromise;
- };
+ this.loading = new Map();
this.sounds = new Map();
soundsAndUrls.map(([sound, url]) => {
- load(url).then(audioBuffer => {
+ load(this, url).then(audioBuffer => {
this.sounds.set(sound, audioBuffer);
});
});
@@ -122,6 +123,20 @@ export class SoundEffectsSystem {
});
}
+ registerSound(url) {
+ return new Promise((resolve, reject) => {
+ load(this, url)
+ .then(audioBuffer => {
+ soundEnum++;
+ this.sounds.set(soundEnum, audioBuffer);
+ resolve({ id: soundEnum, url });
+ })
+ .catch(() => {
+ reject();
+ });
+ });
+ }
+
enqueueSound(sound, loop) {
if (this.isDisabled) return null;
const audioBuffer = this.sounds.get(sound);
diff --git a/src/systems/userinput/userinput.js b/src/systems/userinput/userinput.js
index 7ed593a746..4a9d0e492f 100644
--- a/src/systems/userinput/userinput.js
+++ b/src/systems/userinput/userinput.js
@@ -44,6 +44,9 @@ import { gamepadBindings } from "./bindings/generic-gamepad";
import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "../../utils/vr-caps-detect";
import { hackyMobileSafariTest } from "../../utils/detect-touchscreen";
import { ArrayBackedSet } from "./array-backed-set";
+import { addSetsToBindings } from "./bindings/utils";
+import { InputDeviceE } from "../../types";
+import deepmerge from "deepmerge";
function arrayContentsDiffer(a, b) {
if (a.length !== b.length) return true;
@@ -191,6 +194,25 @@ function computeExecutionStrategy(sortedBindings, masks, activeSets) {
return { actives, masked };
}
+const DeviceToBindingsMapping = {
+ [InputDeviceE.Cardboard]: cardboardUserBindings,
+ [InputDeviceE.Daydream]: daydreamUserBindings,
+ [InputDeviceE.Gamepad]: gamepadBindings,
+ [InputDeviceE.KeyboardMouse]: keyboardMouseUserBindings,
+ [InputDeviceE.OculusGo]: oculusGoUserBindings,
+ [InputDeviceE.OculusTouch]: oculusTouchUserBindings,
+ [InputDeviceE.TouchScreen]: touchscreenUserBindings,
+ [InputDeviceE.Vive]: viveUserBindings,
+ [InputDeviceE.WebXR]: webXRUserBindings,
+ [InputDeviceE.WindowsMixedReality]: wmrUserBindings,
+ [InputDeviceE.XboxController]: xboxControllerUserBindings,
+ [InputDeviceE.GearVR]: gearVRControllerUserBindings,
+ [InputDeviceE.ViveCosmos]: viveCosmosUserBindings,
+ [InputDeviceE.ViveFocusPlus]: viveFocusPlusUserBindings,
+ [InputDeviceE.ViveWand]: viveWandUserBindings,
+ [InputDeviceE.ValveIndex]: indexUserBindings
+};
+
AFRAME.registerSystem("userinput", {
get(path) {
if (!this.frame) return;
@@ -560,5 +582,20 @@ AFRAME.registerSystem("userinput", {
this.prevSortedBindings = this.sortedBindings;
this.maybeToggleXboxMapping();
+ },
+ registerPaths(newPaths) {
+ for (const path of newPaths) {
+ if (path.value in paths[path.type]) {
+ throw Error(`Path ${path.key} already registered`);
+ }
+ paths[path.type][path.value] = `/${path.type}/${path.value}`;
+ }
+ },
+ registerBindings(device, bindings) {
+ bindings = addSetsToBindings(bindings);
+ for (const key in bindings) {
+ DeviceToBindingsMapping[device][key] = deepmerge(DeviceToBindingsMapping[device][key], bindings[key]);
+ }
+ this.registeredMappingsChanged = true;
}
});
diff --git a/src/tokens.js b/src/tokens.js
index b300dcd13b..236e614f13 100644
--- a/src/tokens.js
+++ b/src/tokens.js
@@ -10,10 +10,11 @@ import "./react-components/styles/global.scss";
import { TokenPageLayout } from "./react-components/tokens/TokenPageLayout";
import configs from "./utils/configs";
import { ThemeProvider } from "./react-components/styles/theme";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
registerTelemetry("/tokens", "Backend API Tokens Page");
+const store = getStore();
window.APP = { store };
function TokensRoot() {
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000000..db174a5ff0
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,255 @@
+import { AScene, HubsSystems } from "aframe";
+import { App, HubsWorld } from "./app";
+import { Permission } from "./prefabs/prefabs";
+import { EntityDef } from "./utils/jsx-entity";
+import { NetworkSchema } from "./utils/network-schemas";
+import { EntityID } from "./utils/networking-types";
+import { IComponent } from "bitecs";
+
+export enum SystemOrderE {
+ Setup = 0,
+ PrePhysics = 100,
+ PostPhysics = 200,
+ BeforeMatricesUpdate = 300,
+ BeforeRender = 400,
+ AfterRender = 500
+}
+
+export enum PostProcessOrderE {
+ AfterScene = 0,
+ AfterBloom = 1,
+ AfterUI = 2,
+ AfterAA = 3
+}
+
+export type CoreSystemKeyT = keyof AScene["systems"];
+export type HubsSystemKeyT = keyof HubsSystems;
+export type SystemKeyT = CoreSystemKeyT | HubsSystemKeyT;
+
+export enum SystemsE {
+ PhysicsSystem = "physicsSystem",
+ AudioSystem = "audioSystem",
+ SoundEffectsSystem = "soundEffectsSystem",
+ CameraSystem = "cameraSystem",
+ CharacterControllerSystem = "characterController",
+ WaypointSystem = "waypointSystem",
+ UserInputSystem = "userinput",
+ NavMesh = "nav"
+}
+
+export interface SystemT {
+ (app: App): void;
+}
+
+export type ComponentDataT = {
+ [key: string]: any;
+};
+
+export interface InflatorT {
+ (world: HubsWorld, eid: EntityID, componentProps?: ComponentDataT): EntityID;
+}
+
+export interface InflatorParamT {
+ id: string;
+ inflator: InflatorT;
+}
+
+export type InflatorConfigT = {
+ common?: InflatorParamT;
+ jsx?: InflatorParamT;
+ gltf?: InflatorParamT;
+};
+
+export enum PermissionE {
+ SPAWN_CAMERA = "spawn_camera",
+ SPAWN_AND_MOVE_MEDIA = "spawn_and_move_media",
+ UPDATE_HUB = "update_hub",
+ PIN_OBJECTS = "pin_objects",
+ SPAWN_EMOJI = "spawn_emoji",
+ AMPLIFY_AUDIO = "amplify_audio",
+ FLY = "fly",
+ VOICE_CHAT = "voice_chat",
+ SPAWN_DRAWING = "spawn_drawing",
+ TWEET = "tweet",
+ KICK_USERS = "kick_users",
+ MUTE_USERS = "mute_users"
+}
+
+export type PrefabTemplateFn = (params: ComponentDataT) => EntityDef;
+export type PermissionT = Permission;
+export type PrefabNameT = string;
+export type PrefabDefinitionT = {
+ permission: Permission;
+ template: PrefabTemplateFn;
+};
+export interface PrefabConfigT {
+ id: PrefabNameT;
+ config: PrefabDefinitionT;
+}
+
+export type NetworkSchemaT = NetworkSchema;
+export interface NetworkSchemaConfigT {
+ component: IComponent;
+ schema: NetworkSchemaT;
+}
+export type SystemConfigT = { system: SystemT; order: number };
+
+export type ChatCommandCallbackFn = (app: App, args: string[]) => void;
+export interface ChatCommandConfigT {
+ id: string;
+ command: ChatCommandCallbackFn;
+}
+
+/**
+ * This has to be kept in sync with the preferences-screen PREFERENCE_LIST_ITEM_TYPE
+ */
+export enum PREFERENCE_LIST_ITEM_TYPE {
+ CHECK_BOX = 1,
+ SELECT = 2,
+ NUMBER_WITH_RANGE = 3,
+ MAX_RESOLUTION = 4,
+ MAP_COUNT = 5,
+ CUSTOM_COMPONENT = 6
+}
+
+export type PreferenceDefConfigT = {
+ type: "string" | "number" | "bool" | "object";
+ default: string | number | boolean | object | undefined;
+};
+
+export type PreferenceSelectT = {
+ value: string | number;
+ text: string;
+};
+
+export type PreferenceRangeT = {
+ min: number;
+ max: number;
+ step: number;
+ digits: number;
+};
+
+export type PreferenceUIConfigT =
+ | {
+ prefType: PREFERENCE_LIST_ITEM_TYPE.CHECK_BOX;
+ description: string;
+ promptForRefresh?: boolean;
+ hidden?: () => boolean;
+ disableIfFalse?: string;
+ }
+ | {
+ prefType: PREFERENCE_LIST_ITEM_TYPE.NUMBER_WITH_RANGE;
+ description: string;
+ promptForRefresh?: boolean;
+ hidden?: () => boolean;
+ disableIfFalse?: string;
+ min: number;
+ max: number;
+ step: number;
+ digits: number;
+ }
+ | {
+ prefType: PREFERENCE_LIST_ITEM_TYPE.SELECT;
+ description: string;
+ promptForRefresh?: boolean;
+ hidden?: () => boolean;
+ disableIfFalse?: string;
+ options?: PreferenceSelectT[];
+ }
+ | {
+ prefType: PREFERENCE_LIST_ITEM_TYPE.MAP_COUNT;
+ description: string;
+ promptForRefresh?: boolean;
+ hidden?: () => boolean;
+ disableIfFalse?: string;
+ defaultValue?: number;
+ text?: string;
+ };
+
+export type PreferencePrefsScreenItemT = { key: string | PreferenceUIConfigT };
+export type PreferencePrefsScreenCategory = Map;
+export type PreferenceScreenLabelT = Map;
+export type PreferenceScreenDefT = Map;
+
+export type PreferenceConfigT = {
+ [key: string]: {
+ prefDefinition: PreferenceDefConfigT;
+ prefConfig: PreferenceUIConfigT;
+ };
+};
+
+export type SoundDefT = {
+ id: number;
+ url: string;
+};
+
+export enum InputSetsE {
+ global = "global",
+ inputFocused = "inputFocused",
+ rightCursorHoveringOnPen = "rightCursorHoveringOnPen",
+ rightCursorHoveringOnCamera = "rightCursorHoveringOnCamera",
+ rightCursorHoveringOnInteractable = "rightCursorHoveringOnInteractable",
+ rightCursorHoveringOnUI = "rightCursorHoveringOnUI",
+ rightCursorHoveringOnVideo = "rightCursorHoveringOnVideo",
+ rightCursorHoveringOnNothing = "rightCursorHoveringOnNothing",
+ rightCursorHoldingPen = "rightCursorHoldingPen",
+ rightCursorHoldingCamera = "rightCursorHoldingCamera",
+ rightCursorHoldingInteractable = "rightCursorHoldingInteractable",
+ rightCursorHoldingUI = "rightCursorHoldingUI",
+ rightCursorHoldingNothing = "rightCursorHoldingNothing",
+ leftCursorHoveringOnPen = "leftCursorHoveringOnPen",
+ leftCursorHoveringOnCamera = "leftCursorHoveringOnCamera",
+ leftCursorHoveringOnInteractable = "leftCursorHoveringOnInteractable",
+ leftCursorHoveringOnUI = "leftCursorHoveringOnUI",
+ leftCursorHoveringOnVideo = "leftCursorHoveringOnVideo",
+ leftCursorHoveringOnNothing = "leftCursorHoveringOnNothing",
+ leftCursorHoldingPen = "leftCursorHoldingPen",
+ leftCursorHoldingCamera = "leftCursorHoldingCamera",
+ leftCursorHoldingInteractable = "leftCursorHoldingInteractable",
+ leftCursorHoldingUI = "leftCursorHoldingUI",
+ leftCursorHoldingNothing = "leftCursorHoldingNothing",
+ rightHandTeleporting = "rightHandTeleporting",
+ rightHandHoveringOnPen = "rightHandHoveringOnPen",
+ rightHandHoveringOnCamera = "rightHandHoveringOnCamera",
+ rightHandHoveringOnInteractable = "rightHandHoveringOnInteractable",
+ rightHandHoveringOnNothing = "rightHandHoveringOnNothing",
+ rightHandHoldingPen = "rightHandHoldingPen",
+ rightHandHoldingCamera = "rightHandHoldingCamera",
+ rightHandHoldingInteractable = "rightHandHoldingInteractable",
+ leftHandTeleporting = "leftHandTeleporting",
+ leftHandHoveringOnPen = "leftHandHoveringOnPen",
+ leftHandHoveringOnCamera = "leftHandHoveringOnCamera",
+ leftHandHoveringOnInteractable = "leftHandHoveringOnInteractable",
+ leftHandHoldingPen = "leftHandHoldingPen",
+ leftHandHoldingCamera = "leftHandHoldingCamera",
+ leftHandHoldingInteractable = "leftHandHoldingInteractable",
+ leftHandHoveringOnNothing = "leftHandHoveringOnNothing",
+ debugUserInput = "debugUserInput",
+ inspecting = "inspecting"
+}
+
+export enum InputPathsE {
+ noop = "noop",
+ actions = "actions",
+ haptics = "haptics",
+ device = "device"
+}
+
+export enum InputDeviceE {
+ Cardboard,
+ Daydream,
+ Gamepad,
+ KeyboardMouse,
+ OculusGo,
+ OculusTouch,
+ TouchScreen,
+ Vive,
+ WebXR,
+ WindowsMixedReality,
+ XboxController,
+ GearVR,
+ ViveCosmos,
+ ViveFocusPlus,
+ ViveWand,
+ ValveIndex
+}
diff --git a/src/utils/assign-network-ids.ts b/src/utils/assign-network-ids.ts
index ae5399bc8d..1c4aaba4ed 100644
--- a/src/utils/assign-network-ids.ts
+++ b/src/utils/assign-network-ids.ts
@@ -2,6 +2,7 @@ import { hasComponent } from "bitecs";
import { HubsWorld } from "../app";
import { Networked } from "../bit-components";
import { ClientID, EntityID, NetworkID } from "./networking-types";
+import { Material, Texture } from "three";
export function setNetworkedDataWithRoot(world: HubsWorld, rootNid: NetworkID, eid: EntityID, creator: ClientID) {
let i = 0;
@@ -12,6 +13,16 @@ export function setNetworkedDataWithRoot(world: HubsWorld, rootNid: NetworkID, e
i += 1;
}
});
+ i = 0;
+ world.eid2mat.forEach((mat: Material, matEid: EntityID) => {
+ setInitialNetworkedData(matEid, `${rootNid}.mat.${i}`, rootNid);
+ i += 1;
+ });
+ i = 0;
+ world.eid2tex.forEach((tex: Texture, texEid: EntityID) => {
+ setInitialNetworkedData(texEid, `${rootNid}.tex.${i}`, rootNid);
+ i += 1;
+ });
}
export function setNetworkedDataWithoutRoot(world: HubsWorld, rootNid: NetworkID, childEid: EntityID) {
diff --git a/src/utils/bit-utils.ts b/src/utils/bit-utils.ts
index 009fb52882..6a4dcab82c 100644
--- a/src/utils/bit-utils.ts
+++ b/src/utils/bit-utils.ts
@@ -5,6 +5,7 @@ import { HubsWorld } from "../app";
import { findAncestor, findAncestors, traverseSome } from "./three-utils";
import { EntityID } from "./networking-types";
import qsTruthy from "./qs_truthy";
+import configs from "./configs";
export type ElOrEid = EntityID | AElement;
@@ -25,8 +26,15 @@ export function hasAnyComponent(world: HubsWorld, components: Component[], eid:
return false;
}
-export function findAncestorEntity(world: HubsWorld, eid: number, predicate: (eid: number) => boolean) {
- const obj = findAncestor(world.eid2obj.get(eid)!, (o: Object3D) => !!(o.eid && predicate(o.eid))) as Object3D | null;
+export function findAncestorEntity(
+ world: HubsWorld,
+ eid: number,
+ predicate: (eid: number, world: HubsWorld) => boolean
+) {
+ const obj = findAncestor(
+ world.eid2obj.get(eid)!,
+ (o: Object3D) => !!(o.eid && predicate(o.eid, world))
+ ) as Object3D | null;
return obj && obj.eid!;
}
@@ -36,7 +44,7 @@ export function findAncestorEntities(world: HubsWorld, eid: number, predicate: (
}
export function findAncestorWithComponent(world: HubsWorld, component: Component, eid: number) {
- return findAncestorEntity(world, eid, otherId => hasComponent(world, component, otherId));
+ return findAncestorEntity(world, eid, (otherId, world) => hasComponent(world, component, otherId));
}
export function findAncestorsWithComponent(world: HubsWorld, component: Component, eid: number): EntityID[] {
@@ -69,7 +77,28 @@ export function findChildWithComponent(world: HubsWorld, component: Component, e
}
}
+export function findChildrenWithComponent(world: HubsWorld, component: Component, eid: number) {
+ const obj = world.eid2obj.get(eid);
+ if (obj) {
+ const childrenEids = new Array();
+ obj.traverse((otherObj: Object3D) => {
+ if (otherObj.eid && hasComponent(world, component, otherObj.eid)) {
+ childrenEids.push(otherObj.eid);
+ }
+ });
+ return childrenEids;
+ }
+}
+
const forceNewLoader = qsTruthy("newLoader");
export function shouldUseNewLoader() {
- return forceNewLoader || APP.hub?.user_data?.hubs_use_bitecs_based_client;
+ if (forceNewLoader === true) {
+ return true;
+ } else if (APP.hub?.user_data?.hubs_use_bitecs_based_client !== undefined) {
+ return APP.hub?.user_data?.hubs_use_bitecs_based_client;
+ } else if (configs.feature("bitecs_loader") !== undefined) {
+ return configs.feature("bitecs_loader");
+ } else {
+ return false;
+ }
}
diff --git a/src/utils/create-networked-entity.ts b/src/utils/create-networked-entity.ts
index 60ebeb05a8..1a9c817d7c 100644
--- a/src/utils/create-networked-entity.ts
+++ b/src/utils/create-networked-entity.ts
@@ -3,18 +3,19 @@ import { HubsWorld } from "../app";
import { Networked } from "../bit-components";
import { createMessageDatas } from "../bit-systems/networking";
import { MediaLoaderParams } from "../inflators/media-loader";
-import { PrefabName, prefabs } from "../prefabs/prefabs";
+import { prefabs } from "../prefabs/prefabs";
import { renderAsEntity } from "../utils/jsx-entity";
import { hasPermissionToSpawn } from "../utils/permissions";
import { takeOwnership } from "../utils/take-ownership";
import { setNetworkedDataWithRoot } from "./assign-network-ids";
import type { ClientID, InitialData, NetworkID } from "./networking-types";
+import { PrefabNameT } from "../types";
export function createNetworkedMedia(world: HubsWorld, initialData: MediaLoaderParams) {
return createNetworkedEntity(world, "media", initialData);
}
-export function createNetworkedEntity(world: HubsWorld, prefabName: PrefabName, initialData: InitialData) {
+export function createNetworkedEntity(world: HubsWorld, prefabName: PrefabNameT, initialData: InitialData) {
if (!hasPermissionToSpawn(NAF.clientId, prefabName))
throw new Error(`You do not have permission to spawn ${prefabName}`);
const nid = NAF.utils.createNetworkId();
@@ -25,7 +26,7 @@ export function createNetworkedEntity(world: HubsWorld, prefabName: PrefabName,
export function renderAsNetworkedEntity(
world: HubsWorld,
- prefabName: PrefabName,
+ prefabName: PrefabNameT,
initialData: InitialData,
nid: NetworkID,
creator: ClientID
diff --git a/src/utils/jsx-entity.ts b/src/utils/jsx-entity.ts
index 15e9e9c9f8..497bcfa054 100644
--- a/src/utils/jsx-entity.ts
+++ b/src/utils/jsx-entity.ts
@@ -36,7 +36,6 @@ import {
Billboard,
MaterialTag,
VideoTextureSource,
- Quack,
MixerAnimatableInitialize,
Inspectable,
ObjectMenu,
@@ -57,7 +56,7 @@ import { inflateLink, LinkParams } from "../inflators/link";
import { inflateLinkLoader, LinkLoaderParams } from "../inflators/link-loader";
import { inflateLoopAnimationInitialize, LoopAnimationParams } from "../inflators/loop-animation";
import { inflateSlice9 } from "../inflators/slice9";
-import { TextParams, inflateText } from "../inflators/text";
+import { TextParams, inflateGLTFText, inflateText } from "../inflators/text";
import {
BackgroundParams,
EnvironmentSettingsParams,
@@ -70,7 +69,6 @@ import { inflateSpawnpoint, inflateWaypoint, WaypointParams } from "../inflators
import { inflateReflectionProbe, ReflectionProbeParams } from "../inflators/reflection-probe";
import { HubsWorld } from "../app";
import { Group, Material, Object3D, Texture } from "three";
-import { AlphaMode } from "./create-image-mesh";
import { MediaLoaderParams } from "../inflators/media-loader";
import { preload } from "./preload";
import { DirectionalLightParams, inflateDirectionalLight } from "../inflators/directional-light";
@@ -78,7 +76,6 @@ import { AmbientLightParams, inflateAmbientLight } from "../inflators/ambient-li
import { HemisphereLightParams, inflateHemisphereLight } from "../inflators/hemisphere-light";
import { PointLightParams, inflatePointLight } from "../inflators/point-light";
import { SpotLightParams, inflateSpotLight } from "../inflators/spot-light";
-import { ProjectionMode } from "./projection-mode";
import { inflateSkybox, SkyboxParams } from "../inflators/skybox";
import { inflateSpawner, SpawnerParams } from "../inflators/spawner";
import { inflateVideoTextureTarget, VideoTextureTargetParams } from "../inflators/video-texture-target";
@@ -93,18 +90,19 @@ import { inflateAudioParams } from "../inflators/audio-params";
import { AudioSourceParams, inflateAudioSource } from "../inflators/audio-source";
import { AudioTargetParams, inflateAudioTarget } from "../inflators/audio-target";
import { PhysicsShapeParams, inflatePhysicsShape } from "../inflators/physics-shape";
-import { inflateRigidBody, RigidBodyParams } from "../inflators/rigid-body";
+import { inflateGLTFRigidBody, inflateRigidBody, RigidBodyParams } from "../inflators/rigid-body";
import { AmmoShapeParams, inflateAmmoShape } from "../inflators/ammo-shape";
import { BoxColliderParams, inflateBoxCollider } from "../inflators/box-collider";
import { inflateTrimesh } from "../inflators/trimesh";
import { HeightFieldParams, inflateHeightField } from "../inflators/heightfield";
import { inflateAudioSettings } from "../inflators/audio-settings";
-import { HubsVideoTexture } from "../textures/HubsVideoTexture";
import { inflateMediaLink, MediaLinkParams } from "../inflators/media-link";
import { inflateObjectMenuTarget, ObjectMenuTargetParams } from "../inflators/object-menu-target";
import { inflateObjectMenuTransform, ObjectMenuTransformParams } from "../inflators/object-menu-transform";
import { inflatePlane, PlaneParams } from "../inflators/plane";
import { FollowInFovParams, inflateFollowInFov } from "../inflators/follow-in-fov";
+import { ComponentDataT } from "../types";
+import { HoldableParams, inflateHoldable } from "../inflators/holdable";
preload(
new Promise(resolve => {
@@ -146,7 +144,7 @@ export type Attrs = {
};
export type EntityDef = {
- components: JSXComponentData;
+ components: ComponentDataT;
attrs: Attrs;
children: EntityDef[];
ref?: Ref;
@@ -156,10 +154,10 @@ function isReservedAttr(attr: string): attr is keyof Attrs {
return reservedAttrs.includes(attr);
}
-type ComponentFn = string | ((attrs: Attrs & JSXComponentData, children?: EntityDef[]) => EntityDef);
+type ComponentFn = string | ((attrs: Attrs & ComponentDataT, children?: EntityDef[]) => EntityDef);
export function createElementEntity(
tag: "entity" | ComponentFn,
- attrs: Attrs & JSXComponentData,
+ attrs: Attrs & ComponentDataT,
...children: EntityDef[]
): EntityDef {
attrs = attrs || {};
@@ -167,7 +165,7 @@ export function createElementEntity(
return tag(attrs, children);
} else if (tag === "entity") {
const outputAttrs: Attrs = {};
- const components: JSXComponentData & Attrs = {};
+ const components: ComponentDataT & Attrs = {};
let ref = undefined;
for (const attr in attrs) {
@@ -177,7 +175,7 @@ export function createElementEntity(
ref = attrs[attr];
} else {
// if jsx transformed the attr into attr: true, change it to attr: {}.
- const c = attr as keyof JSXComponentData;
+ const c = attr as keyof ComponentDataT;
components[c] = attrs[c] === true ? {} : attrs[c];
}
}
@@ -224,7 +222,7 @@ export function addMaterialComponent(world: HubsWorld, eid: number, mat: Materia
return eid;
}
-const createDefaultInflator = (C: Component, defaults = {}): InflatorFn => {
+export const createDefaultInflator = (C: Component, defaults = {}): InflatorFn => {
return (world, eid, componentProps) => {
componentProps = Object.assign({}, defaults, componentProps);
addComponent(world, C, eid, true);
@@ -258,13 +256,15 @@ export interface ComponentData {
hemisphereLight?: HemisphereLightParams;
pointLight?: PointLightParams;
spotLight?: SpotLightParams;
- grabbable?: GrabbableParams;
billboard?: { onlyY: boolean };
mirror?: MirrorParams;
audioZone?: AudioZoneParams;
audioParams?: AudioSettings;
mediaFrame?: any;
text?: TextParams;
+ networked?: any;
+ networkedTransform?: any;
+ grabbable?: GrabbableParams;
}
type OptionalParams = Partial | true;
@@ -302,14 +302,12 @@ export interface JSXComponentData extends ComponentData {
offersHandConstraint?: true;
singleActionButton?: true;
holdableButton?: true;
- holdable?: true;
+ holdable?: HoldableParams;
deletable?: true;
makeKinematicOnRelease?: true;
destroyAtExtremeDistance?: true;
- quack?: true;
// @TODO Define all the anys
- networked?: any;
textButton?: any;
hoverButton?: any;
hoverableVisuals?: any;
@@ -317,7 +315,6 @@ export interface JSXComponentData extends ComponentData {
physicsShape?: OptionalParams;
floatyObject?: any;
networkedFloatyObject?: any;
- networkedTransform?: any;
objectMenu?: {
backgroundRef: Ref;
pinButtonRef: Ref;
@@ -378,6 +375,7 @@ export interface JSXComponentData extends ComponentData {
objectMenuTransform?: OptionalParams;
objectMenuTarget?: OptionalParams;
plane?: PlaneParams;
+ text?: TextParams;
}
export interface GLTFComponentData extends ComponentData {
@@ -399,6 +397,11 @@ export interface GLTFComponentData extends ComponentData {
audioTarget: AudioTargetParams;
audioSettings: SceneAudioSettings;
mediaLink: MediaLinkParams;
+ rigidbody?: OptionalParams;
+ // TODO GLTFPhysicsShapeParams
+ physicsShape?: AmmoShapeParams;
+ text?: TextParams;
+ grabbable?: GrabbableParams;
// deprecated
spawnPoint?: true;
@@ -416,7 +419,7 @@ export interface GLTFComponentData extends ComponentData {
declare global {
namespace createElementEntity.JSX {
interface IntrinsicElements {
- entity: JSXComponentData &
+ entity: ComponentDataT &
Attrs & {
children?: IntrinsicElements[];
};
@@ -428,7 +431,7 @@ declare global {
}
}
-export const commonInflators: Required<{ [K in keyof ComponentData]: InflatorFn }> = {
+export const commonInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = {
grabbable: inflateGrabbable,
billboard: createDefaultInflator(Billboard),
@@ -442,10 +445,12 @@ export const commonInflators: Required<{ [K in keyof ComponentData]: InflatorFn
audioZone: inflateAudioZone,
audioParams: inflateAudioParams,
mediaFrame: inflateMediaFrame,
- text: inflateText
+ text: inflateText,
+ networkedTransform: createDefaultInflator(NetworkedTransform),
+ networked: createDefaultInflator(Networked)
};
-const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = {
+export const jsxInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = {
...commonInflators,
cursorRaycastable: createDefaultInflator(CursorRaycastable),
remoteHoverTarget: createDefaultInflator(RemoteHoverTarget),
@@ -458,7 +463,7 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = {
textButton: createDefaultInflator(TextButton),
hoverButton: createDefaultInflator(HoverButton),
hoverableVisuals: createDefaultInflator(HoverableVisuals),
- holdable: createDefaultInflator(Holdable),
+ holdable: inflateHoldable,
deletable: createDefaultInflator(Deletable),
rigidbody: inflateRigidBody,
physicsShape: inflatePhysicsShape,
@@ -466,8 +471,6 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = {
networkedFloatyObject: createDefaultInflator(NetworkedFloatyObject),
makeKinematicOnRelease: createDefaultInflator(MakeKinematicOnRelease),
destroyAtExtremeDistance: createDefaultInflator(DestroyAtExtremeDistance),
- networkedTransform: createDefaultInflator(NetworkedTransform),
- networked: createDefaultInflator(Networked),
objectMenu: createDefaultInflator(ObjectMenu),
mirrorMenu: createDefaultInflator(MirrorMenu),
followInFov: inflateFollowInFov,
@@ -484,9 +487,9 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = {
waypointPreview: createDefaultInflator(WaypointPreview),
pdf: inflatePDF,
mediaLoader: inflateMediaLoader,
- quack: createDefaultInflator(Quack),
mixerAnimatable: createDefaultInflator(MixerAnimatableInitialize),
loopAnimation: inflateLoopAnimationInitialize,
+ text: inflateText,
inspectable: createDefaultInflator(Inspectable),
// inflators that create Object3Ds
object3D: addObject3DComponent,
@@ -500,7 +503,7 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = {
plane: inflatePlane
};
-export const gltfInflators: Required<{ [K in keyof GLTFComponentData]: InflatorFn }> = {
+export const gltfInflators: Required<{ [K in keyof ComponentDataT]: InflatorFn }> = {
...commonInflators,
pdf: inflatePDFLoader,
// Temporarily reuse video loader for audio because of
@@ -533,14 +536,17 @@ export const gltfInflators: Required<{ [K in keyof GLTFComponentData]: InflatorF
trimesh: inflateTrimesh,
heightfield: inflateHeightField,
audioSettings: inflateAudioSettings,
- mediaLink: inflateMediaLink
+ mediaLink: inflateMediaLink,
+ rigidbody: inflateGLTFRigidBody,
+ physicsShape: inflateAmmoShape,
+ text: inflateGLTFText
};
-function jsxInflatorExists(name: string): name is keyof JSXComponentData {
+function jsxInflatorExists(name: string) {
return Object.prototype.hasOwnProperty.call(jsxInflators, name);
}
-export function gltfInflatorExists(name: string): name is keyof GLTFComponentData {
+export function gltfInflatorExists(name: string) {
return Object.prototype.hasOwnProperty.call(gltfInflators, name);
}
diff --git a/src/utils/load-model.tsx b/src/utils/load-model.tsx
index 2b5313e0a7..4edb0b7213 100644
--- a/src/utils/load-model.tsx
+++ b/src/utils/load-model.tsx
@@ -9,7 +9,6 @@ export function* loadModel(world: HubsWorld, src: string, contentType: string, u
const { scene, animations } = yield loadGLTFModel(src, contentType, useCache, null);
scene.animations = animations;
- scene.mixer = new THREE.AnimationMixer(scene);
return renderAsEntity(world, );
}
diff --git a/src/utils/load-video-texture.js b/src/utils/load-video-texture.js
index a8b66495a2..08d9943524 100644
--- a/src/utils/load-video-texture.js
+++ b/src/utils/load-video-texture.js
@@ -48,10 +48,11 @@ export async function loadVideoTexture(src, contentType, loop, autoplay) {
texture = new HubsVideoTexture(videoEl);
videoEl.src = src;
videoEl.onerror = failLoad;
- videoEl.loop = loop;
- videoEl.autoplay = autoplay;
}
+ videoEl.loop = loop;
+ videoEl.autoplay = autoplay;
+
texture.minFilter = LinearFilter;
texture.encoding = sRGBEncoding;
diff --git a/src/utils/network-schemas.ts b/src/utils/network-schemas.ts
index b97517af7e..87ee038ee8 100644
--- a/src/utils/network-schemas.ts
+++ b/src/utils/network-schemas.ts
@@ -5,6 +5,8 @@ import {
NetworkedFloatyObject,
NetworkedMediaFrame,
NetworkedPDF,
+ NetworkedRigidBody,
+ NetworkedText,
NetworkedTransform,
NetworkedVideo,
NetworkedWaypoint
@@ -16,6 +18,8 @@ import { NetworkedTransformSchema } from "./networked-transform-schema";
import { NetworkedVideoSchema } from "./networked-video-schema";
import { NetworkedWaypointSchema } from "./networked-waypoint-schema";
import type { CursorBuffer, EntityID } from "./networking-types";
+import { NetworkedTextSchema } from "./networked-text-schema";
+import { NetworkedRigidBodySchema } from "./networked-rigid-body";
export interface StoredComponent {
version: number;
@@ -46,6 +50,8 @@ schemas.set(NetworkedFloatyObject, {
...defineNetworkSchema(NetworkedFloatyObject)
});
schemas.set(NetworkedPDF, NetworkedPDFSchema);
+schemas.set(NetworkedText, NetworkedTextSchema);
+schemas.set(NetworkedRigidBody, NetworkedRigidBodySchema);
export const networkableComponents = Array.from(schemas.keys());
diff --git a/src/utils/networked-media-frame-schema.ts b/src/utils/networked-media-frame-schema.ts
index 175b022af6..19cd816dc6 100644
--- a/src/utils/networked-media-frame-schema.ts
+++ b/src/utils/networked-media-frame-schema.ts
@@ -8,16 +8,30 @@ const runtimeSerde = defineNetworkSchema(NetworkedMediaFrame);
const migrations = new Map();
migrations.set(0, ({ data }: StoredComponent) => {
- return { version: 1, data };
+ return { version: 2, data };
});
function apply(eid: EntityID, { version, data }: StoredComponent) {
- if (version !== 1) return false;
-
- const { capturedNid, scale }: { capturedNid: string; scale: ArrayVec3 } = data;
- write(NetworkedMediaFrame.capturedNid, eid, capturedNid);
- write(NetworkedMediaFrame.scale, eid, scale);
- return true;
+ if (version === 1) {
+ const { capturedNid, scale }: { capturedNid: string; scale: ArrayVec3 } = data;
+ write(NetworkedMediaFrame.capturedNid, eid, capturedNid);
+ write(NetworkedMediaFrame.scale, eid, scale);
+ write(NetworkedMediaFrame.flags, eid, 0);
+ return true;
+ } else if (version === 2) {
+ const {
+ capturedNid,
+ scale,
+ flags,
+ mediaType
+ }: { capturedNid: string; scale: ArrayVec3; flags: number; mediaType: number } = data;
+ write(NetworkedMediaFrame.capturedNid, eid, capturedNid);
+ write(NetworkedMediaFrame.scale, eid, scale);
+ write(NetworkedMediaFrame.flags, eid, flags);
+ write(NetworkedMediaFrame.mediaType, eid, mediaType);
+ return true;
+ }
+ return false;
}
export const NetworkedMediaFrameSchema: NetworkSchema = {
@@ -26,10 +40,12 @@ export const NetworkedMediaFrameSchema: NetworkSchema = {
deserialize: runtimeSerde.deserialize,
serializeForStorage: function serializeForStorage(eid: EntityID) {
return {
- version: 1,
+ version: 2,
data: {
capturedNid: read(NetworkedMediaFrame.capturedNid, eid),
- scale: read(NetworkedMediaFrame.scale, eid)
+ scale: read(NetworkedMediaFrame.scale, eid),
+ flags: read(NetworkedMediaFrame.flags, eid),
+ mediaType: read(NetworkedMediaFrame.mediaType, eid)
}
};
},
diff --git a/src/utils/networked-rigid-body.ts b/src/utils/networked-rigid-body.ts
new file mode 100644
index 0000000000..41b9cf0aaa
--- /dev/null
+++ b/src/utils/networked-rigid-body.ts
@@ -0,0 +1,31 @@
+import { NetworkedRigidBody } from "../bit-components";
+import { defineNetworkSchema } from "./define-network-schema";
+import { deserializerWithMigrations, Migration, NetworkSchema, read, StoredComponent, write } from "./network-schemas";
+import type { EntityID } from "./networking-types";
+
+const runtimeSerde = defineNetworkSchema(NetworkedRigidBody);
+
+const migrations = new Map();
+
+function apply(eid: EntityID, { version, data }: StoredComponent) {
+ if (version !== 1) return false;
+
+ const { prevType }: { prevType: number } = data;
+ write(NetworkedRigidBody.prevType, eid, prevType);
+ return true;
+}
+
+export const NetworkedRigidBodySchema: NetworkSchema = {
+ componentName: "networked-rigid-body",
+ serialize: runtimeSerde.serialize,
+ deserialize: runtimeSerde.deserialize,
+ serializeForStorage: function serializeForStorage(eid: EntityID) {
+ return {
+ version: 1,
+ data: {
+ prevType: read(NetworkedRigidBody.prevType, eid)
+ }
+ };
+ },
+ deserializeFromStorage: deserializerWithMigrations(migrations, apply)
+};
diff --git a/src/utils/networked-text-schema.ts b/src/utils/networked-text-schema.ts
new file mode 100644
index 0000000000..1f9b272f6d
--- /dev/null
+++ b/src/utils/networked-text-schema.ts
@@ -0,0 +1,136 @@
+import { NetworkedText } from "../bit-components";
+import { defineNetworkSchema } from "./define-network-schema";
+import { deserializerWithMigrations, Migration, NetworkSchema, read, StoredComponent, write } from "./network-schemas";
+import type { EntityID } from "./networking-types";
+
+const migrations = new Map();
+
+function apply(eid: EntityID, { version, data }: StoredComponent) {
+ if (version !== 1) return false;
+
+ const {
+ text,
+ fontSize,
+ color,
+ fillOpacity,
+ anchorX,
+ anchorY,
+ curveRadius,
+ direction,
+ letterSpacing,
+ lineHeight,
+ maxWidth,
+ opacity,
+ outlineBlur,
+ outlineColor,
+ outlineOffsetX,
+ outlineOffsetY,
+ outlineOpacity,
+ outlineWidth,
+ overflowWrap,
+ side,
+ strokeColor,
+ strokeOpacity,
+ strokeWidth,
+ textAlign,
+ textIndent,
+ whiteSpace
+ }: {
+ text: string;
+ fontSize: number;
+ color: number;
+ fillOpacity: string;
+ anchorX: number;
+ anchorY: number;
+ curveRadius: number;
+ direction: number;
+ letterSpacing: number;
+ lineHeight: string;
+ maxWidth: number;
+ opacity: number;
+ outlineBlur: string;
+ outlineColor: number;
+ outlineOffsetX: string;
+ outlineOffsetY: string;
+ outlineOpacity: number;
+ outlineWidth: string;
+ overflowWrap: number;
+ side: number;
+ strokeColor: number;
+ strokeOpacity: number;
+ strokeWidth: string;
+ textAlign: number;
+ textIndent: number;
+ whiteSpace: number;
+ } = data;
+ write(NetworkedText.text, eid, APP.getSid(text));
+ write(NetworkedText.fontSize, eid, fontSize);
+ write(NetworkedText.color, eid, color);
+ write(NetworkedText.fillOpacity, eid, APP.getSid(fillOpacity));
+ write(NetworkedText.anchorX, eid, anchorX);
+ write(NetworkedText.anchorY, eid, anchorY);
+ write(NetworkedText.curveRadius, eid, curveRadius);
+ write(NetworkedText.direction, eid, direction);
+ write(NetworkedText.letterSpacing, eid, letterSpacing);
+ write(NetworkedText.lineHeight, eid, APP.getSid(lineHeight));
+ write(NetworkedText.maxWidth, eid, maxWidth);
+ write(NetworkedText.opacity, eid, opacity);
+ write(NetworkedText.outlineBlur, eid, APP.getSid(outlineBlur));
+ write(NetworkedText.outlineColor, eid, outlineColor);
+ write(NetworkedText.outlineOffsetX, eid, APP.getSid(outlineOffsetX));
+ write(NetworkedText.outlineOffsetY, eid, APP.getSid(outlineOffsetY));
+ write(NetworkedText.outlineOpacity, eid, outlineOpacity);
+ write(NetworkedText.outlineWidth, eid, outlineWidth);
+ write(NetworkedText.overflowWrap, eid, overflowWrap);
+ write(NetworkedText.side, eid, side);
+ write(NetworkedText.strokeColor, eid, strokeColor);
+ write(NetworkedText.strokeOpacity, eid, strokeOpacity);
+ write(NetworkedText.strokeWidth, eid, APP.getSid(strokeWidth));
+ write(NetworkedText.textAlign, eid, textAlign);
+ write(NetworkedText.textIndent, eid, textIndent);
+ write(NetworkedText.text, eid, text);
+ write(NetworkedText.whiteSpace, eid, whiteSpace);
+ return true;
+}
+
+const runtimeSerde = defineNetworkSchema(NetworkedText);
+export const NetworkedTextSchema: NetworkSchema = {
+ componentName: "networked-text",
+ serialize: runtimeSerde.serialize,
+ deserialize: runtimeSerde.deserialize,
+ serializeForStorage: function serializeForStorage(eid: EntityID) {
+ return {
+ version: 1,
+ data: {
+ text: APP.getString(read(NetworkedText.text, eid)),
+ fontSize: read(NetworkedText.fontSize, eid),
+ color: read(NetworkedText.color, eid),
+ fillOpacity: APP.getString(read(NetworkedText.fillOpacity, eid)),
+ anchorX: read(NetworkedText.anchorX, eid),
+ anchorY: read(NetworkedText.anchorY, eid),
+ curveRadius: read(NetworkedText.curveRadius, eid),
+ direction: read(NetworkedText.direction, eid),
+ letterSpacing: read(NetworkedText.letterSpacing, eid),
+ lineHeight: APP.getString(read(NetworkedText.lineHeight, eid)),
+ maxWidth: read(NetworkedText.maxWidth, eid),
+ opacity: read(NetworkedText.opacity, eid),
+ outlineBlur: APP.getString(read(NetworkedText.outlineBlur, eid)),
+ outlineColor: read(NetworkedText.outlineColor, eid),
+ outlineOffsetX: APP.getString(read(NetworkedText.outlineOffsetX, eid)),
+ outlineOffsetY: APP.getString(read(NetworkedText.outlineOffsetY, eid)),
+ outlineOpacity: read(NetworkedText.outlineOpacity, eid),
+ outlineWidth: APP.getString(read(NetworkedText.outlineWidth, eid)),
+ overflowWrap: read(NetworkedText.overflowWrap, eid),
+ side: read(NetworkedText.side, eid),
+ strokeColor: read(NetworkedText.strokeColor, eid),
+ strokeOpacity: read(NetworkedText.strokeOpacity, eid),
+ strokeWidth: APP.getString(read(NetworkedText.strokeWidth, eid)),
+ textAlign: read(NetworkedText.textAlign, eid),
+ textIndent: read(NetworkedText.textIndent, eid),
+ value: read(NetworkedText.text, eid),
+ whiteSpace: read(NetworkedText.whiteSpace, eid)
+ }
+ };
+ },
+ deserializeFromStorage: deserializerWithMigrations(migrations, apply)
+};
diff --git a/src/utils/networked-video-schema.ts b/src/utils/networked-video-schema.ts
index c1accc7e5d..695119658d 100644
--- a/src/utils/networked-video-schema.ts
+++ b/src/utils/networked-video-schema.ts
@@ -8,12 +8,20 @@ const runtimeSerde = defineNetworkSchema(NetworkedVideo);
const migrations = new Map();
function apply(eid: EntityID, { version, data }: StoredComponent) {
- if (version !== 1) return false;
-
- const { time, flags }: { time: number; flags: number } = data;
- write(NetworkedVideo.time, eid, time);
- write(NetworkedVideo.flags, eid, flags);
- return true;
+ if (version === 1) {
+ const { time, flags }: { time: number; flags: number } = data;
+ write(NetworkedVideo.time, eid, time);
+ write(NetworkedVideo.flags, eid, flags);
+ return true;
+ } else if (version === 2) {
+ const { time, flags, projection, src }: { time: number; flags: number; src: string; projection: number } = data;
+ write(NetworkedVideo.time, eid, time);
+ write(NetworkedVideo.flags, eid, flags);
+ write(NetworkedVideo.projection, eid, projection);
+ write(NetworkedVideo.src, eid, APP.getSid(src));
+ return true;
+ }
+ return false;
}
export const NetworkedVideoSchema: NetworkSchema = {
@@ -22,10 +30,12 @@ export const NetworkedVideoSchema: NetworkSchema = {
deserialize: runtimeSerde.deserialize,
serializeForStorage: function serializeForStorage(eid: EntityID) {
return {
- version: 1,
+ version: 2,
data: {
time: read(NetworkedVideo.time, eid),
- flags: read(NetworkedVideo.flags, eid)
+ flags: read(NetworkedVideo.flags, eid),
+ projection: read(NetworkedVideo.projection, eid),
+ src: APP.getString(read(NetworkedVideo.src, eid))
}
};
},
diff --git a/src/utils/networking-types.ts b/src/utils/networking-types.ts
index 766f090173..cd46457e16 100644
--- a/src/utils/networking-types.ts
+++ b/src/utils/networking-types.ts
@@ -1,10 +1,10 @@
import { MediaLoaderParams } from "../inflators/media-loader";
-import { PrefabName } from "../prefabs/prefabs";
+import { PrefabNameT } from "../types";
export type EntityID = number;
export type InitialData = MediaLoaderParams | any;
export interface CreateMessageData {
- prefabName: PrefabName;
+ prefabName: PrefabNameT;
initialData: InitialData;
}
export type ClientID = string;
@@ -13,7 +13,7 @@ export type StringID = number;
export type CreateMessage = {
version: 1;
networkId: NetworkID;
- prefabName: PrefabName;
+ prefabName: PrefabNameT;
initialData: InitialData;
};
export interface CursorBuffer extends Array {
diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts
index 521c28cd6f..b37e9cc902 100644
--- a/src/utils/permissions.ts
+++ b/src/utils/permissions.ts
@@ -1,7 +1,8 @@
-import { PrefabName, prefabs } from "../prefabs/prefabs";
+import { prefabs } from "../prefabs/prefabs";
+import { PrefabNameT } from "../types";
import type { ClientID } from "./networking-types";
-export function hasPermissionToSpawn(creator: ClientID, prefabName: PrefabName) {
+export function hasPermissionToSpawn(creator: ClientID, prefabName: PrefabNameT) {
if (creator === "reticulum") return true;
const perm = prefabs.get(prefabName)!.permission;
return APP.hubChannel!.userCan(creator, perm);
diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js
index df1d4bbff9..e484780b45 100644
--- a/src/utils/phoenix-utils.js
+++ b/src/utils/phoenix-utils.js
@@ -2,7 +2,7 @@ import { Socket } from "phoenix";
import { generateHubName } from "../utils/name-generation";
import configs from "../utils/configs";
import { sleep } from "../utils/async-utils";
-import { store } from "../utils/store-instance";
+import { getStore } from "../utils/store-instance";
export function hasReticulumServer() {
return !!configs.RETICULUM_SERVER;
@@ -198,6 +198,7 @@ export function fetchReticulumAuthenticatedWithToken(token, url, method = "GET",
});
}
export function fetchReticulumAuthenticated(url, method = "GET", payload) {
+ const store = getStore();
return fetchReticulumAuthenticatedWithToken(store.state.credentials.token, url, method, payload);
}
@@ -210,6 +211,7 @@ export async function createAndRedirectToNewHub(name, sceneId, replace, qs) {
}
const headers = { "content-type": "application/json" };
+ const store = getStore();
if (store.state && store.state.credentials.token) {
headers.authorization = `bearer ${store.state.credentials.token}`;
}
diff --git a/src/utils/projection-mode.ts b/src/utils/projection-mode.ts
index 073ed99eef..e9731045b5 100644
--- a/src/utils/projection-mode.ts
+++ b/src/utils/projection-mode.ts
@@ -15,3 +15,12 @@ export function getProjectionFromProjectionName(projectionName: ProjectionModeNa
}
return ProjectionMode.FLAT;
}
+
+export function getProjectionNameFromProjection(projection: ProjectionMode): ProjectionModeName {
+ if (projection === ProjectionMode.FLAT) {
+ return ProjectionModeName.FLAT;
+ } else if (projection === ProjectionMode.SPHERE_EQUIRECTANGULAR) {
+ return ProjectionModeName.SPHERE_EQUIRECTANGULAR;
+ }
+ return ProjectionModeName.FLAT;
+}
diff --git a/src/utils/store-instance.js b/src/utils/store-instance.js
new file mode 100644
index 0000000000..6332f6c8e4
--- /dev/null
+++ b/src/utils/store-instance.js
@@ -0,0 +1,11 @@
+import Store from "../storage/store";
+
+let store;
+export function getStore() {
+ if (store) {
+ return store;
+ } else {
+ store = new Store();
+ return store;
+ }
+}
diff --git a/src/utils/store-instance.ts b/src/utils/store-instance.ts
deleted file mode 100644
index bb7c358f31..0000000000
--- a/src/utils/store-instance.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Store from "../storage/store";
-
-export const store = new Store();
diff --git a/src/utils/theme.js b/src/utils/theme.js
index 654865114f..9597716f05 100644
--- a/src/utils/theme.js
+++ b/src/utils/theme.js
@@ -1,5 +1,5 @@
import { waitForDOMContentLoaded } from "./async-utils";
-import { store } from "./store-instance";
+import { getStore } from "./store-instance";
// NOTE these should be synchronized with the top of shared.scss
const DEFAULT_ACTION_COLOR = "#FF3464";
@@ -99,6 +99,7 @@ function tryGetTheme(themeId) {
}
function getCurrentTheme() {
+ const store = getStore();
const preferredThemeId = store.state?.preferences?.theme;
return tryGetTheme(preferredThemeId);
}
@@ -153,6 +154,7 @@ function applyThemeToBody() {
}
function onThemeChanged(listener) {
+ const store = getStore();
store.addEventListener("themechanged", listener);
const [_darkModeQuery, removeDarkModeListener] = registerDarkModeQuery(listener);
diff --git a/src/utils/three-utils.js b/src/utils/three-utils.js
index d9808919b5..b2b9238298 100644
--- a/src/utils/three-utils.js
+++ b/src/utils/three-utils.js
@@ -23,16 +23,66 @@ export function getLastWorldScale(src, target) {
}
export function disposeMaterial(mtrl) {
- if (mtrl.map) mtrl.map.dispose();
- if (mtrl.lightMap) mtrl.lightMap.dispose();
- if (mtrl.bumpMap) mtrl.bumpMap.dispose();
- if (mtrl.normalMap) mtrl.normalMap.dispose();
- if (mtrl.specularMap) mtrl.specularMap.dispose();
- if (mtrl.envMap) mtrl.envMap.dispose();
- if (mtrl.aoMap) mtrl.aoMap.dispose();
- if (mtrl.metalnessMap) mtrl.metalnessMap.dispose();
- if (mtrl.roughnessMap) mtrl.roughnessMap.dispose();
- if (mtrl.emissiveMap) mtrl.emissiveMap.dispose();
+ if (mtrl.map) {
+ mtrl.map.dispose();
+ if (mtrl.map.eid) {
+ removeEntity(APP.world, mtrl.map.eid);
+ }
+ }
+ if (mtrl.lightMap) {
+ mtrl.lightMap.dispose();
+ if (mtrl.lightMap.eid) {
+ removeEntity(APP.world, mtrl.lightMap.eid);
+ }
+ }
+ if (mtrl.bumpMap) {
+ mtrl.bumpMap.dispose();
+ if (mtrl.bumpMap.eid) {
+ removeEntity(APP.world, mtrl.bumpMap.eid);
+ }
+ }
+ if (mtrl.normalMap) {
+ mtrl.normalMap.dispose();
+ if (mtrl.normalMap.eid) {
+ removeEntity(APP.world, mtrl.normalMap.eid);
+ }
+ }
+ if (mtrl.specularMap) {
+ mtrl.specularMap.dispose();
+ if (mtrl.specularMap.eid) {
+ removeEntity(APP.world, mtrl.specularMap.eid);
+ }
+ }
+ if (mtrl.envMap) {
+ mtrl.envMap.dispose();
+ if (mtrl.envMap.eid) {
+ removeEntity(APP.world, mtrl.envMap.eid);
+ }
+ }
+ if (mtrl.aoMap) {
+ mtrl.aoMap.dispose();
+ if (mtrl.aoMap.eid) {
+ removeEntity(APP.world, mtrl.aoMap.eid);
+ }
+ }
+ if (mtrl.metalnessMap) {
+ mtrl.metalnessMap.dispose();
+ if (mtrl.metalnessMap.eid) {
+ removeEntity(APP.world, mtrl.metalnessMap.eid);
+ }
+ }
+ if (mtrl.roughnessMap) {
+ mtrl.roughnessMap.dispose();
+ if (mtrl.roughnessMap.eid) {
+ removeEntity(APP.world, mtrl.roughnessMap.eid);
+ }
+ }
+ if (mtrl.emissiveMap) {
+ mtrl.emissiveMap.dispose();
+ if (mtrl.emissiveMap.eid) {
+ removeEntity(APP.world, mtrl.emissiveMap.eid);
+ }
+ }
mtrl.dispose();
if (mtrl.eid) {
removeEntity(APP.world, mtrl.eid);
diff --git a/src/verify.js b/src/verify.js
index 1546d6c562..03719c9aad 100644
--- a/src/verify.js
+++ b/src/verify.js
@@ -10,10 +10,11 @@ import "./assets/stylesheets/globals.scss";
import { PageContainer } from "./react-components/layout/PageContainer";
import { Center } from "./react-components/layout/Center";
import { ThemeProvider } from "./react-components/styles/theme";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
registerTelemetry("/verify", "Hubs Verify Email Page");
+const store = getStore();
window.APP = { store };
function VerifyRoot() {
diff --git a/src/whats-new.js b/src/whats-new.js
index de1887e296..af9d68342e 100644
--- a/src/whats-new.js
+++ b/src/whats-new.js
@@ -5,8 +5,9 @@ import markdownit from "markdown-it";
import { FormattedMessage } from "react-intl";
import { WrappedIntlProvider } from "./react-components/wrapped-intl-provider";
import { AuthContextProvider } from "./react-components/auth/AuthContext";
-import { store } from "./utils/store-instance";
+import { getStore } from "./utils/store-instance";
+const store = getStore();
window.APP = { store };
import registerTelemetry from "./telemetry";
diff --git a/tsconfig.json b/tsconfig.json
index cbe1e73df6..60088b491a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,13 +12,7 @@
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
- "typeRoots": [
- "./node_modules/@types",
- "./types"
- ]
+ "typeRoots": ["./node_modules/@types", "./types"]
},
- "include": [
- "types/",
- "src/"
- ]
+ "include": ["types/", "src/"]
}
diff --git a/types/aframe.d.ts b/types/aframe.d.ts
index dbad160219..5d91b871d3 100644
--- a/types/aframe.d.ts
+++ b/types/aframe.d.ts
@@ -8,7 +8,10 @@ declare module "aframe" {
[name: string]: Object3D;
};
getObject3D(string): Object3D?;
- components: { [s: string]: AComponent };
+ components: {
+ [s: string]: AComponent;
+ "player-info": PlayerInfo;
+ };
eid: number;
isPlaying: boolean;
}
@@ -92,6 +95,10 @@ declare module "aframe" {
disable(): void;
}
+ interface PlayerInfo extends AComponent {
+ playerSessionId: string;
+ }
+
interface PersonalSpaceBubbleSystem extends ASystem {
invaders: PersonalSpaceInvader[];
}
diff --git a/types/assets.d.ts b/types/assets.d.ts
index dd05f0f49d..6fb7d55dc1 100644
--- a/types/assets.d.ts
+++ b/types/assets.d.ts
@@ -17,3 +17,8 @@ declare module "*.glb" {
const url: string;
export default url;
}
+
+declare module "*.mp3" {
+ const src: string;
+ export default url;
+}
diff --git a/types/three.d.ts b/types/three.d.ts
index 5ef7d9bbe8..a019ee3379 100644
--- a/types/three.d.ts
+++ b/types/three.d.ts
@@ -21,10 +21,16 @@ declare module "three" {
group: GeometryGroup
) => void;
}
+
+ interface Texture {
+ eid?: number;
+ }
interface Mesh {
reflectionProbeMode: "static" | "dynamic" | false;
}
-
+ interface AnimationAction {
+ eid?: number;
+ }
interface Vector3 {
near: Function;
}
diff --git a/types/troika-three-text.d.ts b/types/troika-three-text.d.ts
index e099337d2e..9dbe86a730 100644
--- a/types/troika-three-text.d.ts
+++ b/types/troika-three-text.d.ts
@@ -3,8 +3,17 @@ declare module "troika-three-text" {
export class Text extends Mesh {
text: string;
- anchorX: number | `${number}%` | 'left' | 'center' | 'right';
- anchorY: number | `${number}%` | 'top' | 'top-baseline' | 'top-cap' | 'top-ex' | 'middle' | 'bottom-baseline' | 'bottom';
+ anchorX: number | `${number}%` | "left" | "center" | "right";
+ anchorY:
+ | number
+ | `${number}%`
+ | "top"
+ | "top-baseline"
+ | "top-cap"
+ | "top-ex"
+ | "middle"
+ | "bottom-baseline"
+ | "bottom";
clipRect: [number, number, number, number] | null;
color: string | number | Color | null;
curveRadius: number;
@@ -16,7 +25,7 @@ declare module "troika-three-text" {
glyphGeometryDetail: number;
gpuAccelerateSDF: boolean;
letterSpacing: number;
- lineHeight: number | 'normal';
+ lineHeight: number | "normal";
material: Material | null;
maxWidth: number;
outlineBlur: number | `${number}%`;
@@ -25,15 +34,16 @@ declare module "troika-three-text" {
outlineOffsetY: number | `${number}%`;
outlineOpacity: number;
outlineWidth: number | `${number}%`;
- overflowWrap: 'normal' | 'break-word';
+ overflowWrap: "normal" | "break-word";
sdfGlyphSize: number | null;
strokeColor: string | number | Color;
strokeOpacity: number;
strokeWidth: number | `${number}%`;
- textAlign: 'left' | 'right' | 'center' | 'justify';
+ textAlign: "left" | "right" | "center" | "justify";
textIndent: number;
- whiteSpace: 'normal' | 'nowrap';
+ whiteSpace: "normal" | "nowrap";
sync(callback?: () => void): void;
+ isTroikaText: true;
}
export const preloadFont: (
diff --git a/webpack.config.js b/webpack.config.js
index 974c0bd98b..0a3f660491 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -285,6 +285,9 @@ module.exports = async (env, argv) => {
// .replaceAll("connect-src", "connect-src https://example.com");
}
+ const addonsConfigFilePath = "./addons.json";
+ const addonsConfig = JSON.parse(fs.readFileSync(addonsConfigFilePath, "utf-8"));
+
const internalHostname = process.env.INTERNAL_HOSTNAME || "hubs.local";
return {
cache: {
@@ -303,7 +306,9 @@ module.exports = async (env, argv) => {
"three/examples/js/libs/basis/basis_transcoder.js": basisTranscoderPath,
"three/examples/js/libs/draco/gltf/draco_wasm_wrapper.js": dracoWasmWrapperPath,
"three/examples/js/libs/basis/basis_transcoder.wasm": basisWasmPath,
- "three/examples/js/libs/draco/gltf/draco_decoder.wasm": dracoWasmPath
+ "three/examples/js/libs/draco/gltf/draco_decoder.wasm": dracoWasmPath,
+
+ hubs$: path.resolve(__dirname, "./src/hubs.js")
},
// Allows using symlinks in node_modules
symlinks: false,
@@ -320,7 +325,7 @@ module.exports = async (env, argv) => {
entry: {
support: path.join(__dirname, "src", "support.js"),
index: path.join(__dirname, "src", "index.js"),
- hub: path.join(__dirname, "src", "hub.js"),
+ hub: [path.join(__dirname, "src", "hub.js"), ...addonsConfig.addons],
scene: path.join(__dirname, "src", "scene.js"),
avatar: path.join(__dirname, "src", "avatar.js"),
link: path.join(__dirname, "src", "link.js"),
@@ -336,6 +341,9 @@ module.exports = async (env, argv) => {
filename: "assets/js/[name]-[chunkhash].js",
publicPath: process.env.BASE_ASSETS_PATH || ""
},
+ optimization: {
+ minimize: argv.mode === "production" ? true : false
+ },
target: ["web", "es5"], // use es5 for webpack runtime to maximize compatibility
devtool: argv.mode === "production" ? "source-map" : "inline-source-map",
devServer: {