Skip to content

Commit 5ee0ba8

Browse files
committed
Initial add-ons API commit
1 parent a8208fc commit 5ee0ba8

26 files changed

+633
-158
lines changed

addons.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"addons": ["hubs-duck-addon", "hubs-portals-addon"]
3+
}

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@
102102
"history": "^4.7.2",
103103
"hls.js": "^0.14.6",
104104
"html2canvas": "^1.0.0-rc.7",
105+
"hubs-duck-addon": "github:MozillaReality/hubs-duck-addon",
106+
"hubs-portals-addon": "github:MozillaReality/hubs-portals-addon",
105107
"js-cookie": "^2.2.0",
106108
"jsonschema": "^1.2.2",
107109
"jwt-decode": "^2.2.0",

src/addons.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { App } from "./app";
2+
import { prefabs } from "./prefabs/prefabs";
3+
4+
import {
5+
InflatorConfigT,
6+
SystemConfigT,
7+
SystemOrderE,
8+
PrefabConfigT,
9+
NetworkSchemaConfigT,
10+
ChatCommandConfigT
11+
} from "./types";
12+
import configs from "./utils/configs";
13+
import { commonInflators, gltfInflators, jsxInflators } from "./utils/jsx-entity";
14+
import { networkableComponents, schemas } from "./utils/network-schemas";
15+
16+
function getNextIdx(slot: Array<SystemConfigT>, system: SystemConfigT) {
17+
return slot.findIndex(item => {
18+
item.order > system.order;
19+
});
20+
}
21+
22+
function registerSystem(system: SystemConfigT) {
23+
let slot = APP.addon_systems.prePhysics;
24+
if (system.order < SystemOrderE.PrePhysics) {
25+
slot = APP.addon_systems.setup;
26+
} else if (system.order < SystemOrderE.PostPhysics) {
27+
slot = APP.addon_systems.prePhysics;
28+
} else if (system.order < SystemOrderE.MatricesUpdate) {
29+
slot = APP.addon_systems.postPhysics;
30+
} else if (system.order < SystemOrderE.BeforeRender) {
31+
slot = APP.addon_systems.beforeRender;
32+
} else if (system.order < SystemOrderE.AfterRender) {
33+
slot = APP.addon_systems.afterRender;
34+
} else if (system.order < SystemOrderE.PostProcessing) {
35+
slot = APP.addon_systems.postProcessing;
36+
} else {
37+
slot = APP.addon_systems.tearDown;
38+
}
39+
const nextIdx = getNextIdx(slot, system);
40+
slot.splice(nextIdx, 0, system);
41+
}
42+
43+
function registerInflator(inflator: InflatorConfigT) {
44+
if (inflator.common) {
45+
commonInflators[inflator.common.id] = inflator.common.inflator;
46+
} else {
47+
if (inflator.jsx) {
48+
jsxInflators[inflator.jsx.id] = inflator.jsx.inflator;
49+
}
50+
if (inflator.gltf) {
51+
gltfInflators[inflator.gltf.id] = inflator.gltf.inflator;
52+
}
53+
}
54+
}
55+
56+
function registerPrefab(prefab: PrefabConfigT) {
57+
if (prefabs.has(prefab.id)) {
58+
throw Error(`Error registering prefab ${name}: prefab already registered`);
59+
}
60+
prefabs.set(prefab.id, prefab.config);
61+
}
62+
63+
function registerNetworkSchema(schemaConfig: NetworkSchemaConfigT) {
64+
if (schemas.has(schemaConfig.component)) {
65+
throw Error(
66+
`Error registering network schema ${schemaConfig.schema.componentName}: network schema already registered`
67+
);
68+
}
69+
schemas.set(schemaConfig.component, schemaConfig.schema);
70+
networkableComponents.push(schemaConfig.component);
71+
}
72+
73+
function registerChatCommand(command: ChatCommandConfigT) {
74+
APP.messageDispatch.registerChatCommand(command.id, command.command);
75+
}
76+
77+
export type AddonIdT = string;
78+
export type AddonNameT = string;
79+
export type AddonDescriptionT = string;
80+
export type AddonOnReadyFn = (app: App, config?: JSON) => void;
81+
82+
export interface InternalAddonConfigT {
83+
name: AddonNameT;
84+
description?: AddonDescriptionT;
85+
onReady?: AddonOnReadyFn;
86+
system?: SystemConfigT | SystemConfigT[];
87+
inflator?: InflatorConfigT | InflatorConfigT[];
88+
prefab?: PrefabConfigT | PrefabConfigT[];
89+
networkSchema?: NetworkSchemaConfigT | NetworkSchemaConfigT[];
90+
chatCommand?: ChatCommandConfigT | ChatCommandConfigT[];
91+
enabled?: boolean;
92+
config?: JSON | undefined;
93+
}
94+
type AddonConfigT = Omit<InternalAddonConfigT, "enabled" | "config">;
95+
96+
const pendingAddons = new Map<AddonIdT, InternalAddonConfigT>();
97+
export const addons = new Map<AddonIdT, AddonConfigT>();
98+
export type AddonRegisterCallbackT = (app: App) => void;
99+
export function registerAddon(id: AddonIdT, config: AddonConfigT) {
100+
console.log(`Add-on ${id} registered`);
101+
pendingAddons.set(id, config);
102+
}
103+
104+
export function onAddonsInit(app: App) {
105+
app.scene?.addEventListener("hub_updated", () => {
106+
for (const [id, addon] of pendingAddons) {
107+
if (addons.has(id)) {
108+
throw Error(`Addon ${id} already registered`);
109+
} else {
110+
addons.set(id, addon);
111+
}
112+
113+
if (app.hub?.user_data && `addon_${id}` in app.hub.user_data) {
114+
addon.enabled = app.hub.user_data[`addon_${id}`];
115+
} else {
116+
addon.enabled = false;
117+
}
118+
119+
if (!addon.enabled) {
120+
continue;
121+
}
122+
123+
if (addon.prefab) {
124+
if (Array.isArray(addon.prefab)) {
125+
addon.prefab.forEach(prefab => {
126+
registerPrefab(prefab);
127+
});
128+
} else {
129+
registerPrefab(addon.prefab);
130+
}
131+
}
132+
133+
if (addon.networkSchema) {
134+
if (Array.isArray(addon.networkSchema)) {
135+
addon.networkSchema.forEach(networkSchema => {
136+
registerNetworkSchema(networkSchema);
137+
});
138+
} else {
139+
registerNetworkSchema(addon.networkSchema);
140+
}
141+
}
142+
143+
if (addon.inflator) {
144+
if (Array.isArray(addon.inflator)) {
145+
addon.inflator.forEach(inflator => {
146+
registerInflator(inflator);
147+
});
148+
} else {
149+
registerInflator(addon.inflator);
150+
}
151+
}
152+
153+
if (addon.system) {
154+
if (Array.isArray(addon.system)) {
155+
addon.system.forEach(system => {
156+
registerSystem(system);
157+
});
158+
} else {
159+
registerSystem(addon.system);
160+
}
161+
}
162+
163+
if (addon.chatCommand) {
164+
if (Array.isArray(addon.chatCommand)) {
165+
addon.chatCommand.forEach(chatCommand => {
166+
registerChatCommand(chatCommand);
167+
});
168+
} else {
169+
registerChatCommand(addon.chatCommand);
170+
}
171+
}
172+
173+
if (addon.onReady) {
174+
let config;
175+
const addonsConfig = configs.feature("addons_config");
176+
if (addonsConfig && id in addonsConfig) {
177+
config = addonsConfig[id];
178+
}
179+
addon.onReady(app, config);
180+
}
181+
}
182+
pendingAddons.clear();
183+
});
184+
}

