зеркало из 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 { MediaLoader, Networked } from "../bit-components";
|
||||
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 { 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 {
|
||||
addComponent,
|
||||
Component,
|
||||
defineQuery,
|
||||
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;
|
||||
import { defineQuery } from "bitecs";
|
||||
import { Networked } from "../bit-components";
|
||||
import type { ClientID, CreateMessageData, EntityID, Message, StringID } from "../utils/networking-types";
|
||||
export let localClientID: ClientID | null = null;
|
||||
export function setLocalClientID(clientID: ClientID) {
|
||||
localClientID = clientID;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
export const createMessageDatas: Map<EntityID, CreateMessageData> = new Map();
|
||||
export const networkedQuery = defineQuery([Networked]);
|
||||
export const pendingMessages: Message[] = [];
|
||||
export const pendingJoins: StringID[] = [];
|
||||
export const pendingParts: StringID[] = [];
|
||||
export function isNetworkInstantiated(eid: EntityID) {
|
||||
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
|
||||
} from "../bit-components";
|
||||
import { timeFmt } from "../components/media-video";
|
||||
import { takeOwnership } from "./networking";
|
||||
import { takeOwnership } from "../utils/take-ownership";
|
||||
import { paths } from "../systems/userinput/paths";
|
||||
import { animate } from "../utils/animate";
|
||||
import { coroutine } from "../utils/coroutine";
|
||||
|
|
|
@ -241,7 +241,8 @@ import { ThemeProvider } from "./react-components/styles/theme";
|
|||
import { LogMessageType } from "./react-components/room/ChatSidebar";
|
||||
import "./load-media-on-paste-or-drop";
|
||||
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";
|
||||
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 { guessContentType } from "./utils/media-url-utils";
|
||||
import { AElement } from "aframe";
|
||||
|
|
|
@ -7,7 +7,7 @@ import ducky from "./assets/models/DuckyMesh.glb";
|
|||
import { EventTarget } from "event-target-shim";
|
||||
import { ExitReason } from "./react-components/room/ExitedRoomScreen";
|
||||
import { LogMessageType } from "./react-components/room/ChatSidebar";
|
||||
import { createNetworkedEntity } from "./bit-systems/networking";
|
||||
import { createNetworkedEntity } from "./utils/create-networked-entity";
|
||||
|
||||
let uiRoot;
|
||||
// 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 { hackyMobileSafariTest } from "./utils/detect-touchscreen";
|
||||
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 isMobile = AFRAME.utils.device.isMobile();
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
ConstraintRemoteLeft,
|
||||
ConstraintRemoteRight
|
||||
} from "../bit-components";
|
||||
import { takeOwnership } from "../bit-systems/networking";
|
||||
import { takeOwnership } from "../utils/take-ownership";
|
||||
|
||||
const queryRemoteRight = defineQuery([HeldRemoteRight, OffersRemoteConstraint]);
|
||||
const queryEnterRemoteRight = enterQuery(queryRemoteRight);
|
||||
|
|
|
@ -16,7 +16,7 @@ import { addObject3DComponent } from "../utils/jsx-entity";
|
|||
import { updateMaterials } from "../utils/material-utils";
|
||||
import { MediaType } from "../utils/media-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 HOVER_COLOR = 0x2f80ed;
|
||||
|
|
|
@ -32,7 +32,8 @@ import { EnvironmentSystem } from "./environment-system";
|
|||
import { NameTagVisibilitySystem } from "./name-tag-visibility-system";
|
||||
|
||||
// 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 { interactionSystem } from "./bit-interaction-system";
|
||||
import { floatyObjectSystem } from "./floaty-object-system";
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { defineQuery, hasComponent } from "bitecs";
|
||||
import { $isStringType, NetworkedMediaFrame } from "../bit-components";
|
||||
import { findAncestor } from "./three-utils";
|
||||
|
||||
const queries = new Map();
|
||||
|
@ -19,85 +18,6 @@ export function hasAnyComponent(world, components, eid) {
|
|||
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) {
|
||||
const obj = findAncestor(world.eid2obj.get(eid), o => o.eid && predicate(o.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;
|
||||
};
|
||||
|
||||
type EntityDef = {
|
||||
export type EntityDef = {
|
||||
components: JSXComponentData;
|
||||
attrs: Attrs;
|
||||
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);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче