Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pinning/Unpinning interaction and media promotion checks for bitECS + entity state API #6092

Merged
merged 29 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5525b2e
Functioning pin behavior sans permissions
May 18, 2023
28e3138
Pinned component add/remove based on network response
May 19, 2023
938806f
Only display pin if file is remote or owned
May 19, 2023
767acb6
Persist pinned component in temporary place for testing
May 19, 2023
97e6313
Add Pinned component during room join on items created by reticulum
May 22, 2023
61cd04f
Remove pinnable system
May 22, 2023
713881a
Clean-up and more performant button visibility checks
May 23, 2023
37603ba
Access APP less, relegate sign-in prompts to TODOs
May 23, 2023
f3758df
Merge branch 'master' into ecs-pin-permissions
May 23, 2023
2106e2b
Remove accidental import
May 23, 2023
5fac68d
Fix for misunderstanding about object menu rendering
May 24, 2023
f61ee78
Move file information from MediaLoaded to new FileInfo component
May 24, 2023
4ae4721
Move pin permissions logic out of ObjectMenu system
May 24, 2023
29fe0ec
Check for necessary components in setPinned
May 25, 2023
335432d
Handle public links in isPinnable
May 25, 2023
8febdbd
Double bang to force flag to boolean
May 25, 2023
5623381
Use EntityID type and move payload construction to createEntityStateP…
May 26, 2023
c60d1ca
prettier ignore on loaderForMediaType, correct delimiter
Jun 1, 2023
96e456b
Remove leftover line from MediaLoaded data handling
Jun 1, 2023
0fca373
Derive state based on network messages, remove FileInfo
Jun 1, 2023
93cf42d
Clean-up unnecessary changes and remove Pinnable component
Jun 2, 2023
8fcc847
Remove Pinnable and Pinned component to prevent confusion, modify how…
Jun 2, 2023
aaa9df3
Use alias to fix naming collision between react state and utility fun…
Jun 2, 2023
39d3b83
Passing file id in order to set inactive
Jun 2, 2023
826ec75
Save entity state for legacy objects
Jun 13, 2023
11594ae
Store pending legacy object saves in dedicated array
Jun 14, 2023
f2e69d7
Merge remote-tracking branch 'origin/master' into ecs-pin-permissions
johnshaughnessy Jun 29, 2023
33dca22
Create entity_state for legacy room objects immediately
johnshaughnessy Jul 1, 2023
3769682
Merge remote-tracking branch 'origin/master' into ecs-pin-permissions
johnshaughnessy Jul 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/bit-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,17 @@ 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;
export const MediaLoaded = defineComponent();
MediaLoader.fileId[$isStringType] = true;
export const MediaLoaded = defineComponent({
fileId: Types.ui8,
src: Types.ui32
});
MediaLoaded.fileId[$isStringType] = true;
MediaLoaded.src[$isStringType] = true;
stalgiag marked this conversation as resolved.
Show resolved Hide resolved
export const MediaContentBounds = defineComponent({
bounds: [Types.f32, 3]
});
Expand Down
8 changes: 6 additions & 2 deletions src/bit-systems/media-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ const loaderForMediaType = {
{ 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.AUDIO]: (world: HubsWorld, { accessibleUrl }: { accessibleUrl: string }) =>
loadAudio(world, accessibleUrl),
[MediaType.HTML]: (world: HubsWorld, { canonicalUrl, thumbnail }: { canonicalUrl: string; thumbnail: string }) =>
stalgiag marked this conversation as resolved.
Show resolved Hide resolved
stalgiag marked this conversation as resolved.
Show resolved Hide resolved
loadHtml(world, canonicalUrl, thumbnail)
};