src/app.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import SceneEntryManager from "./scene-entry-manager";
3030
import { store } from "./utils/store-instance";
3131
import { addObject3DComponent } from "./utils/jsx-entity";
3232
import { ElOrEid } from "./utils/bit-utils";
33+
import { onAddonsInit } from "./addons";
34+
import { CoreSystemKeyT, HubsSystemKeyT, SystemConfigT, SystemKeyT, SystemT } from "./types";
3335

3436
declare global {
3537
interface Window {
@@ -63,7 +65,7 @@ export function getScene() {
6365
return promiseToScene;
6466
}
6567

66-
interface HubDescription {
68+
export interface HubDescription {
6769
hub_id: string;
6870
user_data?: any;
6971
}
@@ -104,6 +106,18 @@ export class App {
104106

105107
dialog = new DialogAdapter();
106108

109+
addon_systems = {
110+
setup: new Array<{ order: number; system: SystemT }>(),
111+
prePhysics: new Array<{ order: number; system: SystemT }>(),
112+
postPhysics: new Array<{ order: number; system: SystemT }>(),
113+
matricesUpdate: new Array<{ order: number; system: SystemT }>(),
114+
beforeRender: new Array<{ order: number; system: SystemT }>(),
115+
render: new Array<{ order: number; system: SystemT }>(),
116+
afterRender: new Array<{ order: number; system: SystemT }>(),
117+
postProcessing: new Array<{ order: number; system: SystemT }>(),
118+
tearDown: new Array<{ order: number; system: SystemT }>()
119+
};
120+
107121
RENDER_ORDER = {
108122
HUD_BACKGROUND: 1,
109123
HUD_ICONS: 2,
@@ -159,6 +173,19 @@ export class App {
159173
return this.sid2str.get(sid);
160174
}
161175

176+
notifyOnInit() {
177+
onAddonsInit(this);
178+
}
179+
180+
getSystem(id: SystemKeyT) {
181+
const systems = this.scene?.systems!;
182+
if (id in systems) {
183+
return systems[id as CoreSystemKeyT];
184+
} else {
185+
return systems["hubs-systems"][id as HubsSystemKeyT];
186+
}
187+
}
188+
162189
// This gets called by a-scene to setup the renderer, camera, and audio listener
163190
// TODO ideally the contorl flow here would be inverted, and we would setup this stuff,
164191
// initialize aframe, and then run our own RAF loop

src/bit-components.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,6 @@ export const LinearScale = defineComponent({
441441
targetY: Types.f32,
442442
targetZ: Types.f32
443443
});
444-
export const Quack = defineComponent();
445444
export const TrimeshTag = defineComponent();
446445
export const HeightFieldTag = defineComponent();
447446
export const LocalAvatar = defineComponent();

src/bit-systems/quack.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/hub.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ import { exposeBitECSDebugHelpers } from "./bitecs-debug-helpers";
273273
import { loadLegacyRoomObjects } from "./utils/load-legacy-room-objects";
274274
import { loadSavedEntityStates } from "./utils/entity-state-utils";
275275
import { shouldUseNewLoader } from "./utils/bit-utils";
276+
import { addons } from "./addons";
276277

277278
const PHOENIX_RELIABLE_NAF = "phx-reliable";
278279
NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF;
@@ -543,8 +544,18 @@ export async function updateEnvironmentForHub(hub, entryManager) {
543544
}
544545
}
545546

546-
export async function updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt = false) {
547-
remountUI({ hub, entryDisallowed: !hubChannel.canEnterRoom(hub), showBitECSBasedClientRefreshPrompt });
547+
export async function updateUIForHub(
548+
hub,
549+
hubChannel,
550+
showBitECSBasedClientRefreshPrompt = false,
551+
showAddonRefreshPrompt = false
552+
) {
553+
remountUI({
554+
hub,
555+
entryDisallowed: !hubChannel.canEnterRoom(hub),
556+
showBitECSBasedClientRefreshPrompt,
557+
showAddonRefreshPrompt
558+
});
548559
}
549560

550561
function onConnectionError(entryManager, connectError) {
@@ -1388,16 +1399,27 @@ document.addEventListener("DOMContentLoaded", async () => {
13881399
const displayName = (userInfo && userInfo.metas[0].profile.displayName) || "API";
13891400

13901401
let showBitECSBasedClientRefreshPrompt = false;
1391-
13921402
if (!!hub.user_data?.hubs_use_bitecs_based_client !== !!window.APP.hub.user_data?.hubs_use_bitecs_based_client) {
13931403
showBitECSBasedClientRefreshPrompt = true;
13941404
setTimeout(() => {
13951405
document.location.reload();
13961406
}, 5000);
13971407
}
1408+
let showAddonRefreshPrompt = false;
1409+
[...addons.keys()].map(id => {
1410+
const key = `addon_${id}`;
1411+
const oldAddonState = !!window.APP.hub.user_data && window.APP.hub.user_data[key];
1412+
const newAddonState = !!hub.user_data && hub.user_data[key];
1413+
if (newAddonState !== oldAddonState) {
1414+
showAddonRefreshPrompt = true;
1415+
setTimeout(() => {
1416+
document.location.reload();
1417+
}, 5000);
1418+
}
1419+
});
13981420

13991421
window.APP.hub = hub;
1400-
updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt);
1422+
updateUIForHub(hub, hubChannel, showBitECSBasedClientRefreshPrompt, showAddonRefreshPrompt);
14011423

14021424
if (
14031425
stale_fields.includes("scene") ||
@@ -1484,4 +1506,6 @@ document.addEventListener("DOMContentLoaded", async () => {
14841506

14851507
authChannel.setSocket(socket);
14861508
linkChannel.setSocket(socket);
1509+
1510+
APP.notifyOnInit();
14871511
});

0 commit comments

Comments
 (0)