зеркало из https://github.com/mozilla/hubs.git
Merge pull request #5814 from mozilla/feature/networking-system-refactor
Feature/networking system refactor
This commit is contained in:
Коммит
5a4d3b54de
|
@ -8,7 +8,7 @@ import { MediaType, mediaTypeName, resolveMediaInfo } from "../utils/media-utils
|
||||||
import { defineQuery, enterQuery, exitQuery, hasComponent, removeComponent, removeEntity } from "bitecs";
|
import { defineQuery, enterQuery, exitQuery, hasComponent, removeComponent, removeEntity } from "bitecs";
|
||||||
import { MediaLoader, Networked } from "../bit-components";
|
import { MediaLoader, Networked } from "../bit-components";
|
||||||
import { crTimeout, crClearTimeout, cancelable, coroutine, makeCancelable } from "../utils/coroutine";
|
import { crTimeout, crClearTimeout, cancelable, coroutine, makeCancelable } from "../utils/coroutine";
|
||||||
import { takeOwnership } from "./networking";
|
import { takeOwnership } from "../utils/take-ownership";
|
||||||
import { renderAsEntity } from "../utils/jsx-entity";
|
import { renderAsEntity } from "../utils/jsx-entity";
|
||||||
import { animate } from "../utils/animate";
|
import { animate } from "../utils/animate";
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { addComponent, defineQuery, enterQuery, hasComponent, removeComponent, removeEntity } from "bitecs";
|
||||||
|
import { HubsWorld } from "../app";
|
||||||
|
import { Networked, Owned } from "../bit-components";
|
||||||
|
import { createNetworkedEntityFromRemote } from "../utils/create-networked-entity";
|
||||||
|
import { networkableComponents, schemas } from "../utils/network-schemas";
|
||||||
|
import type { StringID, UpdateMessage } from "../utils/networking-types";
|
||||||
|
import { hasPermissionToSpawn } from "../utils/permissions";
|
||||||
|
import { takeOwnershipWithTime } from "../utils/take-ownership-with-time";
|
||||||
|
import {
|
||||||
|
createMessageDatas,
|
||||||
|
isNetworkInstantiated,
|
||||||
|
localClientID,
|
||||||
|
networkedQuery,
|
||||||
|
pendingMessages,
|
||||||
|
pendingParts
|
||||||
|
} from "./networking";
|
||||||
|
|
||||||
|
const partedClientIds = new Set<StringID>();
|
||||||
|
const storedUpdates = new Map<StringID, UpdateMessage[]>();
|
||||||
|
const enteredNetworkedQuery = enterQuery(defineQuery([Networked]));
|
||||||
|
export function networkReceiveSystem(world: HubsWorld) {
|
||||||
|
if (!localClientID) return; // Not connected yet.
|
||||||
|
|
||||||
|
{
|
||||||
|
// When a user leaves, remove the entities created by that user
|
||||||
|
const networkedEntities = networkedQuery(world);
|
||||||
|
pendingParts.forEach(partingClientId => {
|
||||||
|
partedClientIds.add(partingClientId);
|
||||||
|
|
||||||
|
networkedEntities
|
||||||
|
.filter(eid => isNetworkInstantiated(eid) && Networked.creator[eid] === partingClientId)
|
||||||
|
.forEach(eid => removeEntity(world, eid));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were hanging onto updates for any newly created non network instantiated entities
|
||||||
|
// we can now apply them. Network instantiated entities are handled when processing creates.
|
||||||
|
enteredNetworkedQuery(world).forEach(eid => {
|
||||||
|
const nid = Networked.id[eid];
|
||||||
|
if (storedUpdates.has(nid)) {
|
||||||
|
console.log("Had stored updates for", APP.getString(nid), storedUpdates.get(nid));
|
||||||
|
const updates = storedUpdates.get(nid)!;
|
||||||
|
|
||||||
|
for (let i = 0; i < updates.length; i++) {
|
||||||
|
const update = updates[i];
|
||||||
|
if (partedClientIds.has(APP.getSid(update.owner))) {
|
||||||
|
console.log("Rewriting update message from client who left.", JSON.stringify(update));
|
||||||
|
update.owner = NAF.clientId;
|
||||||
|
update.lastOwnerTime = update.timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingMessages.unshift({ creates: [], updates, deletes: [] });
|
||||||
|
storedUpdates.delete(nid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < pendingMessages.length; i++) {
|
||||||
|
const message = pendingMessages[i];
|
||||||
|
|
||||||
|
for (let j = 0; j < message.creates.length; j++) {
|
||||||
|
const [nidString, prefabName, initialData] = message.creates[j];
|
||||||
|
const creator = message.fromClientId;
|
||||||
|
if (!creator) {
|
||||||
|
// We do not expect to get here.
|
||||||
|
// We only check because we are synthesizing messages elsewhere;
|
||||||
|
// They should not have any create messages in them.
|
||||||
|
throw new Error("Received create message without a fromClientId.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const nid = APP.getSid(nidString);
|
||||||
|
|
||||||
|
if (world.deletedNids.has(nid)) {
|
||||||
|
// TODO we may need to allow this for reconnects
|
||||||
|
console.log(`Received a create message for an entity I've already deleted. Skipping ${nidString}`);
|
||||||
|
} else if (world.nid2eid.has(nid)) {
|
||||||
|
console.log(`Received create message for entity I already created. Skipping ${nidString}`);
|
||||||
|
} else if (!hasPermissionToSpawn(creator, prefabName)) {
|
||||||
|
// this should only ever happen if there is a bug or the sender is maliciously modified
|
||||||
|
console.log(`Received create from a user who does not have permission to spawn ${prefabName}`);
|
||||||
|
world.ignoredNids.add(nid); // TODO should we just use deletedNids for this?
|
||||||
|
} else {
|
||||||
|
const eid = createNetworkedEntityFromRemote(world, prefabName, initialData, nidString, creator, creator);
|
||||||
|
console.log("got create message for", nidString, eid);
|
||||||
|
|
||||||
|
// If we were hanging onto updates for this nid we can now apply them. And they should be processed before other updates.
|
||||||
|
if (storedUpdates.has(nid)) {
|
||||||
|
console.log("had pending updates for", nidString, storedUpdates.get(nid));
|
||||||
|
Array.prototype.unshift.apply(message.updates, storedUpdates.get(nid));
|
||||||
|
storedUpdates.delete(nid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < message.updates.length; j++) {
|
||||||
|
const updateMessage = message.updates[j];
|
||||||
|
const nid = APP.getSid(updateMessage.nid);
|
||||||
|
|
||||||
|
if (world.ignoredNids.has(nid)) {
|
||||||
|
console.log(`Ignoring update for ignored entity ${updateMessage.nid}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (world.deletedNids.has(nid)) {
|
||||||
|
console.log(`Ignoring update for deleted entity ${updateMessage.nid}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!world.nid2eid.has(nid)) {
|
||||||
|
console.log(`Holding onto an update for ${updateMessage.nid} because we don't have it yet.`);
|
||||||
|
// TODO: What if we will NEVER be able to apply this update?
|
||||||
|
// TODO would be nice if we could squash these updates
|
||||||
|
const updates = storedUpdates.get(nid) || [];
|
||||||
|
updates.push(updateMessage);
|
||||||
|
storedUpdates.set(nid, updates);
|
||||||
|
console.log(storedUpdates);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eid = world.nid2eid.get(nid)!;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Networked.lastOwnerTime[eid] > updateMessage.lastOwnerTime ||
|
||||||
|
(Networked.lastOwnerTime[eid] === updateMessage.lastOwnerTime &&
|
||||||
|
APP.getString(Networked.owner[eid])! < updateMessage.owner) // arbitrary (but consistent) tiebreak
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"Received update from an old owner, skipping",
|
||||||
|
updateMessage.nid,
|
||||||
|
Networked.lastOwnerTime[eid],
|
||||||
|
updateMessage.lastOwnerTime
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateMessage.owner === NAF.clientId) {
|
||||||
|
console.log("Got a message telling us we are the owner.");
|
||||||
|
addComponent(world, Owned, eid);
|
||||||
|
} else if (hasComponent(world, Owned, eid)) {
|
||||||
|
console.log("Lost ownership: ", updateMessage.nid);
|
||||||
|
removeComponent(world, Owned, eid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Networked.creator[eid] = APP.getSid(updateMessage.creator);
|
||||||
|
Networked.owner[eid] = APP.getSid(updateMessage.owner);
|
||||||
|
Networked.lastOwnerTime[eid] = updateMessage.lastOwnerTime;
|
||||||
|
Networked.timestamp[eid] = updateMessage.timestamp;
|
||||||
|
|
||||||
|
// TODO HACK simulating a buffer with a cursor using an array
|
||||||
|
updateMessage.data.cursor = 0;
|
||||||
|
for (let s = 0; s < updateMessage.componentIds.length; s++) {
|
||||||
|
const componentId = updateMessage.componentIds[s];
|
||||||
|
const schema = schemas.get(networkableComponents[componentId])!;
|
||||||
|
schema.deserialize(world, eid, updateMessage.data);
|
||||||
|
}
|
||||||
|
delete updateMessage.data.cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < message.deletes.length; j += 1) {
|
||||||
|
const nid = APP.getSid(message.deletes[j]);
|
||||||
|
if (world.deletedNids.has(nid)) continue;
|
||||||
|
|
||||||
|
world.deletedNids.add(nid);
|
||||||
|
const eid = world.nid2eid.get(nid)!;
|
||||||
|
createMessageDatas.delete(eid);
|
||||||
|
world.nid2eid.delete(nid);
|
||||||
|
removeEntity(world, eid);
|
||||||
|
|
||||||
|
console.log("Deleting ", APP.getString(nid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingMessages.length = 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
const networkedEntities = networkedQuery(world);
|
||||||
|
pendingParts.forEach(partingClientId => {
|
||||||
|
networkedEntities
|
||||||
|
.filter(eid => Networked.owner[eid] === partingClientId)
|
||||||
|
.forEach(eid => {
|
||||||
|
takeOwnershipWithTime(world, eid, Networked.timestamp[eid]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pendingParts.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO If there's a scene-owned entity, we should take ownership of it
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { defineQuery, enterQuery, exitQuery } from "bitecs";
|
||||||
|
import { HubsWorld } from "../app";
|
||||||
|
import { Networked, Owned } from "../bit-components";
|
||||||
|
import { getServerTime } from "../phoenix-adapter";
|
||||||
|
import { messageFor } from "../utils/message-for";
|
||||||
|
import type { EntityID } from "../utils/networking-types";
|
||||||
|
import { createMessageDatas, isNetworkInstantiated, localClientID, networkedQuery, pendingJoins } from "./networking";
|
||||||
|
|
||||||
|
function isNetworkInstantiatedByMe(eid: EntityID) {
|
||||||
|
return isNetworkInstantiated(eid) && Networked.creator[eid] === APP.getSid(NAF.clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticksPerSecond = 12;
|
||||||
|
const millisecondsBetweenTicks = 1000 / ticksPerSecond;
|
||||||
|
let nextTick = 0;
|
||||||
|
|
||||||
|
const ownedNetworkedQuery = defineQuery([Owned, Networked]);
|
||||||
|
const enteredNetworkedQuery = enterQuery(networkedQuery);
|
||||||
|
const enteredOwnedNetworkedQuery = enterQuery(ownedNetworkedQuery);
|
||||||
|
const exitedNetworkedQuery = exitQuery(networkedQuery);
|
||||||
|
|
||||||
|
export function networkSendSystem(world: HubsWorld) {
|
||||||
|
if (!localClientID) return; // Not connected yet
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
if (now < nextTick) return;
|
||||||
|
|
||||||
|
if (now < nextTick + millisecondsBetweenTicks) {
|
||||||
|
nextTick = nextTick + millisecondsBetweenTicks; // The usual case
|
||||||
|
} else {
|
||||||
|
// An unusually long delay happened
|
||||||
|
nextTick = now + millisecondsBetweenTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// TODO: Ensure getServerTime() is monotonically increasing.
|
||||||
|
// TODO: Get the server time from the websocket connection
|
||||||
|
// before we start sending any messages, in case of large local clock skew.
|
||||||
|
const timestamp = getServerTime();
|
||||||
|
ownedNetworkedQuery(world).forEach(eid => {
|
||||||
|
Networked.timestamp[eid] = timestamp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell joining users about entities I network instantiated, and full updates for entities I own
|
||||||
|
{
|
||||||
|
if (pendingJoins.length) {
|
||||||
|
const ownedNetworkedEntities = ownedNetworkedQuery(world);
|
||||||
|
const message = messageFor(
|
||||||
|
world,
|
||||||
|
networkedQuery(world).filter(isNetworkInstantiatedByMe),
|
||||||
|
ownedNetworkedEntities,
|
||||||
|
ownedNetworkedEntities,
|
||||||
|
[],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
if (message) {
|
||||||
|
pendingJoins.forEach(clientId => NAF.connection.sendDataGuaranteed(APP.getString(clientId)!, "nn", message));
|
||||||
|
}
|
||||||
|
pendingJoins.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell everyone about entities I created, entities I own, and entities that were deleted
|
||||||
|
{
|
||||||
|
// Note: Many people may send delete messages about the same entity
|
||||||
|
const deletedEntities = exitedNetworkedQuery(world).filter(eid => {
|
||||||
|
return !world.deletedNids.has(Networked.id[eid]) && isNetworkInstantiated(eid);
|
||||||
|
});
|
||||||
|
const message = messageFor(
|
||||||
|
world,
|
||||||
|
enteredNetworkedQuery(world).filter(isNetworkInstantiatedByMe),
|
||||||
|
ownedNetworkedQuery(world),
|
||||||
|
enteredOwnedNetworkedQuery(world),
|
||||||
|
deletedEntities,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (message) NAF.connection.broadcastDataGuaranteed("nn", message);
|
||||||
|
|
||||||
|
deletedEntities.forEach(eid => {
|
||||||
|
createMessageDatas.delete(eid);
|
||||||
|
world.deletedNids.add(Networked.id[eid]);
|
||||||
|
world.nid2eid.delete(Networked.id[eid]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,518 +1,15 @@
|
||||||
import {
|
import { defineQuery } from "bitecs";
|
||||||
addComponent,
|
import { Networked } from "../bit-components";
|
||||||
Component,
|
import type { ClientID, CreateMessageData, EntityID, Message, StringID } from "../utils/networking-types";
|
||||||
defineQuery,
|
export let localClientID: ClientID | null = null;
|
||||||
enterQuery,
|
|
||||||
exitQuery,
|
|
||||||
hasComponent,
|
|
||||||
removeComponent,
|
|
||||||
removeEntity
|
|
||||||
} from "bitecs";
|
|
||||||
import { HubsWorld } from "../app";
|
|
||||||
import { AEntity, Networked, NetworkedMediaFrame, NetworkedTransform, NetworkedVideo, Owned } from "../bit-components";
|
|
||||||
import { getServerTime } from "../phoenix-adapter";
|
|
||||||
import { CameraPrefab, CubeMediaFramePrefab } from "../prefabs/camera-tool";
|
|
||||||
import { MediaPrefab } from "../prefabs/media";
|
|
||||||
import { defineNetworkSchema } from "../utils/bit-utils";
|
|
||||||
import { renderAsEntity } from "../utils/jsx-entity";
|
|
||||||
|
|
||||||
const prefabs = new Map(
|
|
||||||
Object.entries({
|
|
||||||
camera: {
|
|
||||||
permission: "spawn_camera",
|
|
||||||
template: CameraPrefab
|
|
||||||
},
|
|
||||||
cube: {
|
|
||||||
template: CubeMediaFramePrefab
|
|
||||||
},
|
|
||||||
media: {
|
|
||||||
template: MediaPrefab
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
type EntityID = number;
|
|
||||||
|
|
||||||
export function takeOwnershipWithTime(world: HubsWorld, eid: EntityID, timestamp: number) {
|
|
||||||
if (hasComponent(world, AEntity, eid)) {
|
|
||||||
throw new Error("Cannot take ownership of AEntities with a specific timestamp.");
|
|
||||||
}
|
|
||||||
|
|
||||||
addComponent(world, Owned, eid);
|
|
||||||
Networked.lastOwnerTime[eid] = timestamp;
|
|
||||||
Networked.owner[eid] = APP.getSid(NAF.clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
let localClientID: ClientID | null = null;
|
|
||||||
export function setLocalClientID(clientID: ClientID) {
|
export function setLocalClientID(clientID: ClientID) {
|
||||||
localClientID = clientID;
|
localClientID = clientID;
|
||||||
}
|
}
|
||||||
|
export const createMessageDatas: Map<EntityID, CreateMessageData> = new Map();
|
||||||
export function takeOwnership(world: HubsWorld, eid: EntityID) {
|
export const networkedQuery = defineQuery([Networked]);
|
||||||
// TODO we do this to have a single API for taking ownership of things in new code, but it obviously relies on NAF/AFrame
|
export const pendingMessages: Message[] = [];
|
||||||
if (hasComponent(world, AEntity, eid)) {
|
export const pendingJoins: StringID[] = [];
|
||||||
const el = world.eid2obj.get(eid)!.el!;
|
export const pendingParts: StringID[] = [];
|
||||||
!NAF.utils.isMine(el) && NAF.utils.takeOwnership(el);
|
export function isNetworkInstantiated(eid: EntityID) {
|
||||||
} else {
|
|
||||||
addComponent(world, Owned, eid);
|
|
||||||
Networked.lastOwnerTime[eid] = Math.max(getServerTime(), Networked.lastOwnerTime[eid] + 1);
|
|
||||||
Networked.owner[eid] = APP.getSid(NAF.clientId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateMessageData {
|
|
||||||
prefabName: string;
|
|
||||||
initialData: InitialData;
|
|
||||||
}
|
|
||||||
const createMessageDatas: Map<EntityID, CreateMessageData> = new Map();
|
|
||||||
|
|
||||||
type InitialData = any;
|
|
||||||
|
|
||||||
export function createNetworkedEntityFromRemote(
|
|
||||||
world: HubsWorld,
|
|
||||||
prefabName: string,
|
|
||||||
initialData: InitialData,
|
|
||||||
rootNid: string,
|
|
||||||
creator: ClientID,
|
|
||||||
owner: ClientID
|
|
||||||
) {
|
|
||||||
const eid = renderAsEntity(world, prefabs.get(prefabName)!.template(initialData));
|
|
||||||
const obj = world.eid2obj.get(eid)!;
|
|
||||||
|
|
||||||
createMessageDatas.set(eid, { prefabName, initialData });
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
obj.traverse(function (o) {
|
|
||||||
if (o.eid && hasComponent(world, Networked, o.eid)) {
|
|
||||||
const eid = o.eid;
|
|
||||||
Networked.id[eid] = APP.getSid(i === 0 ? rootNid : `${rootNid}.${i}`);
|
|
||||||
APP.world.nid2eid.set(Networked.id[eid], eid);
|
|
||||||
Networked.creator[eid] = APP.getSid(creator);
|
|
||||||
Networked.owner[eid] = APP.getSid(owner);
|
|
||||||
if (NAF.clientId === owner) takeOwnership(world, eid);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
AFRAME.scenes[0].object3D.add(obj);
|
|
||||||
return eid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnAllowed(creator: ClientID, prefabName: string) {
|
|
||||||
const perm = prefabs.get(prefabName)!.permission;
|
|
||||||
return !perm || APP.hubChannel!.userCan(creator, perm);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createNetworkedEntity(world: HubsWorld, prefabName: string, initialData: InitialData) {
|
|
||||||
if (!spawnAllowed(NAF.clientId, prefabName)) throw new Error(`You do not have permission to spawn ${prefabName}`);
|
|
||||||
const rootNid = NAF.utils.createNetworkId();
|
|
||||||
return createNetworkedEntityFromRemote(world, prefabName, initialData, rootNid, NAF.clientId, NAF.clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const networkedEntitiesQuery = defineQuery([Networked]);
|
|
||||||
const ownedNetworkedEntitiesQuery = defineQuery([Networked, Owned]);
|
|
||||||
|
|
||||||
interface NetworkSchema {
|
|
||||||
serialize: (
|
|
||||||
world: HubsWorld,
|
|
||||||
eid: EntityID,
|
|
||||||
data: CursorBuffer,
|
|
||||||
isFullSync: boolean,
|
|
||||||
writeToShadow: boolean
|
|
||||||
) => boolean;
|
|
||||||
deserialize: (world: HubsWorld, eid: EntityID, data: CursorBuffer) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemas: Map<Component, NetworkSchema> = new Map();
|
|
||||||
schemas.set(NetworkedMediaFrame, defineNetworkSchema(NetworkedMediaFrame));
|
|
||||||
schemas.set(NetworkedTransform, defineNetworkSchema(NetworkedTransform));
|
|
||||||
schemas.set(NetworkedVideo, defineNetworkSchema(NetworkedVideo));
|
|
||||||
const networkableComponents = Array.from(schemas.keys());
|
|
||||||
|
|
||||||
type ClientID = string;
|
|
||||||
type NetworkID = string;
|
|
||||||
type CreateMessage = [networkId: NetworkID, prefabName: string, initialData: InitialData];
|
|
||||||
type UpdateMessage = {
|
|
||||||
nid: NetworkID;
|
|
||||||
lastOwnerTime: number;
|
|
||||||
timestamp: number;
|
|
||||||
owner: ClientID;
|
|
||||||
creator: ClientID;
|
|
||||||
componentIds: number[];
|
|
||||||
data: CursorBuffer;
|
|
||||||
};
|
|
||||||
type CursorBuffer = { cursor?: number; push: (data: any) => {} };
|
|
||||||
type DeleteMessage = NetworkID;
|
|
||||||
interface Message {
|
|
||||||
fromClientId?: ClientID;
|
|
||||||
creates: CreateMessage[];
|
|
||||||
updates: UpdateMessage[];
|
|
||||||
deletes: DeleteMessage[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingMessages: Message[] = [];
|
|
||||||
type StringID = number;
|
|
||||||
const pendingJoins: StringID[] = [];
|
|
||||||
const pendingParts: StringID[] = [];
|
|
||||||
const partedClientIds = new Set<StringID>();
|
|
||||||
|
|
||||||
type Emitter = {
|
|
||||||
on: (event: string, callback: (a: any) => any) => number;
|
|
||||||
off: (event: string, ref: number) => void;
|
|
||||||
trigger: (event: string, payload: any) => void;
|
|
||||||
getBindings: () => any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type PhoenixChannel = any;
|
|
||||||
export function listenForNetworkMessages(channel: PhoenixChannel, presenceEventEmitter: Emitter) {
|
|
||||||
presenceEventEmitter.on("hub:join", ({ key: nid }) => {
|
|
||||||
// TODO: Is it OK to use join events for our own client id?
|
|
||||||
pendingJoins.push(APP.getSid(nid));
|
|
||||||
});
|
|
||||||
presenceEventEmitter.on("hub:leave", ({ key: nid }) => {
|
|
||||||
pendingParts.push(APP.getSid(nid));
|
|
||||||
});
|
|
||||||
channel.on("naf", onNaf);
|
|
||||||
channel.on("nafr", onNafr);
|
|
||||||
}
|
|
||||||
type NafMessage = {
|
|
||||||
from_session_id: string;
|
|
||||||
data: any;
|
|
||||||
dataType: string;
|
|
||||||
source: string;
|
|
||||||
};
|
|
||||||
function onNaf({ from_session_id, data, dataType }: NafMessage) {
|
|
||||||
if (dataType == "nn") {
|
|
||||||
(data as Message).fromClientId = from_session_id;
|
|
||||||
pendingMessages.push(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type NafrMessage = {
|
|
||||||
from_session_id: string;
|
|
||||||
naf: string;
|
|
||||||
parsed?: NafMessage;
|
|
||||||
};
|
|
||||||
function onNafr(message: NafrMessage) {
|
|
||||||
const { from_session_id, naf: unparsedData } = message;
|
|
||||||
// Attach the parsed JSON to the message so that
|
|
||||||
// PhoenixAdapter can process it without parsing it again.
|
|
||||||
message.parsed = JSON.parse(unparsedData);
|
|
||||||
message.parsed!.from_session_id = from_session_id;
|
|
||||||
onNaf(message.parsed!);
|
|
||||||
}
|
|
||||||
|
|
||||||
function messageFor(
|
|
||||||
world: HubsWorld,
|
|
||||||
created: EntityID[],
|
|
||||||
updated: EntityID[],
|
|
||||||
needsFullSyncUpdate: EntityID[],
|
|
||||||
deleted: EntityID[],
|
|
||||||
isBroadcast: boolean
|
|
||||||
) {
|
|
||||||
const message: Message = {
|
|
||||||
creates: [],
|
|
||||||
updates: [],
|
|
||||||
deletes: []
|
|
||||||
};
|
|
||||||
|
|
||||||
created.forEach(eid => {
|
|
||||||
const { prefabName, initialData } = createMessageDatas.get(eid)!;
|
|
||||||
message.creates.push([APP.getString(Networked.id[eid])!, prefabName, initialData]);
|
|
||||||
});
|
|
||||||
|
|
||||||
updated.forEach(eid => {
|
|
||||||
const updateMessage: UpdateMessage = {
|
|
||||||
nid: APP.getString(Networked.id[eid])!,
|
|
||||||
lastOwnerTime: Networked.lastOwnerTime[eid],
|
|
||||||
timestamp: Networked.timestamp[eid],
|
|
||||||
owner: APP.getString(Networked.owner[eid])!, // This should always be NAF.clientId. If it's not, something bad happened
|
|
||||||
creator: APP.getString(Networked.creator[eid])!,
|
|
||||||
componentIds: [],
|
|
||||||
data: []
|
|
||||||
};
|
|
||||||
const isFullSync = needsFullSyncUpdate.includes(eid);
|
|
||||||
|
|
||||||
for (let j = 0; j < networkableComponents.length; j++) {
|
|
||||||
const component = networkableComponents[j];
|
|
||||||
if (hasComponent(world, component, eid)) {
|
|
||||||
if (schemas.get(component)!.serialize(world, eid, updateMessage.data, isFullSync, isBroadcast)) {
|
|
||||||
updateMessage.componentIds.push(j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: If the owner/lastOwnerTime changed, we need to send this updateMessage
|
|
||||||
if (updateMessage.componentIds.length) {
|
|
||||||
message.updates.push(updateMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
deleted.forEach(eid => {
|
|
||||||
// TODO: We are reading component data of a deleted entity here.
|
|
||||||
const nid = Networked.id[eid];
|
|
||||||
message.deletes.push(APP.getString(nid)!);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (message.creates.length || message.updates.length || message.deletes.length) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNetworkInstantiated(eid: EntityID) {
|
|
||||||
return createMessageDatas.has(eid);
|
return createMessageDatas.has(eid);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNetworkInstantiatedByMe(eid: EntityID) {
|
|
||||||
return isNetworkInstantiated(eid) && Networked.creator[eid] === APP.getSid(NAF.clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingUpdatesForNid = new Map<StringID, UpdateMessage[]>();
|
|
||||||
|
|
||||||
const rcvEnteredNetworkedEntitiesQuery = enterQuery(defineQuery([Networked]));
|
|
||||||
export function networkReceiveSystem(world: HubsWorld) {
|
|
||||||
if (!localClientID) return; // Not connected yet.
|
|
||||||
|
|
||||||
{
|
|
||||||
// When a user leaves, remove the entities created by that user
|
|
||||||
const networkedEntities = networkedEntitiesQuery(world);
|
|
||||||
pendingParts.forEach(partingClientId => {
|
|
||||||
partedClientIds.add(partingClientId);
|
|
||||||
|
|
||||||
networkedEntities
|
|
||||||
.filter(eid => isNetworkInstantiated(eid) && Networked.creator[eid] === partingClientId)
|
|
||||||
.forEach(eid => removeEntity(world, eid));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we were hanging onto updates for any newly created non network instantiated entities
|
|
||||||
// we can now apply them. Network instantiated entities are handled when processing creates.
|
|
||||||
rcvEnteredNetworkedEntitiesQuery(world).forEach(eid => {
|
|
||||||
const nid = Networked.id[eid];
|
|
||||||
if (pendingUpdatesForNid.has(nid)) {
|
|
||||||
console.log("Had pending updates for", APP.getString(nid), pendingUpdatesForNid.get(nid));
|
|
||||||
const updates = pendingUpdatesForNid.get(nid)!;
|
|
||||||
|
|
||||||
for (let i = 0; i < updates.length; i++) {
|
|
||||||
const update = updates[i];
|
|
||||||
if (partedClientIds.has(APP.getSid(update.owner))) {
|
|
||||||
console.log("Rewriting update message from client who left.", JSON.stringify(update));
|
|
||||||
update.owner = NAF.clientId;
|
|
||||||
update.lastOwnerTime = update.timestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingMessages.unshift({ creates: [], updates, deletes: [] });
|
|
||||||
pendingUpdatesForNid.delete(nid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < pendingMessages.length; i++) {
|
|
||||||
const message = pendingMessages[i];
|
|
||||||
|
|
||||||
for (let j = 0; j < message.creates.length; j++) {
|
|
||||||
const [nidString, prefabName, initialData] = message.creates[j];
|
|
||||||
const creator = message.fromClientId;
|
|
||||||
if (!creator) {
|
|
||||||
// We do not expect to get here.
|
|
||||||
// We only check because we are synthesizing messages elsewhere;
|
|
||||||
// They should not have any create messages in them.
|
|
||||||
throw new Error("Received create message without a fromClientId.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const nid = APP.getSid(nidString);
|
|
||||||
|
|
||||||
if (world.deletedNids.has(nid)) {
|
|
||||||
// TODO we may need to allow this for reconnects
|
|
||||||
console.log(`Received a create message for an entity I've already deleted. Skipping ${nidString}`);
|
|
||||||
} else if (world.nid2eid.has(nid)) {
|
|
||||||
console.log(`Received create message for entity I already created. Skipping ${nidString}`);
|
|
||||||
} else if (!spawnAllowed(creator, prefabName)) {
|
|
||||||
// this should only ever happen if there is a bug or the sender is maliciously modified
|
|
||||||
console.log(`Received create from a user who does not have permission to spawn ${prefabName}`);
|
|
||||||
world.ignoredNids.add(nid); // TODO should we just use deletedNids for this?
|
|
||||||
} else {
|
|
||||||
const eid = createNetworkedEntityFromRemote(world, prefabName, initialData, nidString, creator, creator);
|
|
||||||
console.log("got create message for", nidString, eid);
|
|
||||||
|
|
||||||
// If we were hanging onto updates for this nid we can now apply them. And they should be processed before other updates.
|
|
||||||
if (pendingUpdatesForNid.has(nid)) {
|
|
||||||
console.log("had pending updates for", nidString, pendingUpdatesForNid.get(nid));
|
|
||||||
Array.prototype.unshift.apply(message.updates, pendingUpdatesForNid.get(nid));
|
|
||||||
pendingUpdatesForNid.delete(nid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = 0; j < message.updates.length; j++) {
|
|
||||||
const updateMessage = message.updates[j];
|
|
||||||
const nid = APP.getSid(updateMessage.nid);
|
|
||||||
|
|
||||||
if (world.ignoredNids.has(nid)) {
|
|
||||||
console.log(`Ignoring update for ignored entity ${updateMessage.nid}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (world.deletedNids.has(nid)) {
|
|
||||||
console.log(`Ignoring update for deleted entity ${updateMessage.nid}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!world.nid2eid.has(nid)) {
|
|
||||||
console.log(`Holding onto an update for ${updateMessage.nid} because we don't have it yet.`);
|
|
||||||
// TODO: What if we will NEVER be able to apply this update?
|
|
||||||
// TODO would be nice if we could squash these updates
|
|
||||||
const updates = pendingUpdatesForNid.get(nid) || [];
|
|
||||||
updates.push(updateMessage);
|
|
||||||
pendingUpdatesForNid.set(nid, updates);
|
|
||||||
console.log(pendingUpdatesForNid);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eid = world.nid2eid.get(nid)!;
|
|
||||||
|
|
||||||
if (
|
|
||||||
Networked.lastOwnerTime[eid] > updateMessage.lastOwnerTime ||
|
|
||||||
(Networked.lastOwnerTime[eid] === updateMessage.lastOwnerTime &&
|
|
||||||
APP.getString(Networked.owner[eid])! < updateMessage.owner) // arbitrary (but consistent) tiebreak
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
"Received update from an old owner, skipping",
|
|
||||||
updateMessage.nid,
|
|
||||||
Networked.lastOwnerTime[eid],
|
|
||||||
updateMessage.lastOwnerTime
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateMessage.owner === NAF.clientId) {
|
|
||||||
console.log("Got a message telling us we are the owner.");
|
|
||||||
addComponent(world, Owned, eid);
|
|
||||||
} else if (hasComponent(world, Owned, eid)) {
|
|
||||||
console.log("Lost ownership: ", updateMessage.nid);
|
|
||||||
removeComponent(world, Owned, eid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Networked.creator[eid] = APP.getSid(updateMessage.creator);
|
|
||||||
Networked.owner[eid] = APP.getSid(updateMessage.owner);
|
|
||||||
Networked.lastOwnerTime[eid] = updateMessage.lastOwnerTime;
|
|
||||||
Networked.timestamp[eid] = updateMessage.timestamp;
|
|
||||||
|
|
||||||
// TODO HACK simulating a buffer with a cursor using an array
|
|
||||||
updateMessage.data.cursor = 0;
|
|
||||||
for (let s = 0; s < updateMessage.componentIds.length; s++) {
|
|
||||||
const componentId = updateMessage.componentIds[s];
|
|
||||||
const schema = schemas.get(networkableComponents[componentId])!;
|
|
||||||
schema.deserialize(world, eid, updateMessage.data);
|
|
||||||
}
|
|
||||||
delete updateMessage.data.cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = 0; j < message.deletes.length; j += 1) {
|
|
||||||
const nid = APP.getSid(message.deletes[j]);
|
|
||||||
if (world.deletedNids.has(nid)) continue;
|
|
||||||
|
|
||||||
world.deletedNids.add(nid);
|
|
||||||
const eid = world.nid2eid.get(nid)!;
|
|
||||||
createMessageDatas.delete(eid);
|
|
||||||
world.nid2eid.delete(nid);
|
|
||||||
removeEntity(world, eid);
|
|
||||||
|
|
||||||
console.log("OK, deleting ", APP.getString(nid));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pendingMessages.length = 0;
|
|
||||||
|
|
||||||
{
|
|
||||||
const networkedEntities = networkedEntitiesQuery(world);
|
|
||||||
pendingParts.forEach(partingClientId => {
|
|
||||||
networkedEntities
|
|
||||||
.filter(eid => Networked.owner[eid] === partingClientId)
|
|
||||||
.forEach(eid => {
|
|
||||||
takeOwnershipWithTime(world, eid, Networked.timestamp[eid]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
pendingParts.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO If there's a scene-owned entity, we should take ownership of it
|
|
||||||
}
|
|
||||||
|
|
||||||
const ticksPerSecond = 12;
|
|
||||||
const millisecondsBetweenTicks = 1000 / ticksPerSecond;
|
|
||||||
let nextTick = 0;
|
|
||||||
|
|
||||||
const sendEnteredNetworkedEntitiesQuery = enterQuery(networkedEntitiesQuery);
|
|
||||||
const sendEnteredOwnedEntitiesQuery = enterQuery(ownedNetworkedEntitiesQuery);
|
|
||||||
const sendExitedNetworkedEntitiesQuery = exitQuery(networkedEntitiesQuery);
|
|
||||||
|
|
||||||
export function networkSendSystem(world: HubsWorld) {
|
|
||||||
if (!localClientID) return; // Not connected yet
|
|
||||||
|
|
||||||
const now = performance.now();
|
|
||||||
if (now < nextTick) return;
|
|
||||||
|
|
||||||
if (now < nextTick + millisecondsBetweenTicks) {
|
|
||||||
nextTick = nextTick + millisecondsBetweenTicks; // The usual case
|
|
||||||
} else {
|
|
||||||
// An unusually long delay happened
|
|
||||||
nextTick = now + millisecondsBetweenTicks;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// TODO: Ensure getServerTime() is monotonically increasing.
|
|
||||||
// TODO: Get the server time from the websocket connection
|
|
||||||
// before we start sending any messages, in case of large local clock skew.
|
|
||||||
const timestamp = getServerTime();
|
|
||||||
ownedNetworkedEntitiesQuery(world).forEach(eid => {
|
|
||||||
Networked.timestamp[eid] = timestamp;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell joining users about entities I network instantiated, and full updates for entities I own
|
|
||||||
{
|
|
||||||
if (pendingJoins.length) {
|
|
||||||
const ownedNetworkedEntities = ownedNetworkedEntitiesQuery(world);
|
|
||||||
const message = messageFor(
|
|
||||||
world,
|
|
||||||
networkedEntitiesQuery(world).filter(isNetworkInstantiatedByMe),
|
|
||||||
ownedNetworkedEntities,
|
|
||||||
ownedNetworkedEntities,
|
|
||||||
[],
|
|
||||||
false
|
|
||||||
);
|
|
||||||
if (message) {
|
|
||||||
pendingJoins.forEach(clientId => NAF.connection.sendDataGuaranteed(APP.getString(clientId)!, "nn", message));
|
|
||||||
}
|
|
||||||
pendingJoins.length = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell everyone about entities I created, entities I own, and entities that were deleted
|
|
||||||
{
|
|
||||||
// Note: Many people may send delete messages about the same entity
|
|
||||||
const deleted = sendExitedNetworkedEntitiesQuery(world).filter(eid => {
|
|
||||||
return !world.deletedNids.has(Networked.id[eid]) && isNetworkInstantiated(eid);
|
|
||||||
});
|
|
||||||
const message = messageFor(
|
|
||||||
world,
|
|
||||||
sendEnteredNetworkedEntitiesQuery(world).filter(isNetworkInstantiatedByMe),
|
|
||||||
ownedNetworkedEntitiesQuery(world),
|
|
||||||
sendEnteredOwnedEntitiesQuery(world),
|
|
||||||
deleted,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (message) NAF.connection.broadcastDataGuaranteed("nn", message);
|
|
||||||
|
|
||||||
deleted.forEach(eid => {
|
|
||||||
createMessageDatas.delete(eid);
|
|
||||||
world.deletedNids.add(Networked.id[eid]);
|
|
||||||
world.nid2eid.delete(Networked.id[eid]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle reconnect
|
|
||||||
// TODO: Handle blocking/unblocking. Does this already work?
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
VideoMenuItem
|
VideoMenuItem
|
||||||
} from "../bit-components";
|
} from "../bit-components";
|
||||||
import { timeFmt } from "../components/media-video";
|
import { timeFmt } from "../components/media-video";
|
||||||
import { takeOwnership } from "./networking";
|
import { takeOwnership } from "../utils/take-ownership";
|
||||||
import { paths } from "../systems/userinput/paths";
|
import { paths } from "../systems/userinput/paths";
|
||||||
import { animate } from "../utils/animate";
|
import { animate } from "../utils/animate";
|
||||||
import { coroutine } from "../utils/coroutine";
|
import { coroutine } from "../utils/coroutine";
|
||||||
|
|
|
@ -241,7 +241,8 @@ import { ThemeProvider } from "./react-components/styles/theme";
|
||||||
import { LogMessageType } from "./react-components/room/ChatSidebar";
|
import { LogMessageType } from "./react-components/room/ChatSidebar";
|
||||||
import "./load-media-on-paste-or-drop";
|
import "./load-media-on-paste-or-drop";
|
||||||
import { swapActiveScene } from "./bit-systems/scene-loading";
|
import { swapActiveScene } from "./bit-systems/scene-loading";
|
||||||
import { listenForNetworkMessages, setLocalClientID } from "./bit-systems/networking";
|
import { setLocalClientID } from "./bit-systems/networking";
|
||||||
|
import { listenForNetworkMessages } from "./utils/listen-for-network-messages";
|
||||||
|
|
||||||
const PHOENIX_RELIABLE_NAF = "phx-reliable";
|
const PHOENIX_RELIABLE_NAF = "phx-reliable";
|
||||||
NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF;
|
NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createNetworkedEntity } from "./bit-systems/networking";
|
import { createNetworkedEntity } from "./utils/create-networked-entity";
|
||||||
import { upload, parseURL } from "./utils/media-utils";
|
import { upload, parseURL } from "./utils/media-utils";
|
||||||
import { guessContentType } from "./utils/media-url-utils";
|
import { guessContentType } from "./utils/media-url-utils";
|
||||||
import { AElement } from "aframe";
|
import { AElement } from "aframe";
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ducky from "./assets/models/DuckyMesh.glb";
|
||||||
import { EventTarget } from "event-target-shim";
|
import { EventTarget } from "event-target-shim";
|
||||||
import { ExitReason } from "./react-components/room/ExitedRoomScreen";
|
import { ExitReason } from "./react-components/room/ExitedRoomScreen";
|
||||||
import { LogMessageType } from "./react-components/room/ChatSidebar";
|
import { LogMessageType } from "./react-components/room/ChatSidebar";
|
||||||
import { createNetworkedEntity } from "./bit-systems/networking";
|
import { createNetworkedEntity } from "./utils/create-networked-entity";
|
||||||
|
|
||||||
let uiRoot;
|
let uiRoot;
|
||||||
// Handles user-entered messages
|
// Handles user-entered messages
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { MediaLoaderParams } from "../inflators/media-loader";
|
||||||
|
import { CameraPrefab, CubeMediaFramePrefab } from "../prefabs/camera-tool";
|
||||||
|
import { MediaPrefab } from "../prefabs/media";
|
||||||
|
import { EntityDef } from "../utils/jsx-entity";
|
||||||
|
|
||||||
|
type CameraPrefabT = () => EntityDef;
|
||||||
|
type CubeMediaPrefabT = () => EntityDef;
|
||||||
|
type MediaPrefabT = (params: MediaLoaderParams) => EntityDef;
|
||||||
|
|
||||||
|
export type PrefabDefinition = {
|
||||||
|
permission?: "spawn_camera";
|
||||||
|
template: CameraPrefabT | CubeMediaPrefabT | MediaPrefabT;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrefabName = "camera" | "cube" | "media";
|
||||||
|
|
||||||
|
export const prefabs = new Map<PrefabName, PrefabDefinition>();
|
||||||
|
prefabs.set("camera", { permission: "spawn_camera", template: CameraPrefab });
|
||||||
|
prefabs.set("cube", { template: CubeMediaFramePrefab });
|
||||||
|
prefabs.set("media", { template: MediaPrefab });
|
|
@ -2,7 +2,7 @@ import qsTruthy from "./utils/qs_truthy";
|
||||||
import nextTick from "./utils/next-tick";
|
import nextTick from "./utils/next-tick";
|
||||||
import { hackyMobileSafariTest } from "./utils/detect-touchscreen";
|
import { hackyMobileSafariTest } from "./utils/detect-touchscreen";
|
||||||
import { SignInMessages } from "./react-components/auth/SignInModal";
|
import { SignInMessages } from "./react-components/auth/SignInModal";
|
||||||
import { createNetworkedEntity } from "./bit-systems/networking";
|
import { createNetworkedEntity } from "./utils/create-networked-entity";
|
||||||
|
|
||||||
const isBotMode = qsTruthy("bot");
|
const isBotMode = qsTruthy("bot");
|
||||||
const isMobile = AFRAME.utils.device.isMobile();
|
const isMobile = AFRAME.utils.device.isMobile();
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
ConstraintRemoteLeft,
|
ConstraintRemoteLeft,
|
||||||
ConstraintRemoteRight
|
ConstraintRemoteRight
|
||||||
} from "../bit-components";
|
} from "../bit-components";
|
||||||
import { takeOwnership } from "../bit-systems/networking";
|
import { takeOwnership } from "../utils/take-ownership";
|
||||||
|
|
||||||
const queryRemoteRight = defineQuery([HeldRemoteRight, OffersRemoteConstraint]);
|
const queryRemoteRight = defineQuery([HeldRemoteRight, OffersRemoteConstraint]);
|
||||||
const queryEnterRemoteRight = enterQuery(queryRemoteRight);
|
const queryEnterRemoteRight = enterQuery(queryRemoteRight);
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { addObject3DComponent } from "../utils/jsx-entity";
|
||||||
import { updateMaterials } from "../utils/material-utils";
|
import { updateMaterials } from "../utils/material-utils";
|
||||||
import { MediaType } from "../utils/media-utils";
|
import { MediaType } from "../utils/media-utils";
|
||||||
import { cloneObject3D, setMatrixWorld } from "../utils/three-utils";
|
import { cloneObject3D, setMatrixWorld } from "../utils/three-utils";
|
||||||
import { takeOwnership } from "../bit-systems/networking";
|
import { takeOwnership } from "../utils/take-ownership";
|
||||||
|
|
||||||
const EMPTY_COLOR = 0x6fc0fd;
|
const EMPTY_COLOR = 0x6fc0fd;
|
||||||
const HOVER_COLOR = 0x2f80ed;
|
const HOVER_COLOR = 0x2f80ed;
|
||||||
|
|
|
@ -32,7 +32,8 @@ import { EnvironmentSystem } from "./environment-system";
|
||||||
import { NameTagVisibilitySystem } from "./name-tag-visibility-system";
|
import { NameTagVisibilitySystem } from "./name-tag-visibility-system";
|
||||||
|
|
||||||
// new world
|
// new world
|
||||||
import { networkSendSystem, networkReceiveSystem } from "../bit-systems/networking";
|
import { networkReceiveSystem } from "../bit-systems/network-receive-system";
|
||||||
|
import { networkSendSystem } from "../bit-systems/network-send-system";
|
||||||
import { onOwnershipLost } from "./on-ownership-lost";
|
import { onOwnershipLost } from "./on-ownership-lost";
|
||||||
import { interactionSystem } from "./bit-interaction-system";
|
import { interactionSystem } from "./bit-interaction-system";
|
||||||
import { floatyObjectSystem } from "./floaty-object-system";
|
import { floatyObjectSystem } from "./floaty-object-system";
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { defineQuery, hasComponent } from "bitecs";
|
import { defineQuery, hasComponent } from "bitecs";
|
||||||
import { $isStringType, NetworkedMediaFrame } from "../bit-components";
|
|
||||||
import { findAncestor } from "./three-utils";
|
import { findAncestor } from "./three-utils";
|
||||||
|
|
||||||
const queries = new Map();
|
const queries = new Map();
|
||||||
|
@ -19,85 +18,6 @@ export function hasAnyComponent(world, components, eid) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO HACK gettting internal bitecs symbol, should expose createShadow
|
|
||||||
const $parentArray = Object.getOwnPropertySymbols(NetworkedMediaFrame.scale).find(s => s.description == "parentArray");
|
|
||||||
const $storeFlattened = Object.getOwnPropertySymbols(NetworkedMediaFrame).find(s => s.description == "storeFlattened");
|
|
||||||
|
|
||||||
const createShadow = (store, key) => {
|
|
||||||
if (!ArrayBuffer.isView(store)) {
|
|
||||||
const shadowStore = store[$parentArray].slice(0);
|
|
||||||
store[key] = store.map((_, eid) => {
|
|
||||||
const { length } = store[eid];
|
|
||||||
const start = length * eid;
|
|
||||||
const end = start + length;
|
|
||||||
return shadowStore.subarray(start, end);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
store[key] = store.slice(0);
|
|
||||||
}
|
|
||||||
return key;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO this array encoding is silly, use a buffer once we are not sending JSON
|
|
||||||
export function defineNetworkSchema(Component) {
|
|
||||||
const componentProps = Component[$storeFlattened];
|
|
||||||
const shadowSymbols = componentProps.map((prop, i) => {
|
|
||||||
return createShadow(prop, Symbol(`netshadow-${i}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
serialize(_world, eid, data, isFullSync, writeToShadow) {
|
|
||||||
const changedPids = [];
|
|
||||||
data.push(changedPids);
|
|
||||||
for (let pid = 0; pid < componentProps.length; pid++) {
|
|
||||||
const prop = componentProps[pid];
|
|
||||||
const shadow = prop[shadowSymbols[pid]];
|
|
||||||
// if property is an array
|
|
||||||
if (ArrayBuffer.isView(prop[eid])) {
|
|
||||||
for (let i = 0; i < prop[eid].length; i++) {
|
|
||||||
if (isFullSync || shadow[eid][i] !== prop[eid][i]) {
|
|
||||||
changedPids.push(pid);
|
|
||||||
// TODO handle EID type and arrays of strings
|
|
||||||
data.push(Array.from(prop[eid]));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (writeToShadow) shadow[eid].set(prop[eid]);
|
|
||||||
} else {
|
|
||||||
if (isFullSync || shadow[eid] !== prop[eid]) {
|
|
||||||
changedPids.push(pid);
|
|
||||||
// TODO handle EID type
|
|
||||||
data.push(prop[$isStringType] ? APP.getString(prop[eid]) : prop[eid]);
|
|
||||||
}
|
|
||||||
if (writeToShadow) shadow[eid] = prop[eid];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!changedPids.length) {
|
|
||||||
data.pop();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
deserialize(_world, eid, data) {
|
|
||||||
const updatedPids = data[data.cursor++];
|
|
||||||
for (let i = 0; i < updatedPids.length; i++) {
|
|
||||||
const pid = updatedPids[i];
|
|
||||||
const prop = componentProps[pid];
|
|
||||||
const shadow = prop[shadowSymbols[pid]];
|
|
||||||
// TODO updating the shadow here is slightly odd. Should taking ownership do it?
|
|
||||||
if (ArrayBuffer.isView(prop[eid])) {
|
|
||||||
prop[eid].set(data[data.cursor++]);
|
|
||||||
shadow[eid].set(prop[eid]);
|
|
||||||
} else {
|
|
||||||
const val = data[data.cursor++];
|
|
||||||
prop[eid] = prop[$isStringType] ? APP.getSid(val) : val;
|
|
||||||
shadow[eid] = prop[eid];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findAncestorEntity(world, eid, predicate) {
|
export function findAncestorEntity(world, eid, predicate) {
|
||||||
const obj = findAncestor(world.eid2obj.get(eid), o => o.eid && predicate(o.eid));
|
const obj = findAncestor(world.eid2obj.get(eid), o => o.eid && predicate(o.eid));
|
||||||
return obj && obj.eid;
|
return obj && obj.eid;
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { hasComponent } from "bitecs";
|
||||||
|
import { HubsWorld } from "../app";
|
||||||
|
import { Networked } from "../bit-components";
|
||||||
|
import { createMessageDatas } from "../bit-systems/networking";
|
||||||
|
import { PrefabName, prefabs } from "../prefabs/prefabs";
|
||||||
|
import { renderAsEntity } from "../utils/jsx-entity";
|
||||||
|
import { hasPermissionToSpawn } from "../utils/permissions";
|
||||||
|
import { takeOwnership } from "../utils/take-ownership";
|
||||||
|
import type { ClientID, InitialData } from "./networking-types";
|
||||||
|
|
||||||
|
export function createNetworkedEntity(world: HubsWorld, prefabName: PrefabName, initialData: InitialData) {
|
||||||
|
if (!hasPermissionToSpawn(NAF.clientId, prefabName))
|
||||||
|
throw new Error(`You do not have permission to spawn ${prefabName}`);
|
||||||
|
const rootNid = NAF.utils.createNetworkId();
|
||||||
|
return createNetworkedEntityFromRemote(world, prefabName, initialData, rootNid, NAF.clientId, NAF.clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNetworkedEntityFromRemote(
|
||||||
|
world: HubsWorld,
|
||||||
|
prefabName: PrefabName,
|
||||||
|
initialData: InitialData,
|
||||||
|
rootNid: string,
|
||||||
|
creator: ClientID,
|
||||||
|
owner: ClientID
|
||||||
|
) {
|
||||||
|
const eid = renderAsEntity(world, prefabs.get(prefabName)!.template(initialData));
|
||||||
|
const obj = world.eid2obj.get(eid)!;
|
||||||
|
|
||||||
|
createMessageDatas.set(eid, { prefabName, initialData });
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
obj.traverse(function (o) {
|
||||||
|
if (o.eid && hasComponent(world, Networked, o.eid)) {
|
||||||
|
const eid = o.eid;
|
||||||
|
Networked.id[eid] = APP.getSid(i === 0 ? rootNid : `${rootNid}.${i}`);
|
||||||
|
APP.world.nid2eid.set(Networked.id[eid], eid);
|
||||||
|
Networked.creator[eid] = APP.getSid(creator);
|
||||||
|
Networked.owner[eid] = APP.getSid(owner);
|
||||||
|
if (NAF.clientId === owner) takeOwnership(world, eid);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AFRAME.scenes[0].object3D.add(obj);
|
||||||
|
return eid;
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { $isStringType, NetworkedMediaFrame } from "../bit-components";
|
||||||
|
|
||||||
|
// TODO HACK gettting internal bitecs symbol, should expose createShadow
|
||||||
|
const $parentArray = Object.getOwnPropertySymbols(NetworkedMediaFrame.scale).find(s => s.description == "parentArray");
|
||||||
|
const $storeFlattened = Object.getOwnPropertySymbols(NetworkedMediaFrame).find(s => s.description == "storeFlattened");
|
||||||
|
|
||||||
|
const createShadow = (store, key) => {
|
||||||
|
if (!ArrayBuffer.isView(store)) {
|
||||||
|
const shadowStore = store[$parentArray].slice(0);
|
||||||
|
store[key] = store.map((_, eid) => {
|
||||||
|
const { length } = store[eid];
|
||||||
|
const start = length * eid;
|
||||||
|
const end = start + length;
|
||||||
|
return shadowStore.subarray(start, end);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
store[key] = store.slice(0);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO this array encoding is silly, use a buffer once we are not sending JSON
|
||||||
|
export function defineNetworkSchema(Component) {
|
||||||
|
const componentProps = Component[$storeFlattened];
|
||||||
|
const shadowSymbols = componentProps.map((prop, i) => {
|
||||||
|
return createShadow(prop, Symbol(`netshadow-${i}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialize(_world, eid, data, isFullSync, writeToShadow) {
|
||||||
|
const changedPids = [];
|
||||||
|
data.push(changedPids);
|
||||||
|
for (let pid = 0; pid < componentProps.length; pid++) {
|
||||||
|
const prop = componentProps[pid];
|
||||||
|
const shadow = prop[shadowSymbols[pid]];
|
||||||
|
// if property is an array
|
||||||
|
if (ArrayBuffer.isView(prop[eid])) {
|
||||||
|
for (let i = 0; i < prop[eid].length; i++) {
|
||||||
|
if (isFullSync || shadow[eid][i] !== prop[eid][i]) {
|
||||||
|
changedPids.push(pid);
|
||||||
|
// TODO handle EID type and arrays of strings
|
||||||
|
data.push(Array.from(prop[eid]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (writeToShadow) shadow[eid].set(prop[eid]);
|
||||||
|
} else {
|
||||||
|
if (isFullSync || shadow[eid] !== prop[eid]) {
|
||||||
|
changedPids.push(pid);
|
||||||
|
// TODO handle EID type
|
||||||
|
data.push(prop[$isStringType] ? APP.getString(prop[eid]) : prop[eid]);
|
||||||
|
}
|
||||||
|
if (writeToShadow) shadow[eid] = prop[eid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changedPids.length) {
|
||||||
|
data.pop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deserialize(_world, eid, data) {
|
||||||
|
const updatedPids = data[data.cursor++];
|
||||||
|
for (let i = 0; i < updatedPids.length; i++) {
|
||||||
|
const pid = updatedPids[i];
|
||||||
|
const prop = componentProps[pid];
|
||||||
|
const shadow = prop[shadowSymbols[pid]];
|
||||||
|
// TODO updating the shadow here is slightly odd. Should taking ownership do it?
|
||||||
|
if (ArrayBuffer.isView(prop[eid])) {
|
||||||
|
prop[eid].set(data[data.cursor++]);
|
||||||
|
shadow[eid].set(prop[eid]);
|
||||||
|
} else {
|
||||||
|
const val = data[data.cursor++];
|
||||||
|
prop[eid] = prop[$isStringType] ? APP.getSid(val) : val;
|
||||||
|
shadow[eid] = prop[eid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -91,7 +91,7 @@ type Attrs = {
|
||||||
ref?: Ref;
|
ref?: Ref;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EntityDef = {
|
export type EntityDef = {
|
||||||
components: JSXComponentData;
|
components: JSXComponentData;
|
||||||
attrs: Attrs;
|
attrs: Attrs;
|
||||||
children: EntityDef[];
|
children: EntityDef[];
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { pendingJoins, pendingMessages, pendingParts } from "../bit-systems/networking";
|
||||||
|
import type { Message } from "./networking-types";
|
||||||
|
|
||||||
|
type Emitter = {
|
||||||
|
on: (event: string, callback: (a: any) => any) => number;
|
||||||
|
off: (event: string, ref: number) => void;
|
||||||
|
trigger: (event: string, payload: any) => void;
|
||||||
|
getBindings: () => any[];
|
||||||
|
};
|
||||||
|
type PhoenixChannel = any;
|
||||||
|
export function listenForNetworkMessages(channel: PhoenixChannel, presenceEventEmitter: Emitter) {
|
||||||
|
presenceEventEmitter.on("hub:join", ({ key: nid }) => {
|
||||||
|
// TODO: Is it OK to use join events for our own client id?
|
||||||
|
pendingJoins.push(APP.getSid(nid));
|
||||||
|
});
|
||||||
|
presenceEventEmitter.on("hub:leave", ({ key: nid }) => {
|
||||||
|
pendingParts.push(APP.getSid(nid));
|
||||||
|
});
|
||||||
|
channel.on("naf", onNaf);
|
||||||
|
channel.on("nafr", onNafr);
|
||||||
|
}
|
||||||
|
type NafMessage = {
|
||||||
|
from_session_id: string;
|
||||||
|
data: any;
|
||||||
|
dataType: string;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
function onNaf({ from_session_id, data, dataType }: NafMessage) {
|
||||||
|
if (dataType == "nn") {
|
||||||
|
(data as Message).fromClientId = from_session_id;
|
||||||
|
pendingMessages.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type NafrMessage = {
|
||||||
|
from_session_id: string;
|
||||||
|
naf: string;
|
||||||
|
parsed?: NafMessage;
|
||||||
|
};
|
||||||
|
function onNafr(message: NafrMessage) {
|
||||||
|
const { from_session_id, naf: unparsedData } = message;
|
||||||
|
// Attach the parsed JSON to the message so that
|
||||||
|
// PhoenixAdapter can process it without parsing it again.
|
||||||
|
message.parsed = JSON.parse(unparsedData);
|
||||||
|
message.parsed!.from_session_id = from_session_id;
|
||||||
|
onNaf(message.parsed!);
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { hasComponent } from "bitecs";
|
||||||
|
import { HubsWorld } from "../app";
|
||||||
|
import { Networked } from "../bit-components";
|
||||||
|
import { createMessageDatas } from "../bit-systems/networking";
|
||||||
|
import { networkableComponents, schemas } from "./network-schemas";
|
||||||
|
import type { EntityID, Message, UpdateMessage } from "./networking-types";
|
||||||
|
|
||||||
|
export function messageFor(
|
||||||
|
world: HubsWorld,
|
||||||
|
created: EntityID[],
|
||||||
|
updated: EntityID[],
|
||||||
|
needsFullSyncUpdate: EntityID[],
|
||||||
|
deleted: EntityID[],
|
||||||
|
isBroadcast: boolean
|
||||||
|
) {
|
||||||
|
const message: Message = {
|
||||||
|
creates: [],
|
||||||
|
updates: [],
|
||||||
|
deletes: []
|
||||||
|
};
|
||||||
|
|
||||||
|
created.forEach(eid => {
|
||||||
|
const { prefabName, initialData } = createMessageDatas.get(eid)!;
|
||||||
|
message.creates.push([APP.getString(Networked.id[eid])!, prefabName, initialData]);
|
||||||
|
});
|
||||||
|
|
||||||
|
updated.forEach(eid => {
|
||||||
|
const updateMessage: UpdateMessage = {
|
||||||
|
nid: APP.getString(Networked.id[eid])!,
|
||||||
|
lastOwnerTime: Networked.lastOwnerTime[eid],
|
||||||
|
timestamp: Networked.timestamp[eid],
|
||||||
|
owner: APP.getString(Networked.owner[eid])!,
|
||||||
|
creator: APP.getString(Networked.creator[eid])!,
|
||||||
|
componentIds: [],
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
const isFullSync = needsFullSyncUpdate.includes(eid);
|
||||||
|
|
||||||
|
for (let j = 0; j < networkableComponents.length; j++) {
|
||||||
|
const component = networkableComponents[j];
|
||||||
|
if (hasComponent(world, component, eid)) {
|
||||||
|
if (schemas.get(component)!.serialize(world, eid, updateMessage.data, isFullSync, isBroadcast)) {
|
||||||
|
updateMessage.componentIds.push(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If the owner/lastOwnerTime changed, we need to send this updateMessage
|
||||||
|
if (updateMessage.componentIds.length) {
|
||||||
|
message.updates.push(updateMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deleted.forEach(eid => {
|
||||||
|
// TODO: We are reading component data of a deleted entity here.
|
||||||
|
const nid = Networked.id[eid];
|
||||||
|
message.deletes.push(APP.getString(nid)!);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (message.creates.length || message.updates.length || message.deletes.length) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Component } from "bitecs";
|
||||||
|
import { HubsWorld } from "../app";
|
||||||
|
import { NetworkedMediaFrame, NetworkedTransform, NetworkedVideo } from "../bit-components";
|
||||||
|
import { defineNetworkSchema } from "../utils/define-network-schema";
|
||||||
|
import type { EntityID, CursorBuffer } from "./networking-types";
|
||||||
|
|
||||||
|
interface NetworkSchema {
|
||||||
|
serialize: (
|
||||||
|
world: HubsWorld,
|
||||||
|
eid: EntityID,
|
||||||
|
data: CursorBuffer,
|
||||||
|
isFullSync: boolean,
|
||||||
|
writeToShadow: boolean
|
||||||
|
) => boolean;
|
||||||
|
deserialize: (world: HubsWorld, eid: EntityID, data: CursorBuffer) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const schemas: Map<Component, NetworkSchema> = new Map();
|
||||||
|
schemas.set(NetworkedMediaFrame, defineNetworkSchema(NetworkedMediaFrame));
|
||||||
|
schemas.set(NetworkedTransform, defineNetworkSchema(NetworkedTransform));
|
||||||
|
schemas.set(NetworkedVideo, defineNetworkSchema(NetworkedVideo));
|
||||||
|
export const networkableComponents = Array.from(schemas.keys());
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { PrefabName } from "../prefabs/prefabs";
|
||||||
|
|
||||||
|
export type EntityID = number;
|
||||||
|
export type InitialData = any;
|
||||||
|
export interface CreateMessageData {
|
||||||
|
prefabName: PrefabName;
|
||||||
|
initialData: InitialData;
|
||||||
|
}
|
||||||
|
export type ClientID = string;
|
||||||
|
export type NetworkID = string;
|
||||||
|
export type StringID = number;
|
||||||
|
export type CreateMessage = [networkId: NetworkID, prefabName: PrefabName, initialData: InitialData];
|
||||||
|
export type CursorBuffer = { cursor?: number; push: (data: any) => {} };
|
||||||
|
export type UpdateMessage = {
|
||||||
|
nid: NetworkID;
|
||||||
|
lastOwnerTime: number;
|
||||||
|
timestamp: number;
|
||||||
|
owner: ClientID;
|
||||||
|
creator: ClientID;
|
||||||
|
componentIds: number[];
|
||||||
|
data: CursorBuffer;
|
||||||
|
};
|
||||||
|
export type DeleteMessage = NetworkID;
|
||||||
|
export type Message = {
|
||||||
|
fromClientId?: ClientID;
|
||||||
|
creates: CreateMessage[];
|
||||||
|
updates: UpdateMessage[];
|
||||||
|
deletes: DeleteMessage[];
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { PrefabName, prefabs } from "../prefabs/prefabs";
|
||||||
|
import type { ClientID } from "./networking-types";
|
||||||
|
|
||||||
|
export function hasPermissionToSpawn(creator: ClientID, prefabName: PrefabName) {
|
||||||
|
const perm = prefabs.get(prefabName)!.permission;
|
||||||
|
return !perm || APP.hubChannel!.userCan(creator, perm);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { addComponent, hasComponent } from "bitecs";
|
||||||
|
import { HubsWorld } from "../app";
|
||||||
|
import { AEntity, Networked, Owned } from "../bit-components";
|
||||||
|
import type { EntityID } from "./networking-types";
|
||||||
|
|
||||||
|
export function takeOwnershipWithTime(world: HubsWorld, eid: EntityID, timestamp: number) {
|
||||||
|
if (hasComponent(world, AEntity, eid)) {
|
||||||
|
throw new Error("Cannot take ownership of AEntities with a specific timestamp.");
|
||||||
|
}
|
||||||
|
|
||||||
|
addComponent(world, Owned, eid);
|
||||||
|
Networked.lastOwnerTime[eid] = timestamp;
|
||||||
|
Networked.owner[eid] = APP.getSid(NAF.clientId);
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { addComponent, hasComponent } from "bitecs";
|
||||||
|
import { HubsWorld } from "../app";
|
||||||
|
import { AEntity, Networked, Owned } from "../bit-components";
|
||||||
|
import { getServerTime } from "../phoenix-adapter";
|
||||||
|
import type { EntityID } from "./networking-types";
|
||||||
|
|
||||||
|
export function takeOwnership(world: HubsWorld, eid: EntityID) {
|
||||||
|
// TODO we do this to have a single API for taking ownership of things in new code, but it obviously relies on NAF/AFrame
|
||||||
|
if (hasComponent(world, AEntity, eid)) {
|
||||||
|
const el = world.eid2obj.get(eid)!.el!;
|
||||||
|
!NAF.utils.isMine(el) && NAF.utils.takeOwnership(el);
|
||||||
|
} else {
|
||||||
|
addComponent(world, Owned, eid);
|
||||||
|
Networked.lastOwnerTime[eid] = Math.max(getServerTime(), Networked.lastOwnerTime[eid] + 1);
|
||||||
|
Networked.owner[eid] = APP.getSid(NAF.clientId);
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче