diff --git a/.defaults.env b/.defaults.env index c8fe7d7ef1..b2e71b8cc7 100644 --- a/.defaults.env +++ b/.defaults.env @@ -18,7 +18,7 @@ THUMBNAIL_SERVER="nearspark-dev.reticulum.io" ASSET_BUNDLE_SERVER="https://asset-bundles-prod.reticulum.io" # Comma-separated list of domains which are known to not need CORS proxying -NON_CORS_PROXY_DOMAINS="hubs.local,dev.reticulum.io,hubs-upload-cdn.com" +NON_CORS_PROXY_DOMAINS="hubs.local,hubs-proxy.local,dev.reticulum.io,hubs-upload-cdn.com" # The root URL under which Hubs expects static assets to be served. BASE_ASSETS_PATH=/ diff --git a/src/bit-components.js b/src/bit-components.js index b3fb0e6e92..93892ae2e7 100644 --- a/src/bit-components.js +++ b/src/bit-components.js @@ -124,8 +124,6 @@ export const PhysicsShape = defineComponent({ heightfieldDistance: Types.f32, flags: Types.ui8 }); -export const Pinnable = defineComponent(); -export const Pinned = defineComponent(); export const DestroyAtExtremeDistance = defineComponent(); export const MediaLoading = defineComponent(); export const FloatyObject = defineComponent({ flags: Types.ui8, releaseGravity: Types.f32 }); @@ -153,9 +151,11 @@ export const CameraTool = defineComponent({ export const MyCameraTool = defineComponent(); export const MediaLoader = defineComponent({ src: Types.ui32, - flags: Types.ui8 + flags: Types.ui8, + fileId: Types.ui32 }); MediaLoader.src[$isStringType] = true; +MediaLoader.fileId[$isStringType] = true; export const MediaLoaded = defineComponent(); export const MediaContentBounds = defineComponent({ bounds: [Types.f32, 3] diff --git a/src/bit-systems/media-loading.ts b/src/bit-systems/media-loading.ts index ef3bc1b6ed..bf69c217d0 100644 --- a/src/bit-systems/media-loading.ts +++ b/src/bit-systems/media-loading.ts @@ -65,23 +65,19 @@ export function* waitForMediaLoaded(world: HubsWorld, eid: EntityID) { } } +// prettier-ignore const loaderForMediaType = { - [MediaType.IMAGE]: ( - world: HubsWorld, - { accessibleUrl, contentType }: { accessibleUrl: string; contentType: string } - ) => loadImage(world, accessibleUrl, contentType), - [MediaType.VIDEO]: ( - world: HubsWorld, - { accessibleUrl, contentType }: { accessibleUrl: string; contentType: string } - ) => loadVideo(world, accessibleUrl, contentType), - [MediaType.MODEL]: ( - world: HubsWorld, - { accessibleUrl, contentType }: { accessibleUrl: string; contentType: string } - ) => loadModel(world, accessibleUrl, contentType, true), - [MediaType.PDF]: (world: HubsWorld, { accessibleUrl }: { accessibleUrl: string }) => loadPDF(world, accessibleUrl), + [MediaType.IMAGE]: (world: HubsWorld, { accessibleUrl, contentType }: { accessibleUrl: string, contentType: string }) => + loadImage(world, accessibleUrl, contentType), + [MediaType.VIDEO]: (world: HubsWorld, { accessibleUrl, contentType }: { accessibleUrl: string, contentType: string }) => + loadVideo(world, accessibleUrl, contentType), + [MediaType.MODEL]: (world: HubsWorld, { accessibleUrl, contentType }: { accessibleUrl: string, contentType: string }) => + loadModel(world, accessibleUrl, contentType, true), + [MediaType.PDF]: (world: HubsWorld, { accessibleUrl }: { accessibleUrl: string }) => + loadPDF(world, accessibleUrl), [MediaType.AUDIO]: (world: HubsWorld, { accessibleUrl }: { accessibleUrl: string }) => loadAudio(world, accessibleUrl), - [MediaType.HTML]: (world: HubsWorld, { canonicalUrl, thumbnail }: { canonicalUrl: string; thumbnail: string }) => + [MediaType.HTML]: (world: HubsWorld, { canonicalUrl, thumbnail }: { canonicalUrl: string, thumbnail: string }) => loadHtml(world, canonicalUrl, thumbnail) }; diff --git a/src/bit-systems/network-receive-system.ts b/src/bit-systems/network-receive-system.ts index 01c70d10d1..ec61173c35 100644 --- a/src/bit-systems/network-receive-system.ts +++ b/src/bit-systems/network-receive-system.ts @@ -2,7 +2,7 @@ import { addComponent, defineQuery, enterQuery, hasComponent, removeComponent, r import { HubsWorld } from "../app"; import { Networked, Owned } from "../bit-components"; import { renderAsNetworkedEntity } from "../utils/create-networked-entity"; -import { deleteEntityState, hasSavedEntityState } from "../utils/entity-state-utils"; +import { createEntityState, deleteEntityState, hasSavedEntityState } from "../utils/entity-state-utils"; import { networkableComponents, schemas, StoredComponent } from "../utils/network-schemas"; import type { ClientID, CursorBufferUpdateMessage, EntityID, StringID, UpdateMessage } from "../utils/networking-types"; import { hasPermissionToSpawn } from "../utils/permissions"; @@ -141,7 +141,6 @@ export function networkReceiveSystem(world: HubsWorld) { world.ignoredNids.add(nid); continue; } - renderAsNetworkedEntity(world, prefabName, initialData, nidString, creator); } } diff --git a/src/bit-systems/networking.ts b/src/bit-systems/networking.ts index 5fece98260..f44ae2d225 100644 --- a/src/bit-systems/networking.ts +++ b/src/bit-systems/networking.ts @@ -1,6 +1,13 @@ import { defineQuery } from "bitecs"; import { Networked } from "../bit-components"; -import type { CreateMessageData, CreatorChange, EntityID, Message, StringID } from "../utils/networking-types"; +import type { + CreateMessageData, + CreatorChange, + EntityID, + Message, + NetworkID, + StringID +} from "../utils/networking-types"; export let localClientID: StringID | null = null; export function setLocalClientID(clientId: StringID) { connectedClientIds.add(clientId); diff --git a/src/bit-systems/object-menu.ts b/src/bit-systems/object-menu.ts index 44840632df..3aaba8597c 100644 --- a/src/bit-systems/object-menu.ts +++ b/src/bit-systems/object-menu.ts @@ -12,7 +12,6 @@ import { } from "../bit-components"; import { anyEntityWith, findAncestorWithComponent } from "../utils/bit-utils"; import { createNetworkedEntity } from "../utils/create-networked-entity"; -import { createEntityState, deleteEntityState } from "../utils/entity-state-utils"; import HubChannel from "../utils/hub-channel"; import type { EntityID } from "../utils/networking-types"; import { setMatrixWorld } from "../utils/three-utils"; @@ -20,6 +19,7 @@ import { deleteTheDeletableAncestor } from "./delete-entity-system"; import { createMessageDatas, isPinned } from "./networking"; import { TRANSFORM_MODE } from "../components/transform-object-button"; import { ScalingHandler } from "../components/scale-button"; +import { canPin, setPinned } from "../utils/bit-pinning-helper"; // Working variables. const _vec3_1 = new Vector3(); @@ -148,9 +148,9 @@ function cloneObject(world: HubsWorld, sourceEid: EntityID) { function handleClicks(world: HubsWorld, menu: EntityID, hubChannel: HubChannel) { if (clicked(world, ObjectMenu.pinButtonRef[menu])) { - createEntityState(hubChannel, world, ObjectMenu.targetRef[menu]); + setPinned(hubChannel, world, ObjectMenu.targetRef[menu], true); } else if (clicked(world, ObjectMenu.unpinButtonRef[menu])) { - deleteEntityState(hubChannel, world, ObjectMenu.targetRef[menu]); + setPinned(hubChannel, world, ObjectMenu.targetRef[menu], false); } else if (clicked(world, ObjectMenu.cameraFocusButtonRef[menu])) { console.log("Clicked focus"); } else if (clicked(world, ObjectMenu.cameraTrackButtonRef[menu])) { @@ -203,8 +203,9 @@ function updateVisibility(world: HubsWorld, menu: EntityID, frozen: boolean) { const obj = world.eid2obj.get(menu)!; obj.visible = visible; - world.eid2obj.get(ObjectMenu.pinButtonRef[menu])!.visible = visible && !isPinned(target); world.eid2obj.get(ObjectMenu.unpinButtonRef[menu])!.visible = visible && isPinned(target); + world.eid2obj.get(ObjectMenu.pinButtonRef[menu])!.visible = + visible && !isPinned(target) && canPin(APP.hubChannel!, world, target); [ ObjectMenu.cameraFocusButtonRef[menu], diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 4c1d76e77d..bf7e1ba6f9 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -13,7 +13,11 @@ import { proxiedUrlFor, isHubsRoomUrl, isLocalHubsSceneUrl, - isLocalHubsAvatarUrl + isLocalHubsAvatarUrl, + isHubsDestinationUrl, + isHubsAvatarUrl, + hubsRoomRegex, + localHubsRoomRegex } from "../utils/media-url-utils"; import { addAnimationComponents } from "../utils/animation"; @@ -363,10 +367,16 @@ AFRAME.registerComponent("media-loader", { // We want to resolve and proxy some hubs urls, like rooms and scene links, // but want to avoid proxying assets in order for this to work in dev environments - const isLocalModelAsset = - isNonCorsProxyDomain(parsedUrl.hostname) && (guessContentType(src) || "").startsWith("model/gltf"); + const isLocalAsset = + isNonCorsProxyDomain(parsedUrl.hostname) && + !(await isHubsDestinationUrl(src)) && + !(await isHubsAvatarUrl(src)) && + !src.match(hubsRoomRegex)?.groups.id && + !src.match(localHubsRoomRegex)?.groups.id; - if (this.data.resolve && !src.startsWith("data:") && !src.startsWith("hubs:") && !isLocalModelAsset) { + console.log("IS LOCAL ASSET?", isLocalAsset, src); + + if (this.data.resolve && !src.startsWith("data:") && !src.startsWith("hubs:") && !isLocalAsset) { const is360 = !!(this.data.mediaOptions.projection && this.data.mediaOptions.projection.startsWith("360")); const quality = getDefaultResolveQuality(is360); const result = await resolveUrl(src, quality, version, forceLocalRefresh); diff --git a/src/components/pinnable.js b/src/components/pinnable.js index aa1533a746..ded6194643 100644 --- a/src/components/pinnable.js +++ b/src/components/pinnable.js @@ -1,6 +1,3 @@ -import { addComponent, removeComponent } from "bitecs"; -import { Pinnable, Pinned } from "../bit-components"; - AFRAME.registerComponent("pinnable", { schema: { pinned: { default: false } @@ -16,18 +13,10 @@ AFRAME.registerComponent("pinnable", { this.el.addEventListener("owned-pager-page-changed", this._persist); this.el.addEventListener("owned-video-state-changed", this._persistAndAnimate); - - addComponent(APP.world, Pinnable, this.el.object3D.eid); }, update() { this._animate(); - - if (this.data.pinned) { - addComponent(APP.world, Pinned, this.el.object3D.eid); - } else { - removeComponent(APP.world, Pinned, this.el.object3D.eid); - } }, _persistAndAnimate() { diff --git a/src/inflators/media-loader.ts b/src/inflators/media-loader.ts index 4bab7cb20e..b99cfa3f20 100644 --- a/src/inflators/media-loader.ts +++ b/src/inflators/media-loader.ts @@ -8,13 +8,14 @@ export type MediaLoaderParams = { resize: boolean; recenter: boolean; animateLoad: boolean; + fileId?: string; isObjectMenuTarget: boolean; }; export function inflateMediaLoader( world: HubsWorld, eid: number, - { src, recenter, resize, animateLoad, isObjectMenuTarget }: MediaLoaderParams + { src, recenter, resize, animateLoad, fileId, isObjectMenuTarget }: MediaLoaderParams ) { addComponent(world, MediaLoader, eid); let flags = 0; @@ -23,5 +24,8 @@ export function inflateMediaLoader( if (animateLoad) flags |= MEDIA_LOADER_FLAGS.ANIMATE_LOAD; if (isObjectMenuTarget) flags |= MEDIA_LOADER_FLAGS.IS_OBJECT_MENU_TARGET; MediaLoader.flags[eid] = flags; + if (fileId) { + MediaLoader.fileId[eid] = APP.getSid(fileId)!; + } MediaLoader.src[eid] = APP.getSid(src)!; } diff --git a/src/load-media-on-paste-or-drop.ts b/src/load-media-on-paste-or-drop.ts index 72400eadad..4d354d5f0e 100644 --- a/src/load-media-on-paste-or-drop.ts +++ b/src/load-media-on-paste-or-drop.ts @@ -51,6 +51,7 @@ export async function spawnFromFileList(files: FileList) { recenter: true, resize: !qsTruthy("noResize"), animateLoad: true, + fileId: response.file_id, isObjectMenuTarget: true }; }) @@ -61,6 +62,7 @@ export async function spawnFromFileList(files: FileList) { recenter: true, resize: !qsTruthy("noResize"), animateLoad: true, + fileId: null, isObjectMenuTarget: true }; }); diff --git a/src/react-components/room/object-hooks.js b/src/react-components/room/object-hooks.js index 66a7d83354..1cc4861c6a 100644 --- a/src/react-components/room/object-hooks.js +++ b/src/react-components/room/object-hooks.js @@ -3,11 +3,8 @@ import { removeNetworkedObject } from "../../utils/removeNetworkedObject"; import { rotateInPlaceAroundWorldUp, affixToWorldUp } from "../../utils/three-utils"; import { getPromotionTokenForFile } from "../../utils/media-utils"; import { hasComponent } from "bitecs"; -import { Pinnable, Pinned, Static } from "../../bit-components"; - -function getPinnedState(el) { - return !!(hasComponent(APP.world, Pinnable, el.eid) && hasComponent(APP.world, Pinned, el.eid)); -} +import { Static } from "../../bit-components"; +import { isPinned as getPinnedState } from "../../bit-systems/networking"; export function isMe(object) { return object.el.id === "avatar-rig"; @@ -31,7 +28,7 @@ export function getObjectUrl(object) { } export function usePinObject(hubChannel, scene, object) { - const [isPinned, setIsPinned] = useState(getPinnedState(object.el)); + const [isPinned, setIsPinned] = useState(getPinnedState(object.el.eid)); const pinObject = useCallback(() => { const el = object.el; @@ -57,11 +54,11 @@ export function usePinObject(hubChannel, scene, object) { const el = object.el; function onPinStateChanged() { - setIsPinned(getPinnedState(el)); + setIsPinned(getPinnedState(el.eid)); } el.addEventListener("pinned", onPinStateChanged); el.addEventListener("unpinned", onPinStateChanged); - setIsPinned(getPinnedState(el)); + setIsPinned(getPinnedState(el.eid)); return () => { el.removeEventListener("pinned", onPinStateChanged); el.removeEventListener("unpinned", onPinStateChanged); @@ -125,7 +122,7 @@ export function useRemoveObject(hubChannel, scene, object) { const canRemoveObject = !!( scene.is("entered") && !isPlayer(object) && - !getPinnedState(el) && + !getPinnedState(el.eid) && !hasComponent(APP.world, Static, el.eid) && hubChannel.can("spawn_and_move_media") ); diff --git a/src/systems/hold-system.js b/src/systems/hold-system.js index 22c4578a3c..8e4c4f094a 100644 --- a/src/systems/hold-system.js +++ b/src/systems/hold-system.js @@ -3,7 +3,6 @@ import { addComponent, removeComponent, defineQuery, hasComponent } from "bitecs import { Held, Holdable, - Pinned, HoveredRemoteRight, HeldRemoteRight, HoveredRemoteLeft, @@ -15,6 +14,7 @@ import { AEntity } from "../bit-components"; import { canMove } from "../utils/permissions-utils"; +import { isPinned } from "../bit-systems/networking"; const GRAB_REMOTE_RIGHT = paths.actions.cursor.right.grab; const DROP_REMOTE_RIGHT = paths.actions.cursor.right.drop; @@ -35,7 +35,7 @@ function grab(world, userinput, queryHovered, held, grabPath) { if ( hovered && userinput.get(grabPath) && - (!hasComponent(world, Pinned, hovered) || AFRAME.scenes[0].is("frozen")) && + (!isPinned(hovered) || AFRAME.scenes[0].is("frozen")) && hasPermissionToGrab(world, hovered) ) { addComponent(world, held, hovered); diff --git a/src/systems/userinput/devices/app-aware-touchscreen.js b/src/systems/userinput/devices/app-aware-touchscreen.js index 1c77657601..449a231a31 100644 --- a/src/systems/userinput/devices/app-aware-touchscreen.js +++ b/src/systems/userinput/devices/app-aware-touchscreen.js @@ -5,16 +5,9 @@ import { findRemoteHoverTarget } from "../../../components/cursor-controller"; // import { canMove } from "../../../utils/permissions-utils"; import ResizeObserver from "resize-observer-polyfill"; import { hasComponent } from "bitecs"; -import { - AEntity, - HeldRemoteRight, - OffersRemoteConstraint, - Pinnable, - Pinned, - SingleActionButton, - Static -} from "../../../bit-components"; +import { AEntity, HeldRemoteRight, OffersRemoteConstraint, SingleActionButton, Static } from "../../../bit-components"; import { anyEntityWith } from "../../../utils/bit-utils"; +import { isPinned } from "../../../bit-systems/networking"; const MOVE_CURSOR_JOB = "MOVE CURSOR"; const MOVE_CAMERA_JOB = "MOVE CAMERA"; @@ -70,14 +63,12 @@ function shouldMoveCursor(touch, rect, raycaster) { .get(remoteHoverTarget) .el.matches(".interactable, .interactable *, .occupiable-waypoint-icon, .teleport-waypoint-icon")); - const isPinned = - hasComponent(APP.world, Pinnable, remoteHoverTarget) && hasComponent(APP.world, Pinned, remoteHoverTarget); const isSceneFrozen = AFRAME.scenes[0].is("frozen"); // TODO isStatic is likely a superfluous check for things matched via OffersRemoteConstraint const isStatic = hasComponent(APP.world, Static, remoteHoverTarget); return ( - isSingleActionButton || (isInteractable && (isSceneFrozen || !isPinned) && !isStatic) + isSingleActionButton || (isInteractable && (isSceneFrozen || !isPinned(remoteHoverTarget)) && !isStatic) // TODO check canMove //&& (remoteHoverTarget && canMove(remoteHoverTarget)) ); diff --git a/src/utils/bit-pinning-helper.ts b/src/utils/bit-pinning-helper.ts new file mode 100644 index 0000000000..ee625aa0c0 --- /dev/null +++ b/src/utils/bit-pinning-helper.ts @@ -0,0 +1,49 @@ +import { HubsWorld } from "../app"; +import { createEntityState, deleteEntityState } from "./entity-state-utils"; +import HubChannel from "./hub-channel"; +import { EntityID } from "./networking-types"; +import { takeOwnership } from "./take-ownership"; +import { createMessageDatas, isNetworkInstantiated, isPinned } from "../bit-systems/networking"; + +export const setPinned = async (hubChannel: HubChannel, world: HubsWorld, eid: EntityID, shouldPin: boolean) => { + _signInAndPinOrUnpinElement(hubChannel, world, eid, shouldPin); +}; + +const _pinElement = async (hubChannel: HubChannel, world: HubsWorld, eid: EntityID) => { + try { + await createEntityState(hubChannel, world, eid); + takeOwnership(world, eid); + } catch (e) { + if (e.reason === "invalid_token") { + // TODO: Sign out and sign in again + console.log("PinningHelper: Pin failed due to invalid token, signing out and trying again", e); + } else { + console.warn("PinningHelper: Pin failed for unknown reason", e); + } + } +}; + +const unpinElement = (hubChannel: HubChannel, world: HubsWorld, eid: EntityID) => { + deleteEntityState(hubChannel, world, eid); +}; + +const _signInAndPinOrUnpinElement = (hubChannel: HubChannel, world: HubsWorld, eid: EntityID, shouldPin: boolean) => { + const action = shouldPin ? () => _pinElement(hubChannel, world, eid) : () => unpinElement(hubChannel, world, eid); + // TODO: Perform conditional sign in + action(); +}; + +export const canPin = (hubChannel: HubChannel, world: HubsWorld, eid: EntityID): boolean => { + const { + initialData: { fileId } + } = createMessageDatas.get(eid)!; + const hasFile = !!fileId; + const hasPromotableFile = + hasFile && APP.store.state.uploadPromotionTokens.some((upload: any) => upload.fileId === fileId); + return ( + isNetworkInstantiated(eid) && + !isPinned(eid) && + hubChannel.can("pin_objects") && // TODO: Remove once conditional sign in is implemented + (!hasFile || hasPromotableFile) + ); +}; diff --git a/src/utils/entity-state-utils.ts b/src/utils/entity-state-utils.ts index 105233629d..45b3465720 100644 --- a/src/utils/entity-state-utils.ts +++ b/src/utils/entity-state-utils.ts @@ -29,6 +29,9 @@ export type CreateEntityStatePayload = { nid: NetworkID; create_message: CreateMessage; updates: UpdateEntityStatePayload[]; + file_id?: string; + file_access_token?: string; + promotion_token?: string; }; export type DeleteEntityStatePayload = { @@ -44,13 +47,23 @@ export function hasSavedEntityState(world: HubsWorld, eid: EntityID) { export async function createEntityState(hubChannel: HubChannel, world: HubsWorld, eid: EntityID) { const payload = createEntityStatePayload(world, eid); + return createEntityStateWithPayload(hubChannel, world, payload); +} + +export async function createEntityStateWithPayload( + hubChannel: HubChannel, + world: HubsWorld, + payload: CreateEntityStatePayload +) { // console.log("save_entity_state", payload); - return push(hubChannel, "save_entity_state", payload); + return push(hubChannel, "save_entity_state", payload).catch(err => { + console.warn("Failed to save entity state", err); + }); } export async function updateEntityState(hubChannel: HubChannel, world: HubsWorld, eid: EntityID) { const payload = updateEntityStatePayload(world, eid); - // console.log("update_entity_state", payload); + // console.log("update_entity_state", payload); return push(hubChannel, "update_entity_state", payload); } @@ -64,6 +77,12 @@ export async function deleteEntityState(hubChannel: HubChannel, world: HubsWorld const payload: DeleteEntityStatePayload = { nid: APP.getString(Networked.id[eid])! as NetworkID }; + const { + initialData: { fileId } + } = createMessageDatas.get(eid)!; + if (fileId) { + payload.file_id = fileId; + } // console.log("delete_entity_state", payload); return push(hubChannel, "delete_entity_state", payload); } @@ -143,11 +162,29 @@ function createEntityStatePayload(world: HubsWorld, rootEid: EntityID): CreateEn } }); - return { + const payload = { nid: rootNid, create_message, updates - }; + } as CreateEntityStatePayload; + + const { + prefabName, + initialData: { fileId, src } + } = createMessageDatas.get(rootEid)!; + + if (prefabName == "media" && fileId && src) { + const fileAccessToken = new URL(src).searchParams.get("token") as string; + const { promotionToken } = APP.store.state.uploadPromotionTokens.find( + (upload: { fileId: string }) => upload.fileId === fileId + ); + if (promotionToken) { + payload.file_id = fileId; + payload.file_access_token = fileAccessToken; + payload.promotion_token = promotionToken; + } + } + return payload; } const networkedQuery = defineQuery([Networked]); diff --git a/src/utils/jsx-entity.ts b/src/utils/jsx-entity.ts index 565706cdd3..77ed305216 100644 --- a/src/utils/jsx-entity.ts +++ b/src/utils/jsx-entity.ts @@ -469,7 +469,6 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = { quack: createDefaultInflator(Quack), mixerAnimatable: createDefaultInflator(MixerAnimatableInitialize), loopAnimation: inflateLoopAnimationInitialize, - // inflators that create Object3Ds object3D: addObject3DComponent, slice9: inflateSlice9, diff --git a/src/utils/load-legacy-room-objects.ts b/src/utils/load-legacy-room-objects.ts index 220d3564fe..0f93374bfa 100644 --- a/src/utils/load-legacy-room-objects.ts +++ b/src/utils/load-legacy-room-objects.ts @@ -1,10 +1,9 @@ import { pendingMessages } from "../bit-systems/networking"; -import { messageForLegacyRoomObjects } from "./message-for"; -import { StorableMessage } from "./networking-types"; +import { messageForLegacyRoomObject } from "./message-for"; import { getReticulumFetchUrl } from "./phoenix-utils"; +import { CreateEntityStatePayload, createEntityStateWithPayload } from "./entity-state-utils"; type LegacyRoomObject = any; -type StoredRoomDataNode = LegacyRoomObject | StorableMessage; type StoredRoomData = { asset: { @@ -12,29 +11,58 @@ type StoredRoomData = { generator: "reticulum"; }; scenes: [{ nodes: number[]; name: "Room Objects" }]; - nodes: StoredRoomDataNode[]; + nodes: LegacyRoomObject[]; extensionsUsed: ["HUBS_components"]; }; -export function isStorableMessage(node: any): node is StorableMessage { - return !!(node.version && node.creates && node.updates && node.deletes); -} - +// "Legacy Room Objects" are objects that were pinned to the room +// before the release of the Entity State apis. +// +// We do not run any migration on the backend to transform Legacy +// Room Objects into Entity States. This means we need to load +// and handle Legacy Room Objects in the client indefinitely. +// +// First, we download the Legacy Room Objects. +// +// Then, we synthesize `CreateMessage` and `UpdateMessages` for each. +// The only type of Legacy Room Objects that are saved in the database +// are "media" objects: images, videos, models, etc that were previously +// added to the room and then "pinned" by a user. Therefore, it is easy +// for us to synthesize `CreateMessage`s using the "media" prefab. +// +// We queue the synthesized messages so that the network-receive-system +// can handle them like any other normal messages. +// +// Finally, we need each Legacy Room Object to have an associated Entity State +// record in the database. If they don't, then clients will not be able +// to update it. (For example, a user will move a pinned entity, send a request +// to reticulum to update the Entity State, and reticulum will reject the +// update because there's no matching record.) +// export async function loadLegacyRoomObjects(hubId: string) { const objectsUrl = getReticulumFetchUrl(`/${hubId}/objects.gltf`) as URL; const response = await fetch(objectsUrl); const roomData: StoredRoomData = await response.json(); - const legacyRoomObjects: LegacyRoomObject[] = roomData.nodes.filter(node => !isStorableMessage(node)); if (hubId === APP.hub!.hub_id) { - const message = messageForLegacyRoomObjects(legacyRoomObjects); - if (message) { + const legacyRoomObjects: LegacyRoomObject[] = roomData.nodes; + legacyRoomObjects.forEach(obj => { + let message = messageForLegacyRoomObject(obj); + let nid = obj.name; + let payload: CreateEntityStatePayload = { + nid, + create_message: message.creates[0], + updates: [ + { + root_nid: nid, + nid, + update_message: message.updates[0] + } + ] + }; + createEntityStateWithPayload(APP.hubChannel!, APP.world, payload); message.fromClientId = "reticulum"; - pendingMessages.push(message); - // TODO All clients must use the new loading path for this to work correctly, - // because all clients must agree on which netcode to use (hubs networking - // systems or networked aframe) for a given object. - } + }); } } diff --git a/src/utils/media-url-utils.js b/src/utils/media-url-utils.js index 7e1485a967..d2b218061c 100644 --- a/src/utils/media-url-utils.js +++ b/src/utils/media-url-utils.js @@ -185,7 +185,8 @@ async function isHubsServer(url) { const hubsSceneRegex = /https?:\/\/[^/]+\/scenes\/[a-zA-Z0-9]{7}(?:\/|$)/; const hubsAvatarRegex = /https?:\/\/[^/]+\/avatars\/(?[a-zA-Z0-9]{7})(?:\/|$)/; -const hubsRoomRegex = /(https?:\/\/)?[^/]+\/(?[a-zA-Z0-9]{7})(?:\/|$)/; +export const hubsRoomRegex = /(https?:\/\/)?[^/]+\/(?[a-zA-Z0-9]{7})(?:\/|$)/; +export const localHubsRoomRegex = /https?:\/\/[^/]+\/hub\.html\?hub_id=(?[a-zA-Z0-9]{7})/; export const isLocalHubsUrl = async url => (await isHubsServer(url)) && new URL(url).origin === document.location.origin; @@ -200,7 +201,7 @@ export const isHubsRoomUrl = async url => (await isHubsServer(url)) && !(await isHubsAvatarUrl(url)) && !(await isHubsSceneUrl(url)) && - url.match(hubsRoomRegex)?.groups.id; + !url.match(hubsRoomRegex)?.groups.id; export const isHubsDestinationUrl = async url => (await isHubsServer(url)) && ((await isHubsSceneUrl(url)) || (await isHubsRoomUrl(url))); diff --git a/src/utils/message-for.ts b/src/utils/message-for.ts index 688d182bcc..03e666e014 100644 --- a/src/utils/message-for.ts +++ b/src/utils/message-for.ts @@ -107,51 +107,46 @@ export interface LegacyRoomObject { scale: [number, number, number]; } -export function messageForLegacyRoomObjects(objects: LegacyRoomObject[]) { +export function messageForLegacyRoomObject(obj: LegacyRoomObject) { const message: Message = { creates: [], updates: [], deletes: [] }; - objects.forEach(obj => { - const nid = obj.name; - const initialData: MediaLoaderParams = { - src: obj.extensions.HUBS_components.media.src, - resize: true, - recenter: true, - animateLoad: true, - isObjectMenuTarget: true - }; - const createMessage: CreateMessage = { - version: 1, - networkId: nid, - prefabName: "media", - initialData - }; - message.creates.push(createMessage); + const nid = obj.name; + const initialData: MediaLoaderParams = { + src: obj.extensions.HUBS_components.media.src, + resize: true, + recenter: true, + animateLoad: true, + isObjectMenuTarget: true + }; + const createMessage: CreateMessage = { + version: 1, + networkId: nid, + prefabName: "media", + initialData + }; + message.creates.push(createMessage); - const updateMessage: StorableUpdateMessage = { - data: { - "networked-transform": { - version: 1, - data: { - position: obj.translation || [0, 0, 0], - rotation: obj.rotation || [0, 0, 0, 1], - scale: obj.scale || [1, 1, 1] - } + const updateMessage: StorableUpdateMessage = { + data: { + "networked-transform": { + version: 1, + data: { + position: obj.translation || [0, 0, 0], + rotation: obj.rotation || [0, 0, 0, 1], + scale: obj.scale || [1, 1, 1] } - }, - nid, - lastOwnerTime: 1, - timestamp: 1, - owner: "reticulum" - }; - message.updates.push(updateMessage); - }); + } + }, + nid, + lastOwnerTime: 1, + timestamp: 1, + owner: "reticulum" + }; + message.updates.push(updateMessage); - if (message.creates.length || message.updates.length) { - return message; - } - return null; + return message; }