Expand Down Expand Up @@ -216,6 +217,9 @@ function* loadAndAnimateMedia(world: HubsWorld, eid: EntityID, clearRollbacks: C
if (MediaLoader.flags[eid] & MEDIA_LOADER_FLAGS.ANIMATE_LOAD) {
yield* animateScale(world, media);
}
addComponent(world, MediaLoaded, eid);
stalgiag marked this conversation as resolved.
Show resolved Hide resolved
MediaLoaded.fileId[eid] = MediaLoader.fileId[eid];
MediaLoaded.src[eid] = MediaLoader.src[eid];
removeComponent(world, MediaLoader, eid);

if (media) {
Expand Down
8 changes: 6 additions & 2 deletions src/bit-systems/network-receive-system.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { addComponent, defineQuery, enterQuery, hasComponent, removeComponent, removeEntity } from "bitecs";
import { HubsWorld } from "../app";
import { Networked, Owned } from "../bit-components";
import { Networked, Owned, Pinnable, Pinned } from "../bit-components";
import { renderAsNetworkedEntity } from "../utils/create-networked-entity";
import { deleteEntityState, hasSavedEntityState } from "../utils/entity-state-utils";
import { networkableComponents, schemas, StoredComponent } from "../utils/network-schemas";
Expand All @@ -12,6 +12,7 @@ import {
createMessageDatas,
disconnectedClientIds,
isNetworkInstantiated,
isPinned,
localClientID,
networkedQuery,
pendingCreatorChanges,
Expand Down Expand Up @@ -141,7 +142,6 @@ export function networkReceiveSystem(world: HubsWorld) {
world.ignoredNids.add(nid);
continue;
}

renderAsNetworkedEntity(world, prefabName, initialData, nidString, creator);
}
}
Expand Down Expand Up @@ -229,6 +229,10 @@ export function networkReceiveSystem(world: HubsWorld) {
removeComponent(world, Owned, eid);
}

if (isPinned(eid) && hasComponent(world, Pinnable, eid)) {
addComponent(world, Pinned, eid);
}

Networked.owner[eid] = APP.getSid(updateMessage.owner);
Networked.lastOwnerTime[eid] = updateMessage.lastOwnerTime;
Networked.timestamp[eid] = updateMessage.timestamp;
Expand Down
26 changes: 19 additions & 7 deletions src/bit-systems/object-menu.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { defineQuery, enterQuery, entityExists, exitQuery, hasComponent } from "bitecs";
import { addComponent, defineQuery, enterQuery, entityExists, exitQuery, hasComponent, removeComponent } from "bitecs";
import { Matrix4, Quaternion, Vector3 } from "three";
import type { HubsWorld } from "../app";
import {
HeldRemoteRight,
HoveredRemoteRight,
Interacted,
MediaLoaded,
ObjectMenu,
ObjectMenuTarget,
Pinnable,
Pinned,
RemoteRight,
Rigidbody
} 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";
import { deleteTheDeletableAncestor } from "./delete-entity-system";
import { createMessageDatas, isPinned } from "./networking";
import { createMessageDatas } from "./networking";
import { TRANSFORM_MODE } from "../components/transform-object-button";
import { ScalingHandler } from "../components/scale-button";
import { setPinned } from "../utils/bit-pinning-helper";
import { getPromotionTokenForFile } from "../utils/media-utils";
import { get } from "react-hook-form";

// Working variables.
const _vec3_1 = new Vector3();
Expand Down Expand Up @@ -148,9 +153,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])) {
Expand Down Expand Up @@ -203,8 +208,15 @@ 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);
if (hasComponent(world, Pinnable, target)) {
if (hasComponent(world, Pinned, target)) {
world.eid2obj.get(ObjectMenu.unpinButtonRef[menu])!.visible = visible;
} else {
const fileId = APP.getString(MediaLoaded.fileId[target]);
const canPin = !fileId || getPromotionTokenForFile(fileId);
world.eid2obj.get(ObjectMenu.pinButtonRef[menu])!.visible = visible && canPin;
}
}

[
ObjectMenu.cameraFocusButtonRef[menu],
Expand Down
6 changes: 5 additions & 1 deletion src/inflators/media-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)!;
}
2 changes: 2 additions & 0 deletions src/load-media-on-paste-or-drop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function spawnFromFileList(files: FileList) {
recenter: true,
resize: !qsTruthy("noResize"),
animateLoad: true,
fileId: response.file_id,
isObjectMenuTarget: true
};
})
Expand All @@ -61,6 +62,7 @@ export async function spawnFromFileList(files: FileList) {
recenter: true,
resize: !qsTruthy("noResize"),
animateLoad: true,
fileId: null,
stalgiag marked this conversation as resolved.
Show resolved Hide resolved
isObjectMenuTarget: true
};
});
Expand Down
1 change: 1 addition & 0 deletions src/prefabs/media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function MediaPrefab(params: MediaLoaderParams): EntityDef {
networkedTransform
mediaLoader={params}
deletable
pinnable
grabbable={{ cursor: true, hand: true }}
destroyAtExtremeDistance
floatyObject={{
Expand Down
49 changes: 49 additions & 0 deletions src/utils/bit-pinning-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { HubsWorld } from "../app";
import { MediaLoaded } from "../bit-components";
import { createEntityState, deleteEntityState } from "./entity-state-utils";
import HubChannel from "./hub-channel";
import { takeOwnership } from "./take-ownership";

export const setPinned = async (hubChannel: HubChannel, world: HubsWorld, eid: number, shouldPin: boolean) => {
_signInAndPinOrUnpinElement(hubChannel, world, eid, shouldPin);
};

const _pinElement = async (hubChannel: HubChannel, world: HubsWorld, eid: number) => {
const src = APP.getString(MediaLoaded.src[eid]);
const fileId = APP.getString(MediaLoaded.fileId[eid]);
let fileAccessToken, promotionToken;
if (src) {
fileAccessToken = new URL(src).searchParams.get("token") as string;
const storedPromotionToken = APP.store.state.uploadPromotionTokens.find(
(upload: { fileId: string }) => upload.fileId === fileId
);
if (storedPromotionToken) {
promotionToken = storedPromotionToken.promotionToken;
}
}

try {
await createEntityState(hubChannel, world, eid, fileId!, fileAccessToken, promotionToken);
takeOwnership(world, eid);
} catch (e) {
if (e.reason === "invalid_token") {
// TODO: Sign out and sign in again
// signInAndPinOrUnpinElement(el);
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: number) => {
deleteEntityState(hubChannel, world, eid);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the way legacy room objects are handled, I think we should createEntityState in response to loading legacy room objects. Otherwise, when a client loads a legacy room object and synthesizes a "fake" create entity message for it, if the client moves the entity around, it will send "update" messages to reticulum, and reticulum will reject them (because it would have no stored entity state for it). This might be wasteful, so we may consider making an atomic "move RoomObject to EntityState" method later. But because I am concerned about an error causing us to lose information, I thought to do this non-destructively first. (And then delete both the RoomObject and the EntityState when an element is unpinned.)

};

const _signInAndPinOrUnpinElement = (hubChannel: HubChannel, world: HubsWorld, eid: number, shouldPin: boolean) => {
const action = shouldPin ? () => _pinElement(hubChannel, world, eid) : () => unpinElement(hubChannel, world, eid);

// TODO: Perform conditional sign in

action();
};
19 changes: 17 additions & 2 deletions src/utils/entity-state-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -42,15 +45,27 @@ export function hasSavedEntityState(world: HubsWorld, eid: EntityID) {
});
}

export async function createEntityState(hubChannel: HubChannel, world: HubsWorld, eid: EntityID) {
export async function createEntityState(
hubChannel: HubChannel,
world: HubsWorld,
eid: EntityID,
fileId?: string,
fileAccessToken?: string,
promotionToken?: string
) {
const payload = createEntityStatePayload(world, eid);
if (fileId && promotionToken) {
payload.file_id = fileId;
payload.file_access_token = fileAccessToken;
payload.promotion_token = promotionToken;
}
// console.log("save_entity_state", payload);
return push(hubChannel, "save_entity_state", payload);
}

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);
}

Expand Down
5 changes: 4 additions & 1 deletion src/utils/jsx-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import {
VideoTextureSource,
Quack,
Mirror,
MixerAnimatableInitialize
MixerAnimatableInitialize,
Pinnable
} from "../bit-components";
import { inflateMediaLoader } from "../inflators/media-loader";
import { inflateMediaFrame } from "../inflators/media-frame";
Expand Down Expand Up @@ -311,6 +312,7 @@ export interface JSXComponentData extends ComponentData {
rigidbody?: OptionalParams<RigidBodyParams>;
physicsShape?: OptionalParams<PhysicsShapeParams>;
floatyObject?: any;
pinnable?: any;
networkedFloatyObject?: any;
networkedTransform?: any;
objectMenu?: {
Expand Down Expand Up @@ -451,6 +453,7 @@ const jsxInflators: Required<{ [K in keyof JSXComponentData]: InflatorFn }> = {
networkedTransform: createDefaultInflator(NetworkedTransform),
networked: createDefaultInflator(Networked),
objectMenu: createDefaultInflator(ObjectMenu),
pinnable: createDefaultInflator(Pinnable),
linkHoverMenu: createDefaultInflator(LinkHoverMenu),
linkHoverMenuItem: createDefaultInflator(LinkHoverMenuItem),
pdfMenu: createDefaultInflator(PDFMenu),
Expand Down
16 changes: 14 additions & 2 deletions src/utils/listen-for-network-messages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { addComponent, hasComponent, removeComponent } from "bitecs";
import {
connectedClientIds,
disconnectedClientIds,
Expand All @@ -9,6 +10,7 @@ import {
} from "../bit-systems/networking";
import { EntityState } from "./entity-state-utils";
import type { ClientID, CreatorChange, Message } from "./networking-types";
import { Pinnable, Pinned } from "../bit-components";

type Emitter = {
on: (event: string, callback: (a: any) => any) => number;
Expand Down Expand Up @@ -91,7 +93,13 @@ export function queueEntityStateAsMessage(entityState: EntityState) {
}

function onEntityStateCreated(response: { data: EntityState[] }) {
// console.log("entity_state_saved", response);
const rootNid = APP.getSid(response.data[0]!.create_message.networkId);
const eid = APP.world.nid2eid.get(rootNid);
if (eid) {
if (hasComponent(APP.world, Pinnable, eid)) {
addComponent(APP.world, Pinned, eid);
}
}
queueEntityStateAsMessage(response.data[0]!);
}

Expand All @@ -100,6 +108,10 @@ function onEntityStateUpdated(_response: any) {
}

function onEntityStateDeleted(response: CreatorChange) {
// console.log("entity_state_deleted", response);
const rootNid = APP.getSid(response.nid);
const eid = APP.world.nid2eid.get(rootNid);
if (eid) {
removeComponent(APP.world, Pinned, eid);
}
stalgiag marked this conversation as resolved.
Show resolved Hide resolved
pendingCreatorChanges.push(response);
}