Merge pull request #16507 from highfidelity/master

Merge master into instancing
This commit is contained in:
Sabrina Shanman 2019-11-18 13:54:33 -08:00 committed by GitHub
commit 518d6a530a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
91 changed files with 11968 additions and 2878 deletions

View file

@ -257,6 +257,7 @@ endif()
if (BUILD_CLIENT)
add_subdirectory(interface)
add_subdirectory(screenshare)
set_target_properties(interface PROPERTIES FOLDER "Apps")
option(USE_SIXENSE "Build Interface with sixense library/plugin" OFF)

View file

@ -1081,6 +1081,12 @@ void AvatarMixer::setupEntityQuery() {
priorityZoneQuery["avatarPriority"] = true;
priorityZoneQuery["type"] = "Zone";
QJsonObject queryFlags;
queryFlags["includeAncestors"] = true;
queryFlags["includeDescendants"] = true;
priorityZoneQuery["flags"] = queryFlags;
priorityZoneQuery["name"] = true; // Handy for debugging.
_entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery);
_slaveSharedData.entityTree = entityTree;
}

View file

@ -99,6 +99,7 @@ namespace {
glm::vec3 position;
bool isInPriorityZone { false };
float zoneVolume { std::numeric_limits<float>::max() };
EntityItemID id {};
static bool operation(const OctreeElementPointer& element, void* extraData) {
auto findPriorityZone = static_cast<FindPriorityZone*>(extraData);
@ -113,6 +114,7 @@ namespace {
if (volume < findPriorityZone->zoneVolume) { // Smaller volume wins
findPriorityZone->isInPriorityZone = zoneItem->getAvatarPriority() == COMPONENT_MODE_ENABLED;
findPriorityZone->zoneVolume = volume;
findPriorityZone->id = zoneItem->getEntityItemID();
}
}
}
@ -152,7 +154,15 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared
EntityTree& entityTree = *slaveSharedData.entityTree;
FindPriorityZone findPriorityZone { newPosition } ;
entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone);
_avatar->setHasPriority(findPriorityZone.isInPriorityZone);
bool currentlyHasPriority = findPriorityZone.isInPriorityZone;
if (currentlyHasPriority != _avatar->getHasPriority()) {
_avatar->setHasPriority(currentlyHasPriority);
auto nodeList = DependencyManager::get<NodeList>();
auto packet = NLPacket::create(PacketType::AvatarZonePresence, 2 * NUM_BYTES_RFC4122_UUID, true);
packet->write(_avatar->getSessionUUID().toRfc4122());
packet->write(findPriorityZone.id.toRfc4122());
nodeList->sendPacket(std::move(packet), nodeList->getDomainSockAddr());
}
_avatar->setNeedsHeroCheck(false);
}

View file

@ -146,23 +146,27 @@ macro(SET_PACKAGING_PARAMETERS)
set(DMG_SUBFOLDER_ICON "${HF_CMAKE_DIR}/installer/install-folder.rsrc")
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(SCREENSHARE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
if (CLIENT_ONLY)
set(CONSOLE_EXEC_NAME "Console.app")
else ()
set(CONSOLE_EXEC_NAME "Sandbox.app")
endif()
set(CONSOLE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${CONSOLE_EXEC_NAME}")
set(SCREENSHARE_EXEC_NAME "hifi-screenshare.app")
set(SCREENSHARE_INSTALL_APP_PATH "${SCREENSHARE_INSTALL_DIR}/${SCREENSHARE_EXEC_NAME}")
set(CONSOLE_APP_CONTENTS "${CONSOLE_INSTALL_APP_PATH}/Contents")
set(COMPONENT_APP_PATH "${CONSOLE_APP_CONTENTS}/MacOS/Components.app")
set(COMPONENT_INSTALL_DIR "${COMPONENT_APP_PATH}/Contents/MacOS")
set(CONSOLE_PLUGIN_INSTALL_DIR "${COMPONENT_APP_PATH}/Contents/PlugIns")
set(SCREENSHARE_APP_CONTENTS "${SCREENSHARE_INSTALL_APP_PATH}/Contents")
set(INTERFACE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${INTERFACE_BUNDLE_NAME}.app")
set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns")
@ -170,9 +174,11 @@ macro(SET_PACKAGING_PARAMETERS)
else ()
if (WIN32)
set(CONSOLE_INSTALL_DIR "server-console")
set(SCREENSHARE_INSTALL_DIR "hifi-screenshare")
set(NITPICK_INSTALL_DIR "nitpick")
else ()
set(CONSOLE_INSTALL_DIR ".")
set(SCREENSHARE_INSTALL_DIR ".")
set(NITPICK_INSTALL_DIR ".")
endif ()
@ -186,6 +192,7 @@ macro(SET_PACKAGING_PARAMETERS)
set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.ico")
set(CONSOLE_EXEC_NAME "server-console.exe")
set(SCREENSHARE_EXEC_NAME "hifi-screenshare.exe")
set(DS_EXEC_NAME "domain-server.exe")
set(AC_EXEC_NAME "assignment-client.exe")

View file

@ -766,6 +766,7 @@ void DomainServer::setupNodeListAndAssignments() {
packetReceiver.registerListener(PacketType::DomainServerPathQuery, this, "processPathQueryPacket");
packetReceiver.registerListener(PacketType::NodeJsonStats, this, "processNodeJSONStatsPacket");
packetReceiver.registerListener(PacketType::DomainDisconnectRequest, this, "processNodeDisconnectRequestPacket");
packetReceiver.registerListener(PacketType::AvatarZonePresence, this, "processAvatarZonePresencePacket");
// NodeList won't be available to the settings manager when it is created, so call registerListener here
packetReceiver.registerListener(PacketType::DomainSettingsRequest, &_settingsManager, "processSettingsRequestPacket");
@ -3613,3 +3614,62 @@ void DomainServer::handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMes
handleOctreeFileReplacement(message->readAll(), QString(), QString(), username);
}
}
void DomainServer::processAvatarZonePresencePacket(QSharedPointer<ReceivedMessage> message) {
QUuid avatar = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
QUuid zone = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
if (avatar.isNull()) {
qCWarning(domain_server) << "Ignoring null avatar presence";
return;
}
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
auto matchingNode = limitedNodeList->nodeWithUUID(avatar);
if (!matchingNode) {
qCWarning(domain_server) << "Ignoring avatar presence for unknown avatar" << avatar;
return;
}
QString verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
static const int SCREENSHARE_EXPIRATION_SECONDS = 24 * 60 * 60;
screensharePresence(zone.isNull() ? "" : zone.toString(), verifiedUsername, SCREENSHARE_EXPIRATION_SECONDS);
}
void DomainServer::screensharePresence(QString roomname, QString username, int expirationSeconds) {
if (!DependencyManager::get<AccountManager>()->hasValidAccessToken()) {
static std::once_flag presenceAuthorityWarning;
std::call_once(presenceAuthorityWarning, [] {
qCDebug(domain_server) << "No authority to send screensharePresence.";
});
return;
}
JSONCallbackParameters callbackParams;
callbackParams.callbackReceiver = this;
callbackParams.jsonCallbackMethod = "handleSuccessfulScreensharePresence";
callbackParams.errorCallbackMethod = "handleFailedScreensharePresence";
const QString PATH = "api/v1/domains/%1/screenshare";
QString domain_id = uuidStringWithoutCurlyBraces(getID());
QJsonObject json, screenshare;
screenshare["username"] = username;
screenshare["roomname"] = roomname;
if (expirationSeconds > 0) {
screenshare["expiration"] = expirationSeconds;
}
json["screenshare"] = screenshare;
DependencyManager::get<AccountManager>()->sendRequest(
PATH.arg(domain_id),
AccountManagerAuth::Required,
QNetworkAccessManager::PostOperation,
callbackParams, QJsonDocument(json).toJson()
);
}
void DomainServer::handleSuccessfulScreensharePresence(QNetworkReply* requestReply) {
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply->readAll()).object();
if (jsonObject["status"].toString() != "success") {
qCWarning(domain_server) << "screensharePresence api call failed:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact);
}
}
void DomainServer::handleFailedScreensharePresence(QNetworkReply* requestReply) {
qCWarning(domain_server) << "screensharePresence api call failed:" << requestReply->error();
}

View file

@ -78,6 +78,8 @@ public:
bool isAssetServerEnabled();
void screensharePresence(QString roomname, QString username, int expiration_seconds = 0);
public slots:
/// Called by NodeList to inform us a node has been added
void nodeAdded(SharedNodePointer node);
@ -96,6 +98,7 @@ private slots:
void processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatACK(QSharedPointer<ReceivedMessage> message);
void processAvatarZonePresencePacket(QSharedPointer<ReceivedMessage> packet);
void handleDomainContentReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message);
@ -129,6 +132,9 @@ private slots:
void handleSuccessfulICEServerAddressUpdate(QNetworkReply* requestReply);
void handleFailedICEServerAddressUpdate(QNetworkReply* requestReply);
void handleSuccessfulScreensharePresence(QNetworkReply* requestReply);
void handleFailedScreensharePresence(QNetworkReply* requestReply);
void updateReplicatedNodes();
void updateDownstreamNodes();
void updateUpstreamNodes();

File diff suppressed because it is too large Load diff

View file

@ -5994,14 +5994,6 @@
"state": "INAIRRUN",
"var": "isInAirRun"
},
{
"state": "strafeRightHmd",
"var": "isMovingRightHmd"
},
{
"state": "strafeLeftHmd",
"var": "isMovingLeftHmd"
},
{
"state": "seated",
"var": "isSeated"
@ -6062,14 +6054,6 @@
"state": "INAIRRUN",
"var": "isInAirRun"
},
{
"state": "strafeRightHmd",
"var": "isMovingRightHmd"
},
{
"state": "strafeLeftHmd",
"var": "isMovingLeftHmd"
},
{
"state": "seated",
"var": "isSeated"
@ -6214,7 +6198,7 @@
"transitions": [
{
"state": "idleSettle",
"var": "isNotInput"
"var": "isNotMoving"
},
{
"state": "WALKFWD",
@ -6228,14 +6212,6 @@
"state": "strafeLeftHmd",
"var": "isMovingLeftHmd"
},
{
"state": "STRAFERIGHT",
"var": "isInputRight"
},
{
"state": "STRAFELEFT",
"var": "isInputLeft"
},
{
"state": "turnRight",
"var": "isTurningRight"
@ -6278,7 +6254,7 @@
"transitions": [
{
"state": "idleSettle",
"var": "isNotInput"
"var": "isNotMoving"
},
{
"state": "WALKFWD",
@ -6292,14 +6268,6 @@
"state": "strafeRightHmd",
"var": "isMovingRightHmd"
},
{
"state": "STRAFERIGHT",
"var": "isInputRight"
},
{
"state": "STRAFELEFT",
"var": "isInputLeft"
},
{
"state": "turnRight",
"var": "isTurningRight"

View file

@ -49,4 +49,6 @@ Item {
Component.onCompleted: {
load(root.url, root.scriptUrl);
}
signal sendToScript(var message);
}

View file

@ -184,6 +184,7 @@
#include "scripting/AssetMappingsScriptingInterface.h"
#include "scripting/ClipboardScriptingInterface.h"
#include "scripting/DesktopScriptingInterface.h"
#include "scripting/ScreenshareScriptingInterface.h"
#include "scripting/AccountServicesScriptingInterface.h"
#include "scripting/HMDScriptingInterface.h"
#include "scripting/MenuScriptingInterface.h"
@ -967,6 +968,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<KeyboardScriptingInterface>();
DependencyManager::set<GrabManager>();
DependencyManager::set<AvatarPackager>();
DependencyManager::set<ScreenshareScriptingInterface>();
PlatformHelper::setup();
QObject::connect(PlatformHelper::instance(), &PlatformHelper::systemWillWake, [] {
@ -2919,6 +2921,7 @@ Application::~Application() {
DependencyManager::destroy<SoundCache>();
DependencyManager::destroy<OctreeStatsProvider>();
DependencyManager::destroy<GeometryCache>();
DependencyManager::destroy<ScreenshareScriptingInterface>();
DependencyManager::get<ResourceManager>()->cleanup();
@ -3430,7 +3433,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
surfaceContext->setContextProperty("Users", DependencyManager::get<UsersScriptingInterface>().data());
surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
surfaceContext->setContextProperty("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
surfaceContext->setContextProperty("Camera", &_myCamera);
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
@ -3536,6 +3539,7 @@ void Application::userKickConfirmation(const QUuid& nodeID) {
}
void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) {
surfaceContext->setContextProperty("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
surfaceContext->setContextProperty("Users", DependencyManager::get<UsersScriptingInterface>().data());
surfaceContext->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().data());
surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
@ -7314,6 +7318,7 @@ void Application::registerScriptEngineWithApplicationServices(const ScriptEngine
scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data());
scriptEngine->registerGlobalObject("Camera", &_myCamera);
scriptEngine->registerGlobalObject("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get<SpeechRecognizer>().data());

View file

@ -45,10 +45,6 @@ SpatiallyNestableWeakPointer InterfaceParentFinder::find(QUuid parentID, bool& s
success = true;
return parent;
}
if (parentID == AVATAR_SELF_ID) {
success = true;
return avatarManager->getMyAvatar();
}
success = false;
return parent;

View file

@ -29,6 +29,8 @@
#include <VrMenu.h>
#include <ScriptEngines.h>
#include <MenuItemProperties.h>
#include <ui/types/FileTypeProfile.h>
#include <ui/types/HFWebEngineProfile.h>
#include "Application.h"
#include "AccountManager.h"
@ -582,8 +584,20 @@ Menu::Menu() {
QString("hifi/tablet/TabletNetworkingPreferences.qml"), "NetworkingPreferencesDialog");
});
addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0,
DependencyManager::get<AssetClient>().data(), SLOT(clearCache()));
action = addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCaches);
connect(action, &QAction::triggered, [] {
// The following caches are cleared immediately
DependencyManager::get<AssetClient>()->clearCache();
#ifndef Q_OS_ANDROID
FileTypeProfile::clearCache();
HFWebEngineProfile::clearCache();
#endif
// Clear the KTX cache on the next restart. It can't be cleared immediately because its files might be in use.
Setting::Handle<int>(KTXCache::SETTING_VERSION_NAME, KTXCache::INVALID_VERSION).set(KTXCache::INVALID_VERSION);
});
addCheckableActionToQMenuAndActionHash(networkMenu,
MenuOption::DisableActivityLogger,
0,

View file

@ -56,7 +56,7 @@ namespace MenuOption {
const QString CalibrateCamera = "Calibrate Camera";
const QString CenterPlayerInView = "Center Player In View";
const QString Chat = "Chat...";
const QString ClearDiskCache = "Clear Disk Cache";
const QString ClearDiskCaches = "Clear Disk Caches (requires restart)";
const QString Collisions = "Collisions";
const QString Connexion = "Activate 3D Connexion Devices";
const QString Console = "Console...";

View file

@ -42,7 +42,7 @@ static const std::array<int, RefreshRateManager::RefreshRateRegime::REGIME_NUM>
{ { 30, 20, 10, 2, 30, 30 } };
static const std::array<int, RefreshRateManager::RefreshRateRegime::REGIME_NUM> REALTIME_PROFILE =
{ { 60, 60, 10, 2, 30, 30} };
{ { 60, 60, 60, 2, 30, 30} };
static const std::array<std::array<int, RefreshRateManager::RefreshRateRegime::REGIME_NUM>, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> REFRESH_RATE_PROFILES =
{ { ECO_PROFILE, INTERACTIVE_PROFILE, REALTIME_PROFILE } };

View file

@ -543,26 +543,8 @@ void AvatarManager::removeDeadAvatarEntities(const SetOfEntities& deadEntities)
for (auto entity : deadEntities) {
QUuid entityOwnerID = entity->getOwningAvatarID();
AvatarSharedPointer avatar = getAvatarBySessionID(entityOwnerID);
const bool REQUIRES_REMOVAL_FROM_TREE = false;
if (avatar) {
avatar->clearAvatarEntity(entity->getID(), REQUIRES_REMOVAL_FROM_TREE);
}
if (entityTree && entity->isMyAvatarEntity()) {
entityTree->withWriteLock([&] {
// We only need to delete the direct children (rather than the descendants) because
// when the child is deleted, it will take care of its own children. If the child
// is also an avatar-entity, we'll end up back here. If it's not, the entity-server
// will take care of it in the usual way.
entity->forEachChild([&](SpatiallyNestablePointer child) {
EntityItemPointer childEntity = std::dynamic_pointer_cast<EntityItem>(child);
if (childEntity) {
entityTree->deleteEntity(childEntity->getID(), true, true);
if (avatar) {
avatar->clearAvatarEntity(childEntity->getID(), REQUIRES_REMOVAL_FROM_TREE);
}
}
});
});
avatar->clearAvatarEntity(entity->getID());
}
}
}

View file

@ -1518,7 +1518,8 @@ void MyAvatar::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteAr
}
void MyAvatar::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
AvatarData::clearAvatarEntity(entityID, requiresRemovalFromTree);
// NOTE: the requiresRemovalFromTree argument is unused
AvatarData::clearAvatarEntity(entityID);
_avatarEntitiesLock.withWriteLock([&] {
_cachedAvatarEntityBlobsToDelete.push_back(entityID);
});
@ -1526,7 +1527,12 @@ void MyAvatar::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFrom
void MyAvatar::sanitizeAvatarEntityProperties(EntityItemProperties& properties) const {
properties.setEntityHostType(entity::HostType::AVATAR);
properties.setOwningAvatarID(getID());
// Note: we store AVATAR_SELF_ID in EntityItem::_owningAvatarID and we usually
// store the actual sessionUUID in EntityItemProperties::_owningAvatarID (for JS
// consumption, for example). However at this context we are preparing properties
// for outgoing packet, in which case we use AVATAR_SELF_ID.
properties.setOwningAvatarID(AVATAR_SELF_ID);
// there's no entity-server to tell us we're the simulation owner, so always set the
// simulationOwner to the owningAvatarID and a high priority.
@ -1581,20 +1587,20 @@ void MyAvatar::handleChangedAvatarEntityData() {
// AvatarData::_packedAvatarEntityData via deeper logic.
// move the lists to minimize lock time
std::vector<QUuid> cachedBlobsToDelete;
std::vector<QUuid> cachedBlobsToUpdate;
std::vector<QUuid> entitiesToDelete;
std::vector<QUuid> entitiesToAdd;
std::vector<QUuid> entitiesToUpdate;
std::vector<EntityItemID> cachedBlobsToDelete;
std::vector<EntityItemID> cachedBlobsToUpdate;
std::vector<EntityItemID> entitiesToDelete;
std::vector<EntityItemID> entitiesToAdd;
std::vector<EntityItemID> entitiesToUpdate;
_avatarEntitiesLock.withWriteLock([&] {
cachedBlobsToDelete = std::move(_cachedAvatarEntityBlobsToDelete);
cachedBlobsToUpdate = std::move(_cachedAvatarEntityBlobsToAddOrUpdate);
entitiesToDelete = std::move(_entitiesToDelete);
entitiesToAdd = std::move(_entitiesToAdd);
entitiesToUpdate = std::move(_entitiesToUpdate);
cachedBlobsToDelete.swap(_cachedAvatarEntityBlobsToDelete);
cachedBlobsToUpdate.swap(_cachedAvatarEntityBlobsToAddOrUpdate);
entitiesToDelete.swap(_entitiesToDelete);
entitiesToAdd.swap(_entitiesToAdd);
entitiesToUpdate.swap(_entitiesToUpdate);
});
auto removeAllInstancesHelper = [] (const QUuid& id, std::vector<QUuid>& v) {
auto removeAllInstancesHelper = [] (const EntityItemID& id, std::vector<EntityItemID>& v) {
uint32_t i = 0;
while (i < v.size()) {
if (id == v[i]) {
@ -1621,11 +1627,7 @@ void MyAvatar::handleChangedAvatarEntityData() {
}
// DELETE real entities
for (const auto& id : entitiesToDelete) {
entityTree->withWriteLock([&] {
entityTree->deleteEntity(id);
});
}
entityTree->deleteEntitiesByID(entitiesToDelete);
// ADD real entities
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
@ -1696,7 +1698,7 @@ void MyAvatar::handleChangedAvatarEntityData() {
_needToSaveAvatarEntitySettings = true;
}
// also remove from list of stale blobs to avoid failed entity lookup later
std::set<QUuid>::iterator blobItr = _staleCachedAvatarEntityBlobs.find(id);
std::set<EntityItemID>::iterator blobItr = _staleCachedAvatarEntityBlobs.find(id);
if (blobItr != _staleCachedAvatarEntityBlobs.end()) {
_staleCachedAvatarEntityBlobs.erase(blobItr);
}
@ -1764,9 +1766,9 @@ bool MyAvatar::updateStaleAvatarEntityBlobs() const {
return false;
}
std::set<QUuid> staleBlobs = std::move(_staleCachedAvatarEntityBlobs);
std::set<EntityItemID> staleIDs = std::move(_staleCachedAvatarEntityBlobs);
int32_t numFound = 0;
for (const auto& id : staleBlobs) {
for (const auto& id : staleIDs) {
bool found = false;
EntityItemProperties properties;
entityTree->withReadLock([&] {
@ -1851,7 +1853,7 @@ void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
++constItr;
}
// find and erase deleted IDs from _cachedAvatarEntityBlobs
std::vector<QUuid> deletedIDs;
std::vector<EntityItemID> deletedIDs;
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.begin();
while (itr != _cachedAvatarEntityBlobs.end()) {
QUuid id = itr.key();
@ -2469,18 +2471,11 @@ bool isWearableEntity(const EntityItemPointer& entity) {
void MyAvatar::removeWornAvatarEntity(const EntityItemID& entityID) {
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (entityTree) {
auto entity = entityTree->findEntityByID(entityID);
if (entity && isWearableEntity(entity)) {
entityTree->withWriteLock([&entityID, &entityTree] {
// remove this entity first from the entity tree
entityTree->deleteEntity(entityID, true, true);
});
// remove the avatar entity from our internal list
// (but indicate it doesn't need to be pulled from the tree)
clearAvatarEntity(entityID, false);
treeRenderer->deleteEntity(entityID);
clearAvatarEntity(entityID);
}
}
}
@ -3098,7 +3093,7 @@ void MyAvatar::initAnimGraph() {
graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation.json");
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation_withSplineIKNode.json");
graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation-optimized-ik.json");
#endif
}
@ -3934,6 +3929,10 @@ float MyAvatar::getGravity() {
void MyAvatar::setSessionUUID(const QUuid& sessionUUID) {
QUuid oldSessionID = getSessionUUID();
Avatar::setSessionUUID(sessionUUID);
bool sendPackets = !DependencyManager::get<NodeList>()->getSessionUUID().isNull();
if (!sendPackets) {
return;
}
QUuid newSessionID = getSessionUUID();
if (newSessionID != oldSessionID) {
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
@ -3943,7 +3942,6 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) {
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
bool sendPackets = !DependencyManager::get<NodeList>()->getSessionUUID().isNull();
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
entityTree->withWriteLock([&] {
for (const auto& entityID : avatarEntityIDs) {
@ -3951,11 +3949,9 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) {
if (!entity) {
continue;
}
// update OwningAvatarID so entity can be identified as "ours" later
entity->setOwningAvatarID(newSessionID);
// NOTE: each attached AvatarEntity already have the correct updated parentID
// via magic in SpatiallyNestable, hence we check against newSessionID
if (sendPackets && entity->getParentID() == newSessionID) {
if (entity->getParentID() == newSessionID) {
// but when we have a real session and the AvatarEntity is parented to MyAvatar
// we need to update the "packedAvatarEntityData" sent to the avatar-mixer
// because it contains a stale parentID somewhere deep inside

View file

@ -2970,19 +2970,19 @@ private:
// correctly stored in _cachedAvatarEntityBlobs. These come from loadAvatarEntityDataFromSettings() and
// setAvatarEntityData(). These changes need to be extracted from _cachedAvatarEntityBlobs and applied to
// real EntityItems.
std::vector<QUuid> _entitiesToDelete;
std::vector<QUuid> _entitiesToAdd;
std::vector<QUuid> _entitiesToUpdate;
std::vector<EntityItemID> _entitiesToDelete;
std::vector<EntityItemID> _entitiesToAdd;
std::vector<EntityItemID> _entitiesToUpdate;
//
// The _cachedAvatarEntityBlobsToDelete/Add/Update lists are for changes whose "authoritative sources" are
// already reflected in real EntityItems. These changes need to be propagated to _cachedAvatarEntityBlobs
// and eventually to settings.
std::vector<QUuid> _cachedAvatarEntityBlobsToDelete;
std::vector<QUuid> _cachedAvatarEntityBlobsToAddOrUpdate;
std::vector<QUuid> _cachedAvatarEntityBlobUpdatesToSkip;
std::vector<EntityItemID> _cachedAvatarEntityBlobsToDelete;
std::vector<EntityItemID> _cachedAvatarEntityBlobsToAddOrUpdate;
std::vector<EntityItemID> _cachedAvatarEntityBlobUpdatesToSkip;
//
// Also these lists for tracking delayed changes to blobs and Settings
mutable std::set<QUuid> _staleCachedAvatarEntityBlobs;
mutable std::set<EntityItemID> _staleCachedAvatarEntityBlobs;
//
// keep a ScriptEngine around so we don't have to instantiate on the fly (these are very slow to create/delete)
mutable std::mutex _scriptEngineLock;

View file

@ -561,9 +561,18 @@ void OtherAvatar::handleChangedAvatarEntityData() {
_avatarEntitiesLock.withReadLock([&] {
packedAvatarEntityData = _packedAvatarEntityData;
});
foreach (auto entityID, recentlyRemovedAvatarEntities) {
if (!packedAvatarEntityData.contains(entityID)) {
entityTree->deleteEntity(entityID, true, true);
if (!recentlyRemovedAvatarEntities.empty()) {
std::vector<EntityItemID> idsToDelete;
idsToDelete.reserve(recentlyRemovedAvatarEntities.size());
foreach (auto entityID, recentlyRemovedAvatarEntities) {
if (!packedAvatarEntityData.contains(entityID)) {
idsToDelete.push_back(entityID);
}
}
if (!idsToDelete.empty()) {
bool force = true;
bool ignoreWarnings = true;
entityTree->deleteEntitiesByID(idsToDelete, force, ignoreWarnings);
}
}

View file

@ -52,6 +52,9 @@
* <em>Read-only.</em>
* <p><strong>Warning:</strong> Not yet implemented.</p>
*
* @property {FilterFlags} PICK_BYPASS_IGNORE - Allows pick to intersect entities even when their ignorePickIntersection property is 'true'.
* For debug purposes. <em>Read-only.</em>
*
* @property {IntersectionType} INTERSECTED_NONE - Intersected nothing. <em>Read-only.</em>
* @property {IntersectionType} INTERSECTED_ENTITY - Intersected an entity. <em>Read-only.</em>
* @property {IntersectionType} INTERSECTED_LOCAL_ENTITY - Intersected a local entity. <em>Read-only.</em>
@ -87,6 +90,8 @@ class PickScriptingInterface : public QObject, public Dependency {
Q_PROPERTY(unsigned int PICK_ALL_INTERSECTIONS READ PICK_ALL_INTERSECTIONS CONSTANT)
Q_PROPERTY(unsigned int PICK_BYPASS_IGNORE READ PICK_BYPASS_IGNORE CONSTANT)
Q_PROPERTY(unsigned int INTERSECTED_NONE READ INTERSECTED_NONE CONSTANT)
Q_PROPERTY(unsigned int INTERSECTED_ENTITY READ INTERSECTED_ENTITY CONSTANT)
Q_PROPERTY(unsigned int INTERSECTED_LOCAL_ENTITY READ INTERSECTED_LOCAL_ENTITY CONSTANT)
@ -282,6 +287,8 @@ public:
unsigned int getPerFrameTimeBudget() const;
void setPerFrameTimeBudget(unsigned int numUsecs);
static constexpr unsigned int PICK_BYPASS_IGNORE() { return PickFilter::getBitMask(PickFilter::FlagBit::PICK_BYPASS_IGNORE); }
public slots:
/**jsdoc

View file

@ -0,0 +1,276 @@
//
// ScreenshareScriptingInterface.cpp
// interface/src/scripting/
//
// Created by Milad Nazeri and Zach Fox on 2019-10-23.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QThread>
#include <QUrl>
#include <AccountManager.h>
#include <AddressManager.h>
#include <DependencyManager.h>
#include <UUID.h>
#include "EntityScriptingInterface.h"
#include "ScreenshareScriptingInterface.h"
ScreenshareScriptingInterface::ScreenshareScriptingInterface() {
auto esi = DependencyManager::get<EntityScriptingInterface>();
if (!esi) {
return;
}
// This signal/slot connection is used when the screen share local web entity sends an event bridge message.
QObject::connect(esi.data(), &EntityScriptingInterface::webEventReceived, this, &ScreenshareScriptingInterface::onWebEventReceived);
};
ScreenshareScriptingInterface::~ScreenshareScriptingInterface() {
stopScreenshare();
}
static const EntityTypes::EntityType LOCAL_SCREENSHARE_WEB_ENTITY_TYPE = EntityTypes::Web;
static const uint8_t LOCAL_SCREENSHARE_WEB_ENTITY_FPS = 30;
// This is going to be a good amount of work to make this work dynamically for any screensize.
// V1 will have only hardcoded values.
static const glm::vec3 LOCAL_SCREENSHARE_WEB_ENTITY_LOCAL_POSITION(0.0f, -0.0862f, 0.0711f);
static const glm::vec3 LOCAL_SCREENSHARE_WEB_ENTITY_DIMENSIONS(4.0419f, 2.2735f, 0.0100f);
static const QString LOCAL_SCREENSHARE_WEB_ENTITY_URL =
"https://content.highfidelity.com/Experiences/Releases/usefulUtilities/smartBoard/screenshareViewer/screenshareClient.html";
static const QString LOCAL_SCREENSHARE_WEB_ENTITY_HOST_TYPE ="local";
void ScreenshareScriptingInterface::startScreenshare(const QUuid& screenshareZoneID,
const QUuid& smartboardEntityID,
const bool& isPresenter) {
// We must start a new QProcess from the main thread.
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "startScreenshare", Q_ARG(const QUuid&, screenshareZoneID),
Q_ARG(const QUuid&, smartboardEntityID), Q_ARG(const bool&, isPresenter));
return;
}
// These three private member variables are set now so that they may be used later during asynchronous
// callbacks.
_screenshareZoneID = screenshareZoneID;
_smartboardEntityID = smartboardEntityID;
_isPresenter = isPresenter;
// If we are presenting, and the screenshare process is already running, don't do anything else here.
if (_isPresenter && _screenshareProcess && _screenshareProcess->state() != QProcess::NotRunning) {
return;
}
// If we're presenting...
if (_isPresenter) {
// ...make sure we first reset this `std::unique_ptr`.
_screenshareProcess.reset(new QProcess(this));
// Ensure that the screenshare executable exists where we expect it to.
// Error out and reset the screen share state machine if the executable doesn't exist.
QFileInfo screenshareExecutable(SCREENSHARE_EXE_PATH);
if (!screenshareExecutable.exists() || !screenshareExecutable.isFile()) {
qDebug() << "Screenshare executable doesn't exist at" << SCREENSHARE_EXE_PATH;
stopScreenshare();
emit screenshareError();
return;
}
}
// Don't continue with any more of this logic if we can't get the `AccountManager` or `AddressManager`.
auto accountManager = DependencyManager::get<AccountManager>();
if (!accountManager) {
return;
}
auto addressManager = DependencyManager::get<AddressManager>();
if (!addressManager) {
return;
}
// Construct and send a request to the Metaverse to obtain the information
// necessary to start the screen sharing process.
// This request requires:
// 1. The domain ID of the domain in which the user's avatar is present
// 2. User authentication information that is automatically included when `sendRequest()` is passed
// with the `AccountManagerAuth::Required` argument.
// Note that this request will only return successfully if the Domain Server has already registered
// the user paired with the current domain with the Metaverse.
// See `DomainServer::screensharePresence()` for more info about that.
QString currentDomainID = uuidStringWithoutCurlyBraces(addressManager->getDomainID());
QString requestURLPath = "api/v1/domains/%1/screenshare";
JSONCallbackParameters callbackParams;
callbackParams.callbackReceiver = this;
callbackParams.jsonCallbackMethod = "handleSuccessfulScreenshareInfoGet";
callbackParams.errorCallbackMethod = "handleFailedScreenshareInfoGet";
accountManager->sendRequest(
requestURLPath.arg(currentDomainID),
AccountManagerAuth::Required,
QNetworkAccessManager::GetOperation,
callbackParams
);
}
void ScreenshareScriptingInterface::stopScreenshare() {
// We can only deal with our Screen Share `QProcess` on the main thread.
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "stopScreenshare");
return;
}
// If the Screen Share process is running...
if (_screenshareProcess && _screenshareProcess->state() != QProcess::NotRunning) {
//...terminate it and make sure that scripts know we terminated it by emitting
// `screenshareProcessTerminated()`.
_screenshareProcess->terminate();
emit screenshareProcessTerminated();
}
// Delete the local web entity if we know about it here.
if (!_screenshareViewerLocalWebEntityUUID.isNull()) {
auto esi = DependencyManager::get<EntityScriptingInterface>();
if (esi) {
esi->deleteEntity(_screenshareViewerLocalWebEntityUUID);
}
}
// Reset all private member variables related to screen share here.
_screenshareViewerLocalWebEntityUUID = "{00000000-0000-0000-0000-000000000000}";
_token = "";
_projectAPIKey = "";
_sessionID = "";
_isPresenter = false;
}
// Called when the Metaverse returns the information necessary to start/view a screen share.
void ScreenshareScriptingInterface::handleSuccessfulScreenshareInfoGet(QNetworkReply* reply) {
// Read the reply and get it into a format we understand.
QString answer = reply->readAll();
QByteArray answerByteArray = answer.toUtf8();
QJsonDocument answerJSONObject = QJsonDocument::fromJson(answerByteArray);
// This Metaverse endpoint will always return a status key/value pair of "success" if things went well.
QString status = answerJSONObject["status"].toString();
if (status != "success") {
qDebug() << "Error when retrieving screenshare info via HTTP. Error:" << reply->errorString();
stopScreenshare();
emit screenshareError();
return;
}
// Store the information necessary to start/view a screen share in these private member variables.
_token = answerJSONObject["token"].toString();
_projectAPIKey = answerJSONObject["projectApiKey"].toString();
_sessionID = answerJSONObject["sessionID"].toString();
// Make sure we have all of the info that we need.
if (_token.isEmpty() || _projectAPIKey.isEmpty() || _sessionID.isEmpty()) {
qDebug() << "Not all Screen Share information was retrieved from the backend. Stopping...";
stopScreenshare();
emit screenshareError();
return;
}
// If we're presenting:
// 1. Build a list of arguments that we're going to pass to the screen share Electron app.
// 2. Make sure we connect a signal/slot to know when the user quits the Electron app.
// 3. Start the screen share Electron app with the list of args from (1).
if (_isPresenter) {
QStringList arguments;
arguments << " ";
arguments << "--token=" + _token << " ";
arguments << "--projectAPIKey=" + _projectAPIKey << " ";
arguments << "--sessionID=" + _sessionID << " ";
connect(_screenshareProcess.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
[=](int exitCode, QProcess::ExitStatus exitStatus) {
stopScreenshare();
emit screenshareProcessTerminated();
});
_screenshareProcess->start(SCREENSHARE_EXE_PATH, arguments);
}
// Make sure we can grab the entity scripting interface. Error out if we can't.
auto esi = DependencyManager::get<EntityScriptingInterface>();
if (!esi) {
stopScreenshare();
emit screenshareError();
return;
}
// If, for some reason, we already have a record of a screen share local Web entity, delete it.
if (!_screenshareViewerLocalWebEntityUUID.isNull()) {
esi->deleteEntity(_screenshareViewerLocalWebEntityUUID);
}
// Set up the entity properties associated with the screen share local Web entity.
EntityItemProperties localScreenshareWebEntityProps;
localScreenshareWebEntityProps.setType(LOCAL_SCREENSHARE_WEB_ENTITY_TYPE);
localScreenshareWebEntityProps.setMaxFPS(LOCAL_SCREENSHARE_WEB_ENTITY_FPS);
localScreenshareWebEntityProps.setLocalPosition(LOCAL_SCREENSHARE_WEB_ENTITY_LOCAL_POSITION);
localScreenshareWebEntityProps.setSourceUrl(LOCAL_SCREENSHARE_WEB_ENTITY_URL);
localScreenshareWebEntityProps.setParentID(_smartboardEntityID);
localScreenshareWebEntityProps.setDimensions(LOCAL_SCREENSHARE_WEB_ENTITY_DIMENSIONS);
// The lines below will be used when writing the feature to support scaling the Smartboard entity to any arbitrary size.
//EntityPropertyFlags desiredSmartboardProperties;
//desiredSmartboardProperties += PROP_POSITION;
//desiredSmartboardProperties += PROP_DIMENSIONS;
//EntityItemProperties smartboardProps = esi->getEntityProperties(_smartboardEntityID, desiredSmartboardProperties);
// Add the screen share local Web entity to Interface's entity tree.
// When the Web entity loads the page specified by `LOCAL_SCREENSHARE_WEB_ENTITY_URL`, it will broadcast an Event Bridge
// message, which we will consume inside `ScreenshareScriptingInterface::onWebEventReceived()`.
_screenshareViewerLocalWebEntityUUID = esi->addEntity(localScreenshareWebEntityProps, LOCAL_SCREENSHARE_WEB_ENTITY_HOST_TYPE);
}
void ScreenshareScriptingInterface::handleFailedScreenshareInfoGet(QNetworkReply* reply) {
qDebug() << "Failed to get screenshare info via HTTP. Error:" << reply->errorString();
stopScreenshare();
emit screenshareError();
}
// This function will handle _all_ web events received via `EntityScriptingInterface::webEventReceived()`, including
// those not related to screen sharing.
void ScreenshareScriptingInterface::onWebEventReceived(const QUuid& entityID, const QVariant& message) {
// Bail early if the entity that sent the Web event isn't the one we care about.
if (entityID == _screenshareViewerLocalWebEntityUUID) {
// Bail early if we can't grab the Entity Scripting Interface.
auto esi = DependencyManager::get<EntityScriptingInterface>();
if (!esi) {
return;
}
// Web events received from the screen share Web JS will always be in stringified JSON format.
QByteArray jsonByteArray = QVariant(message).toString().toUtf8();
QJsonDocument jsonObject = QJsonDocument::fromJson(jsonByteArray);
// It should never happen where the screen share Web JS sends a message without the `app` key's value
// set to "screenshare".
if (jsonObject["app"] != "screenshare") {
return;
}
// The screen share Web JS only sends a message with one method: "eventBridgeReady". Handle it here.
if (jsonObject["method"] == "eventBridgeReady") {
// Stuff a JSON object full of information necessary for the screen share local Web entity
// to connect to the screen share session associated with the room in which the user's avatar is standing.
QJsonObject responseObject;
responseObject.insert("app", "screenshare");
responseObject.insert("method", "receiveConnectionInfo");
QJsonObject responseObjectData;
responseObjectData.insert("token", _token);
responseObjectData.insert("projectAPIKey", _projectAPIKey);
responseObjectData.insert("sessionID", _sessionID);
responseObject.insert("data", responseObjectData);
esi->emitScriptEvent(_screenshareViewerLocalWebEntityUUID, responseObject.toVariantMap());
}
}
}

View file

@ -0,0 +1,72 @@
//
// ScreenshareScriptingInterface.h
// interface/src/scripting/
//
// Created by Milad Nazeri and Zach Fox on 2019-10-23.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ScreenshareScriptingInterface_h
#define hifi_ScreenshareScriptingInterface_h
#include <QObject>
#include <QProcess>
#include <QtCore/QCoreApplication>
#include <QNetworkReply>
#include <PathUtils.h>
class ScreenshareScriptingInterface : public QObject, public Dependency {
Q_OBJECT
public:
ScreenshareScriptingInterface();
~ScreenshareScriptingInterface();
Q_INVOKABLE void startScreenshare(const QUuid& screenshareZoneID, const QUuid& smartboardEntityID, const bool& isPresenter = false);
Q_INVOKABLE void stopScreenshare();
signals:
void screenshareError();
void screenshareProcessTerminated();
void startScreenshareViewer();
private slots:
void onWebEventReceived(const QUuid& entityID, const QVariant& message);
void handleSuccessfulScreenshareInfoGet(QNetworkReply* reply);
void handleFailedScreenshareInfoGet(QNetworkReply* reply);
private:
#if DEV_BUILD
#ifdef Q_OS_WIN
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/hifi-screenshare-win32-x64/hifi-screenshare.exe" };
#elif defined(Q_OS_MAC)
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/screenshare-darwin-x64/hifi-screenshare.app" };
#else
// This path won't exist on other platforms, so the Screenshare Scripting Interface will exit early when invoked.
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/screenshare-other-os/hifi-screenshare" };
#endif
#else
#ifdef Q_OS_WIN
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare.exe" };
#elif defined(Q_OS_MAC)
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare.app" };
#else
// This path won't exist on other platforms, so the Screenshare Scripting Interface will exit early when invoked.
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare" };
#endif
#endif
std::unique_ptr<QProcess> _screenshareProcess{ nullptr };
QUuid _screenshareViewerLocalWebEntityUUID;
QString _token{ "" };
QString _projectAPIKey{ "" };
QString _sessionID{ "" };
QUuid _screenshareZoneID;
QUuid _smartboardEntityID;
bool _isPresenter{ false };
};
#endif // hifi_ScreenshareScriptingInterface_h

View file

@ -17,7 +17,7 @@ GameplayObjects::GameplayObjects() {
}
bool GameplayObjects::addToGameplayObjects(const QUuid& avatarID) {
containsData = true;
_containsData = true;
if (std::find(_avatarIDs.begin(), _avatarIDs.end(), avatarID) == _avatarIDs.end()) {
_avatarIDs.push_back(avatarID);
}
@ -29,7 +29,7 @@ bool GameplayObjects::removeFromGameplayObjects(const QUuid& avatarID) {
}
bool GameplayObjects::addToGameplayObjects(const EntityItemID& entityID) {
containsData = true;
_containsData = true;
if (std::find(_entityIDs.begin(), _entityIDs.end(), entityID) == _entityIDs.end()) {
_entityIDs.push_back(entityID);
}

View file

@ -26,7 +26,7 @@ class GameplayObjects {
public:
GameplayObjects();
bool getContainsData() const { return containsData; }
bool getContainsData() const { return _containsData; }
std::vector<QUuid> getAvatarIDs() const { return _avatarIDs; }
bool addToGameplayObjects(const QUuid& avatarID);
@ -37,7 +37,7 @@ public:
bool removeFromGameplayObjects(const EntityItemID& entityID);
private:
bool containsData { false };
bool _containsData { false };
std::vector<QUuid> _avatarIDs;
std::vector<EntityItemID> _entityIDs;
};

View file

@ -62,16 +62,7 @@ void AvatarCertifyBanner::show(const QUuid& avatarID) {
void AvatarCertifyBanner::clear() {
if (_active) {
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = entityTreeRenderer->getTree();
if (!entityTree) {
return;
}
entityTree->withWriteLock([&] {
entityTree->deleteEntity(_bannerID);
});
DependencyManager::get<EntityTreeRenderer>()->deleteEntity(_bannerID);
_active = false;
}
}

View file

@ -9,6 +9,7 @@
//
#include "GameWorkload.h"
#include "GameWorkloadRenderer.h"
#include "SelectedWorkloadRenderer.h"
#include <ViewFrustum.h>
#include <workload/RegionTracker.h>
#include <workload/SpaceClassifier.h>
@ -35,6 +36,7 @@ public:
model.addJob<PhysicsBoundary>("PhysicsBoundary", regionTrackerOut);
model.addJob<GameSpaceToRender>("SpaceToRender");
model.addJob<SelectedWorkloadRenderer>("SelectedWorkloadRender");
out = regionTrackerOut;
}

View file

@ -16,6 +16,7 @@
#include <GeometryCache.h>
#include <shaders/Shaders.h>
#include "SelectedWorkloadRenderer.h"
void GameSpaceToRender::configure(const Config& config) {
_freezeViews = config.freezeViews;

View file

@ -0,0 +1,88 @@
//
// SelectedWorkloadRenderer.cpp
//
// Created by Andrew Meadows 2019.11.08
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "SelectedWorkloadRenderer.h"
#include <cstring>
#include <gpu/Context.h>
#include <workload/Space.h>
#include "Application.h"
#include "GameWorkloadRenderer.h"
#include "scripting/SelectionScriptingInterface.h"
void SelectedWorkloadRenderer::run(const workload::WorkloadContextPointer& runContext, Outputs& outputs) {
auto gameWorkloadContext = std::dynamic_pointer_cast<GameWorkloadContext>(runContext);
if (!gameWorkloadContext) {
return;
}
auto space = gameWorkloadContext->_space;
if (!space) {
return;
}
render::Transaction transaction;
auto scene = gameWorkloadContext->_scene;
auto selection = DependencyManager::get<SelectionScriptingInterface>();
// Note: the "DebugWorkloadSelection" name is a secret hard-coded C++ debug feature.
// If you create such a named list using JS and the "Selection" API then it will be picked up here
// and the workload proxies for corresponding entities will be rendered.
GameplayObjects selectedObjects = selection->getList("DebugWorkloadSelection");
if (!selectedObjects.getContainsData()) {
// nothing to render
// clear item if it exists and bail
if (render::Item::isValidID(_spaceRenderItemID)) {
transaction.updateItem<GameWorkloadRenderItem>(_spaceRenderItemID, [](GameWorkloadRenderItem& item) {
item.setVisible(false);
});
scene->enqueueTransaction(transaction);
}
return;
}
std::vector<EntityItemID> entityIDs = selectedObjects.getEntityIDs();
workload::indexed_container::Indices indices;
indices.reserve(entityIDs.size());
auto entityTreeRenderer = qApp->getEntities();
auto entityTree = entityTreeRenderer->getTree();
for (auto id : entityIDs) {
EntityItemPointer entity = entityTree->findEntityByID(id);
if (entity) {
indices.push_back(entity->getSpaceIndex());
}
}
workload::Proxy::Vector proxies;
proxies.reserve(indices.size());
space->copySelectedProxyValues(proxies, indices);
if (!render::Item::isValidID(_spaceRenderItemID)) {
_spaceRenderItemID = scene->allocateID();
auto renderItem = std::make_shared<GameWorkloadRenderItem>();
renderItem->editBound().setBox(glm::vec3(-16000.0f), 32000.0f);
transaction.resetItem(_spaceRenderItemID, std::make_shared<GameWorkloadRenderItem::Payload>(renderItem));
}
bool showProxies = true;
bool showViews = false;
bool visible = true;
workload::Views views(0);
transaction.updateItem<GameWorkloadRenderItem>(_spaceRenderItemID, [visible, showProxies, proxies, showViews, views](GameWorkloadRenderItem& item) {
item.setVisible(visible);
item.showProxies(showProxies);
item.setAllProxies(proxies);
item.showViews(showViews);
item.setAllViews(views);
});
scene->enqueueTransaction(transaction);
}

View file

@ -0,0 +1,32 @@
//
// GameWorkloadRender.h
//
// Created by Sam Gateau on 2/20/2018.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_SelectedWorkloadRenderer_h
#define hifi_SelectedWorkloadRenderer_h
#include "GameWorkload.h"
#include "GameWorkloadRenderer.h"
class SelectedWorkloadRenderer {
public:
using Config = GameSpaceToRenderConfig;
using Outputs = render::Transaction;
using JobModel = workload::Job::ModelO<SelectedWorkloadRenderer, Outputs, Config>;
SelectedWorkloadRenderer() {}
void configure(const Config& config) {}
void run(const workload::WorkloadContextPointer& renderContext, Outputs& outputs);
protected:
render::ItemID _spaceRenderItemID{ render::Item::INVALID_ITEM_ID };
};
#endif

View file

@ -1510,19 +1510,29 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
if (_previousControllerParameters.inputX > 0.0f) {
// right
if (!_headEnabled) {
_animVars.set("isInputRight", true);
} else {
_animVars.set("isInputRight", false);
}
_animVars.set("isInputLeft", false);
_animVars.set("isInputForward", false);
_animVars.set("isInputBackward", false);
_animVars.set("isInputRight", true);
_animVars.set("isInputLeft", false);
_animVars.set("isNotInput", false);
_animVars.set("isNotInputSlow", false);
_animVars.set("isNotInputNoMomentum", false);
} else {
// left
if (!_headEnabled) {
_animVars.set("isInputLeft", true);
} else {
_animVars.set("isInputLeft", false);
}
_animVars.set("isInputForward", false);
_animVars.set("isInputBackward", false);
_animVars.set("isInputRight", false);
_animVars.set("isInputLeft", true);
_animVars.set("isNotInput", false);
_animVars.set("isNotInputSlow", false);
_animVars.set("isNotInputNoMomentum", false);

View file

@ -111,11 +111,17 @@ QList<HifiAudioDeviceInfo> getAvailableDevices(QAudio::Mode mode, const QString&
}
if (defaultDesktopDevice.getDevice().isNull()) {
qCDebug(audioclient) << __FUNCTION__ << "Default device not found in list:" << defDeviceName
<< "Setting Default to: " << devices.first().deviceName();
defaultDesktopDevice = HifiAudioDeviceInfo(devices.first(), true, mode, HifiAudioDeviceInfo::desktop);
if (devices.size() > 0) {
qCDebug(audioclient) << __FUNCTION__ << "Default device not found in list:" << defDeviceName
<< "Setting Default to: " << devices.first().deviceName();
newDevices.push_front(HifiAudioDeviceInfo(devices.first(), true, mode, HifiAudioDeviceInfo::desktop));
} else {
//current audio list is empty for some reason.
qCDebug(audioclient) << __FUNCTION__ << "Default device not found in list no alternative selection available";
}
} else {
newDevices.push_front(defaultDesktopDevice);
}
newDevices.push_front(defaultDesktopDevice);
if (!hmdName.isNull()) {
HifiAudioDeviceInfo hmdDevice;
@ -1829,6 +1835,8 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice
_audioInput->deleteLater();
_audioInput = NULL;
_numInputCallbackBytes = 0;
_inputDeviceInfo.setDevice(QAudioDeviceInfo());
}
if (_dummyAudioInput) {
@ -2075,6 +2083,8 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi
delete[] _localOutputMixBuffer;
_localOutputMixBuffer = NULL;
_outputDeviceInfo.setDevice(QAudioDeviceInfo());
}
// cleanup any resamplers

View file

@ -40,138 +40,182 @@ class AudioStreamStatsInterface : public QObject {
Q_OBJECT
/**jsdoc
* Statistics for an audio stream.
*
* <p>Provided in the {@link AudioStats} API.</p>
*
* @class AudioStats.AudioStreamStats
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*
* @property {number} lossRate <em>Read-only.</em>
* @property {number} lossCount <em>Read-only.</em>
* @property {number} lossRateWindow <em>Read-only.</em>
* @property {number} lossCountWindow <em>Read-only.</em>
* @property {number} framesDesired <em>Read-only.</em>
* @property {number} framesAvailable <em>Read-only.</em>
* @property {number} framesAvailableAvg <em>Read-only.</em>
* @property {number} unplayedMsMax <em>Read-only.</em>
* @property {number} starveCount <em>Read-only.</em>
* @property {number} lastStarveDurationCount <em>Read-only.</em>
* @property {number} dropCount <em>Read-only.</em>
* @property {number} overflowCount <em>Read-only.</em>
* @property {number} timegapMsMax <em>Read-only.</em>
* @property {number} timegapMsAvg <em>Read-only.</em>
* @property {number} timegapMsMaxWindow <em>Read-only.</em>
* @property {number} timegapMsAvgWindow <em>Read-only.</em>
* @property {number} dropCount - The number of silent or old audio frames dropped.
* <em>Read-only.</em>
* @property {number} framesAvailable - The number of audio frames containing data available.
* <em>Read-only.</em>
* @property {number} framesAvailableAvg - The time-weighted average of audio frames containing data available.
* <em>Read-only.</em>
* @property {number} framesDesired - The desired number of audio frames for the jitter buffer.
* <em>Read-only.</em>
* @property {number} lastStarveDurationCount - The most recent number of consecutive times that audio frames have not been
* available for processing.
* <em>Read-only.</em>
* @property {number} lossCount - The total number of audio packets lost.
* <em>Read-only.</em>
* @property {number} lossCountWindow - The number of audio packets lost since the previous statistic.
* <em>Read-only.</em>
* @property {number} lossRate - The ratio of the total number of audio packets lost to the total number of audio packets
* expected.
* <em>Read-only.</em>
* @property {number} lossRateWindow - The ratio of the number of audio packets lost to the number of audio packets
* expected since the previous statistic.
* <em>Read-only.</em>
* @property {number} overflowCount - The number of times that the audio ring buffer has overflowed.
* <em>Read-only.</em>
* @property {number} starveCount - The total number of times that audio frames have not been available for processing.
* <em>Read-only.</em>
* @property {number} timegapMsAvg - The overall average time between data packets, in ms.
* <em>Read-only.</em>
* @property {number} timegapMsAvgWindow - The recent average time between data packets, in ms.
* <em>Read-only.</em>
* @property {number} timegapMsMax - The overall maximum time between data packets, in ms.
* <em>Read-only.</em>
* @property {number} timegapMsMaxWindow - The recent maximum time between data packets, in ms.
* <em>Read-only.</em>
* @property {number} unplayedMsMax - The duration of audio waiting to be played, in ms.
* <em>Read-only.</em>
*/
/**jsdoc
* Triggered when the ratio of the total number of audio packets lost to the total number of audio packets expected changes.
* @function AudioStats.AudioStreamStats.lossRateChanged
* @param {number} lossRate
* @param {number} lossRate - The ratio of the total number of audio packets lost to the total number of audio packets
* expected.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, lossRate)
/**jsdoc
* Triggered when the total number of audio packets lost changes.
* @function AudioStats.AudioStreamStats.lossCountChanged
* @param {number} lossCount
* @param {number} lossCount - The total number of audio packets lost.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, lossCount)
/**jsdoc
* Triggered when the ratio of the number of audio packets lost to the number of audio packets expected since the previous
* statistic changes.
* @function AudioStats.AudioStreamStats.lossRateWindowChanged
* @param {number} lossRateWindow
* @param {number} lossRateWindow - The ratio of the number of audio packets lost to the number of audio packets expected
* since the previous statistic.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, lossRateWindow)
/**jsdoc
* Triggered when the number of audio packets lost since the previous statistic changes.
* @function AudioStats.AudioStreamStats.lossCountWindowChanged
* @param {number} lossCountWindow
* @param {number} lossCountWindow - The number of audio packets lost since the previous statistic.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, lossCountWindow)
/**jsdoc
* Triggered when the desired number of audio frames for the jitter buffer changes.
* @function AudioStats.AudioStreamStats.framesDesiredChanged
* @param {number} framesDesired
* @param {number} framesDesired - The desired number of audio frames for the jitter buffer.
* @returns {Signal}
*/
AUDIO_PROPERTY(int, framesDesired)
/**jsdoc
* Triggered when the number of audio frames containing data available changes.
* @function AudioStats.AudioStreamStats.framesAvailableChanged
* @param {number} framesAvailable
* @param {number} framesAvailable - The number of audio frames containing data available.
* @returns {Signal}
*/
AUDIO_PROPERTY(int, framesAvailable)
/**jsdoc
* Triggered when the time-weighted average of audio frames containing data available changes.
* @function AudioStats.AudioStreamStats.framesAvailableAvgChanged
* @param {number} framesAvailableAvg
* @param {number} framesAvailableAvg - The time-weighted average of audio frames containing data available.
* @returns {Signal}
*/
AUDIO_PROPERTY(int, framesAvailableAvg)
/**jsdoc
* Triggered when the duration of audio waiting to be played changes.
* @function AudioStats.AudioStreamStats.unplayedMsMaxChanged
* @param {number} unplayedMsMax
* @param {number} unplayedMsMax - The duration of audio waiting to be played, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, unplayedMsMax)
/**jsdoc
* Triggered when the total number of times that audio frames have not been available for processing changes.
* @function AudioStats.AudioStreamStats.starveCountChanged
* @param {number} starveCount
* @param {number} starveCount - The total number of times that audio frames have not been available for processing.
* @returns {Signal}
*/
AUDIO_PROPERTY(int, starveCount)
/**jsdoc
* Triggered when the most recenbernumber of consecutive times that audio frames have not been available for processing
* changes.
* @function AudioStats.AudioStreamStats.lastStarveDurationCountChanged
* @param {number} lastStarveDurationCount
* @param {number} lastStarveDurationCount - The most recent number of consecutive times that audio frames have not been
* available for processing.
* @returns {Signal}
*/
AUDIO_PROPERTY(int, lastStarveDurationCount)
/**jsdoc
* Triggered when the number of silent or old audio frames dropped changes.
* @function AudioStats.AudioStreamStats.dropCountChanged
* @param {number} dropCount
* @param {number} dropCount - The number of silent or old audio frames dropped.
* @returns {Signal}
*/
AUDIO_PROPERTY(int, dropCount)
/**jsdoc
* Triggered when the number of times that the audio ring buffer has overflowed changes.
* @function AudioStats.AudioStreamStats.overflowCountChanged
* @param {number} overflowCount
* @param {number} overflowCount - The number of times that the audio ring buffer has overflowed.
* @returns {Signal}
*/
AUDIO_PROPERTY(int, overflowCount)
/**jsdoc
* Triggered when the overall maximum time between data packets changes.
* @function AudioStats.AudioStreamStats.timegapMsMaxChanged
* @param {number} timegapMsMax
* @param {number} timegapMsMax - The overall maximum time between data packets, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(quint64, timegapMsMax)
/**jsdoc
* Triggered when the overall average time between data packets changes.
* @function AudioStats.AudioStreamStats.timegapMsAvgChanged
* @param {number} timegapMsAvg
* @param {number} timegapMsAvg - The overall average time between data packets, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(quint64, timegapMsAvg)
/**jsdoc
* Triggered when the recent maximum time between data packets changes.
* @function AudioStats.AudioStreamStats.timegapMsMaxWindowChanged
* @param {number} timegapMsMaxWindow
* @param {number} timegapMsMaxWindow - The recent maximum time between data packets, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(quint64, timegapMsMaxWindow)
/**jsdoc
* Triggered when the recent average time between data packets changes.
* @function AudioStats.AudioStreamStats.timegapMsAvgWindowChanged
* @param {number} timegapMsAvgWindow
* @param {number} timegapMsAvgWindow - The recent average time between data packets, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(quint64, timegapMsAvgWindow)
@ -188,79 +232,106 @@ class AudioStatsInterface : public QObject {
Q_OBJECT
/**jsdoc
* Audio stats from the client.
* The <code>AudioStats</code> API provides statistics of the client and mixer audio.
*
* @namespace AudioStats
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*
* @property {number} pingMs <em>Read-only.</em>
* @property {number} inputReadMsMax <em>Read-only.</em>
* @property {number} inputUnplayedMsMax <em>Read-only.</em>
* @property {number} outputUnplayedMsMax <em>Read-only.</em>
* @property {number} sentTimegapMsMax <em>Read-only.</em>
* @property {number} sentTimegapMsAvg <em>Read-only.</em>
* @property {number} sentTimegapMsMaxWindow <em>Read-only.</em>
* @property {number} sentTimegapMsAvgWindow <em>Read-only.</em>
* @property {AudioStats.AudioStreamStats} clientStream <em>Read-only.</em>
* @property {AudioStats.AudioStreamStats} mixerStream <em>Read-only.</em>
* @property {AudioStats.AudioStreamStats} clientStream - Statistics of the client's audio stream.
* <em>Read-only.</em>
* @property {number} inputReadMsMax - The maximum duration of a block of audio data recently read from the microphone, in
* ms.
* <em>Read-only.</em>
* @property {number} inputUnplayedMsMax - The maximum duration of microphone audio recently in the input buffer waiting to
* be played, in ms.
* <em>Read-only.</em>
* @property {AudioStats.AudioStreamStats} mixerStream - Statistics of the audio mixer's stream.
* <em>Read-only.</em>
* @property {number} outputUnplayedMsMax - The maximum duration of output audio recently in the output buffer waiting to
* be played, in ms.
* <em>Read-only.</em>
* @property {number} pingMs - The current ping time to the audio mixer, in ms.
* <em>Read-only.</em>
* @property {number} sentTimegapMsAvg - The overall average time between sending data packets to the audio mixer, in ms.
* <em>Read-only.</em>
* @property {number} sentTimegapMsAvgWindow - The recent average time between sending data packets to the audio mixer, in
* ms.
* <em>Read-only.</em>
* @property {number} sentTimegapMsMax - The overall maximum time between sending data packets to the audio mixer, in ms.
* <em>Read-only.</em>
* @property {number} sentTimegapMsMaxWindow - The recent maximum time between sending data packets to the audio mixer, in
* ms.
* <em>Read-only.</em>
*/
/**jsdoc
* Triggered when the ping time to the audio mixer changes.
* @function AudioStats.pingMsChanged
* @param {number} pingMs
* @param {number} pingMs - The ping time to the audio mixer, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, pingMs);
/**jsdoc
* Triggered when the maximum duration of a block of audio data recently read from the microphone changes.
* @function AudioStats.inputReadMsMaxChanged
* @param {number} inputReadMsMax
* @param {number} inputReadMsMax - The maximum duration of a block of audio data recently read from the microphone, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, inputReadMsMax);
/**jsdoc
* Triggered when the maximum duration of microphone audio recently in the input buffer waiting to be played changes.
* @function AudioStats.inputUnplayedMsMaxChanged
* @param {number} inputUnplayedMsMax
* @param {number} inputUnplayedMsMax - The maximum duration of microphone audio recently in the input buffer waiting to be
* played, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, inputUnplayedMsMax);
/**jsdoc
* Triggered when the maximum duration of output audio recently in the output buffer waiting to be played changes.
* @function AudioStats.outputUnplayedMsMaxChanged
* @param {number} outputUnplayedMsMax
* @param {number} outputUnplayedMsMax - The maximum duration of output audio recently in the output buffer waiting to be
* played, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(float, outputUnplayedMsMax);
/**jsdoc
* Triggered when the overall maximum time between sending data packets to the audio mixer changes.
* @function AudioStats.sentTimegapMsMaxChanged
* @param {number} sentTimegapMsMax
* @param {number} sentTimegapMsMax - The overall maximum time between sending data packets to the audio mixer, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(quint64, sentTimegapMsMax);
/**jsdoc
* Triggered when the overall average time between sending data packets to the audio mixer changes.
* @function AudioStats.sentTimegapMsAvgChanged
* @param {number} sentTimegapMsAvg
* @param {number} sentTimegapMsAvg - The overall average time between sending data packets to the audio mixer, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(quint64, sentTimegapMsAvg);
/**jsdoc
* Triggered when the recent maximum time between sending data packets to the audio mixer changes.
* @function AudioStats.sentTimegapMsMaxWindowChanged
* @param {number} sentTimegapMsMaxWindow
* @param {number} sentTimegapMsMaxWindow - The recent maximum time between sending data packets to the audio mixer, in ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(quint64, sentTimegapMsMaxWindow);
/**jsdoc
* Triggered when the recent average time between sending data packets to the audio mixer changes.
* @function AudioStats.sentTimegapMsAvgWindowChanged
* @param {number} sentTimegapMsAvgWindow
* @param {number} sentTimegapMsAvgWindow - The recent average time between sending data packets to the audio mixer, in
* ms.
* @returns {Signal}
*/
AUDIO_PROPERTY(quint64, sentTimegapMsAvgWindow);
@ -287,18 +358,22 @@ public:
signals:
/**jsdoc
* Triggered when the mixer's stream statistics have been updated.
* @function AudioStats.mixerStreamChanged
* @returns {Signal}
*/
void mixerStreamChanged();
/**jsdoc
* Triggered when the client's stream statisticss have been updated.
* @function AudioStats.clientStreamChanged
* @returns {Signal}
*/
void clientStreamChanged();
/**jsdoc
* Triggered when the injector streams' statistics have been updated.
* <p><strong>Note:</strong> The injector streams' statistics are currently not provided.</p>
* @function AudioStats.injectorStreamsChanged
* @returns {Signal}
*/

View file

@ -333,18 +333,22 @@ void Avatar::setTargetScale(float targetScale) {
}
void Avatar::removeAvatarEntitiesFromTree() {
if (_packedAvatarEntityData.empty()) {
return;
}
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (entityTree) {
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
entityTree->withWriteLock([&] {
for (const auto& entityID : avatarEntityIDs) {
entityTree->deleteEntity(entityID, true, true);
}
});
std::vector<EntityItemID> ids;
ids.reserve(_packedAvatarEntityData.size());
PackedAvatarEntityMap::const_iterator itr = _packedAvatarEntityData.constBegin();
while (itr != _packedAvatarEntityData.constEnd()) {
ids.push_back(itr.key());
++itr;
}
bool force = true;
bool ignoreWarnings = true;
entityTree->deleteEntitiesByID(ids, force, ignoreWarnings); // locks tree
}
}

View file

@ -3033,15 +3033,12 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent
}
void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
// NOTE: requiresRemovalFromTree is unused
bool removedEntity = false;
_avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] {
removedEntity = _packedAvatarEntityData.remove(entityID);
});
insertRemovedEntityID(entityID);
if (removedEntity && _clientTraitsHandler) {
// we have a client traits handler, so we need to mark this removed instance trait as deleted
// so that changes are sent next frame

View file

@ -1179,7 +1179,7 @@ public:
/**jsdoc
* @function Avatar.clearAvatarEntity
* @param {Uuid} entityID - The entity ID.
* @param {boolean} [requiresRemovalFromTree=true] - Requires removal from tree.
* @param {boolean} [requiresRemovalFromTree=true] - unused
* @deprecated This function is deprecated and will be removed.
*/
Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true);

View file

@ -171,8 +171,6 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer<ReceivedMessage> m
return;
}
// AJT: DON'T CHECK THIS IN, disable model URL overrides.
/*
// only accept an override if this is for a trait type we override
// and the version matches what we last sent for skeleton
if (traitType == AvatarTraits::SkeletonModelURL
@ -194,7 +192,6 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer<ReceivedMessage> m
} else {
message->seek(message->getPosition() + traitBinarySize);
}
*/
}
}
}

View file

@ -226,7 +226,7 @@ void EntityTreeRenderer::stopDomainAndNonOwnedEntities() {
EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID);
if (entityItem && !entityItem->getScript().isEmpty()) {
if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) {
if (!(entityItem->isLocalEntity() || entityItem->isMyAvatarEntity())) {
if (_currentEntitiesInside.contains(entityID)) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
@ -240,7 +240,6 @@ void EntityTreeRenderer::stopDomainAndNonOwnedEntities() {
void EntityTreeRenderer::clearDomainAndNonOwnedEntities() {
stopDomainAndNonOwnedEntities();
auto sessionUUID = getTree()->getMyAvatarSessionUUID();
std::unordered_map<EntityItemID, EntityRendererPointer> savedEntities;
std::unordered_set<EntityRendererPointer> savedRenderables;
// remove all entities from the scene
@ -249,7 +248,7 @@ void EntityTreeRenderer::clearDomainAndNonOwnedEntities() {
for (const auto& entry : _entitiesInScene) {
const auto& renderer = entry.second;
const EntityItemPointer& entityItem = renderer->getEntity();
if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == sessionUUID))) {
if (!(entityItem->isLocalEntity() || entityItem->isMyAvatarEntity())) {
fadeOutRenderable(renderer);
} else {
savedEntities[entry.first] = entry.second;
@ -261,7 +260,7 @@ void EntityTreeRenderer::clearDomainAndNonOwnedEntities() {
_renderablesToUpdate = savedRenderables;
_entitiesInScene = savedEntities;
if (_layeredZones.clearDomainAndNonOwnedZones(sessionUUID)) {
if (_layeredZones.clearDomainAndNonOwnedZones()) {
applyLayeredZones();
}
@ -683,7 +682,7 @@ void EntityTreeRenderer::leaveDomainAndNonOwnedEntities() {
QSet<EntityItemID> currentEntitiesInsideToSave;
foreach (const EntityItemID& entityID, _currentEntitiesInside) {
EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID);
if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) {
if (!(entityItem->isLocalEntity() || entityItem->isMyAvatarEntity())) {
emit leaveEntity(entityID);
if (_entitiesScriptEngine) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
@ -1215,13 +1214,13 @@ void EntityTreeRenderer::updateZone(const EntityItemID& id) {
}
}
bool EntityTreeRenderer::LayeredZones::clearDomainAndNonOwnedZones(const QUuid& sessionUUID) {
bool EntityTreeRenderer::LayeredZones::clearDomainAndNonOwnedZones() {
bool zonesChanged = false;
auto it = begin();
while (it != end()) {
auto zone = it->zone.lock();
if (!zone || !(zone->isLocalEntity() || (zone->isAvatarEntity() && zone->getOwningAvatarID() == sessionUUID))) {
if (!zone || !(zone->isLocalEntity() || zone->isMyAvatarEntity())) {
zonesChanged = true;
it = erase(it);
} else {
@ -1362,6 +1361,10 @@ EntityItemPointer EntityTreeRenderer::getEntity(const EntityItemID& id) {
return result;
}
void EntityTreeRenderer::deleteEntity(const EntityItemID& id) const {
DependencyManager::get<EntityScriptingInterface>()->deleteEntity(id);
}
void EntityTreeRenderer::onEntityChanged(const EntityItemID& id) {
_changedEntitiesGuard.withWriteLock([&] {
_changedEntities.insert(id);

View file

@ -118,6 +118,7 @@ public:
void setProxyWindow(const EntityItemID& id, QWindow* proxyWindow);
void setCollisionSound(const EntityItemID& id, const SharedSoundPointer& sound);
EntityItemPointer getEntity(const EntityItemID& id);
void deleteEntity(const EntityItemID& id) const;
void onEntityChanged(const EntityItemID& id);
// Access the workload Space
@ -229,7 +230,7 @@ private:
class LayeredZones : public std::vector<LayeredZone> {
public:
bool clearDomainAndNonOwnedZones(const QUuid& sessionUUID);
bool clearDomainAndNonOwnedZones();
void sort() { std::sort(begin(), end(), std::less<LayeredZone>()); }
bool equals(const LayeredZones& other) const;

View file

@ -43,6 +43,7 @@ const Transform& EntityRenderer::getModelTransform() const {
void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::Status::Getters& statusGetters) {
auto nodeList = DependencyManager::get<NodeList>();
// DANGER: nodeList->getSessionUUID() will return null id when not connected to domain.
const QUuid& myNodeID = nodeList->getSessionUUID();
statusGetters.push_back([entity]() -> render::Item::Status::Value {
@ -103,9 +104,9 @@ void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::St
(unsigned char)render::Item::Status::Icon::HAS_ACTIONS);
});
statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value {
statusGetters.push_back([entity] () -> render::Item::Status::Value {
if (entity->isAvatarEntity()) {
if (entity->getOwningAvatarID() == myNodeID) {
if (entity->isMyAvatarEntity()) {
return render::Item::Status::Value(1.0f, render::Item::Status::Value::GREEN,
(unsigned char)render::Item::Status::Icon::ENTITY_HOST_TYPE);
} else {

View file

@ -53,6 +53,15 @@ void DeleteEntityOperator::addEntityIDToDeleteList(const EntityItemID& searchEnt
}
}
void DeleteEntityOperator::addEntityToDeleteList(const EntityItemPointer& entity) {
assert(entity && entity->getElement());
EntityToDeleteDetails details;
details.entity = entity;
details.containingElement = entity->getElement();
details.cube = details.containingElement->getAACube();
_entitiesToDelete << details;
_lookingCount++;
}
// does this entity tree element contain the old entity
bool DeleteEntityOperator::subTreeContainsSomeEntitiesToDelete(const OctreeElementPointer& element) {

View file

@ -42,6 +42,7 @@ public:
~DeleteEntityOperator();
void addEntityIDToDeleteList(const EntityItemID& searchEntityID);
void addEntityToDeleteList(const EntityItemPointer& entity);
virtual bool preRecursion(const OctreeElementPointer& element) override;
virtual bool postRecursion(const OctreeElementPointer& element) override;

View file

@ -43,7 +43,7 @@ QList<EntityItemID> EntityEditFilters::getZonesByPosition(glm::vec3& position) {
}
bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut,
bool& wasChanged, EntityTree::FilterType filterType, EntityItemID& itemID, EntityItemPointer& existingEntity) {
bool& wasChanged, EntityTree::FilterType filterType, EntityItemID& itemID, const EntityItemPointer& existingEntity) {
// get the ids of all the zones (plus the global entity edit filter) that the position
// lies within

View file

@ -55,7 +55,7 @@ public:
void removeFilter(EntityItemID entityID);
bool filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged,
EntityTree::FilterType filterType, EntityItemID& entityID, EntityItemPointer& existingEntity);
EntityTree::FilterType filterType, EntityItemID& entityID, const EntityItemPointer& existingEntity);
signals:
void filterAdded(EntityItemID id, bool success);

View file

@ -73,8 +73,12 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
if (properties.getEntityHostType() == entity::HostType::AVATAR) {
if (!_myAvatar) {
qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit with no myAvatar";
} else if (properties.getOwningAvatarID() == _myAvatar->getID()) {
// this is an avatar-based entity --> update our avatar-data rather than sending to the entity-server
} else if (properties.getOwningAvatarID() == _myAvatar->getID() || properties.getOwningAvatarID() == AVATAR_SELF_ID) {
// this is a local avatar-entity --> update our avatar-data rather than sending to the entity-server
// Note: we store AVATAR_SELF_ID in EntityItem::_owningAvatarID and we usually
// store the actual sessionUUID in EntityItemProperties::_owningAvatarID.
// However at this context we check for both cases just in case. Really we just want to know
// where to route the data: entity-server or avatar-mixer.
queueEditAvatarEntityMessage(entityTree, entityItemID);
} else {
qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit for another avatar";

View file

@ -1347,7 +1347,7 @@ EntityItemProperties EntityItem::getProperties(const EntityPropertyFlags& desire
COPY_ENTITY_PROPERTY_TO_PROPERTIES(created, getCreated);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(lastEditedBy, getLastEditedBy);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(entityHostType, getEntityHostType);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(owningAvatarID, getOwningAvatarID);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(owningAvatarID, getOwningAvatarIDForProperties);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(queryAACube, getQueryAACube);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(canCastShadow, getCanCastShadow);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(isVisibleInSecondaryCamera, isVisibleInSecondaryCamera);
@ -3203,6 +3203,7 @@ void EntityItem::somethingChangedNotification() {
});
}
// static
void EntityItem::retrieveMarketplacePublicKey() {
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
@ -3234,6 +3235,23 @@ void EntityItem::retrieveMarketplacePublicKey() {
});
}
void EntityItem::collectChildrenForDelete(std::vector<EntityItemPointer>& entitiesToDelete, const QUuid& sessionID) const {
// Deleting an entity has consequences for its children, however there are rules dictating what can be deleted.
// This method helps enforce those rules: not for this entity, but for its children.
for (SpatiallyNestablePointer child : getChildren()) {
if (child && child->getNestableType() == NestableType::Entity) {
EntityItemPointer childEntity = std::static_pointer_cast<EntityItem>(child);
// NOTE: null sessionID means "collect ALL known children", else we only collect: local-entities and myAvatar-entities
if (sessionID.isNull() || childEntity->isLocalEntity() || childEntity->isMyAvatarEntity()) {
if (std::find(entitiesToDelete.begin(), entitiesToDelete.end(), childEntity) == entitiesToDelete.end()) {
entitiesToDelete.push_back(childEntity);
childEntity->collectChildrenForDelete(entitiesToDelete, sessionID);
}
}
}
}
}
void EntityItem::setSpaceIndex(int32_t index) {
assert(_spaceIndex == -1);
_spaceIndex = index;
@ -3398,6 +3416,7 @@ void EntityItem::prepareForSimulationOwnershipBid(EntityItemProperties& properti
properties.setSimulationOwner(Physics::getSessionUUID(), priority);
setPendingOwnershipPriority(priority);
// TODO: figure out if it would be OK to NOT bother set these properties here
properties.setEntityHostType(getEntityHostType());
properties.setOwningAvatarID(getOwningAvatarID());
setLastBroadcast(now); // for debug/physics status icons
@ -3409,9 +3428,27 @@ bool EntityItem::isWearable() const {
}
bool EntityItem::isMyAvatarEntity() const {
return _hostType == entity::HostType::AVATAR && Physics::getSessionUUID() == _owningAvatarID;
return _hostType == entity::HostType::AVATAR && AVATAR_SELF_ID == _owningAvatarID;
};
QUuid EntityItem::getOwningAvatarIDForProperties() const {
if (isMyAvatarEntity()) {
// NOTE: we always store AVATAR_SELF_ID for MyAvatar's avatar entities,
// however for EntityItemProperties to be consumed by outside contexts (e.g. JS)
// we use the actual "sessionUUID" which is conveniently cached in the Physics namespace
return Physics::getSessionUUID();
}
return _owningAvatarID;
}
void EntityItem::setOwningAvatarID(const QUuid& owningAvatarID) {
if (!owningAvatarID.isNull() && owningAvatarID == Physics::getSessionUUID()) {
_owningAvatarID = AVATAR_SELF_ID;
} else {
_owningAvatarID = owningAvatarID;
}
}
void EntityItem::addGrab(GrabPointer grab) {
enableNoBootstrap();
SpatiallyNestable::addGrab(grab);

View file

@ -18,6 +18,7 @@
#include <glm/glm.hpp>
#include <QtGui/QWindow>
#include <QSet>
#include <Octree.h> // for EncodeBitstreamParams class
#include <OctreeElement.h> // for OctreeElement::AppendState
@ -49,6 +50,7 @@ typedef std::shared_ptr<EntityTree> EntityTreePointer;
typedef std::shared_ptr<EntityDynamicInterface> EntityDynamicPointer;
typedef std::shared_ptr<EntityTreeElement> EntityTreeElementPointer;
using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr<EntityTreeElementExtraEncodeData>;
using SetOfEntities = QSet<EntityItemPointer>;
#define DONT_ALLOW_INSTANTIATION virtual void pureVirtualFunctionPlaceHolder() = 0;
#define ALLOW_INSTANTIATION virtual void pureVirtualFunctionPlaceHolder() override { };
@ -514,7 +516,8 @@ public:
// if this entity is an avatar entity, which avatar is it associated with?
QUuid getOwningAvatarID() const { return _owningAvatarID; }
virtual void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; }
QUuid getOwningAvatarIDForProperties() const;
void setOwningAvatarID(const QUuid& owningAvatarID);
virtual bool wantsHandControllerPointerEvents() const { return false; }
virtual bool wantsKeyboardFocus() const { return false; }
@ -540,6 +543,8 @@ public:
static QString _marketplacePublicKey;
static void retrieveMarketplacePublicKey();
void collectChildrenForDelete(std::vector<EntityItemPointer>& entitiesToDelete, const QUuid& sessionID) const;
float getBoundingRadius() const { return _boundingRadius; }
void setSpaceIndex(int32_t index);
int32_t getSpaceIndex() const { return _spaceIndex; }

View file

@ -3936,7 +3936,7 @@ bool EntityItemProperties::decodeCloneEntityMessage(const QByteArray& buffer, in
processedBytes = 0;
if (NUM_BYTES_RFC4122_UUID * 2 > packetLength) {
qCDebug(entities) << "EntityItemProperties::processEraseMessageDetails().... bailing because not enough bytes in buffer";
qCDebug(entities) << "EntityItemProperties::decodeCloneEntityMessage().... bailing because not enough bytes in buffer";
return false; // bail to prevent buffer overflow
}

View file

@ -480,17 +480,11 @@ QUuid EntityScriptingInterface::addEntityInternal(const EntityItemProperties& pr
_activityTracking.addedEntityCount++;
auto nodeList = DependencyManager::get<NodeList>();
auto sessionID = nodeList->getSessionUUID();
EntityItemProperties propertiesWithSimID = properties;
propertiesWithSimID.setEntityHostType(entityHostType);
if (entityHostType == entity::HostType::AVATAR) {
if (sessionID.isNull()) {
// null sessionID is unacceptable in this case
sessionID = AVATAR_SELF_ID;
}
propertiesWithSimID.setOwningAvatarID(sessionID);
// only allow adding our own avatar entities from script
propertiesWithSimID.setOwningAvatarID(AVATAR_SELF_ID);
} else if (entityHostType == entity::HostType::LOCAL) {
// For now, local entities are always collisionless
// TODO: create a separate, local physics simulation that just handles local entities (and MyAvatar?)
@ -498,6 +492,8 @@ QUuid EntityScriptingInterface::addEntityInternal(const EntityItemProperties& pr
}
// the created time will be set in EntityTree::addEntity by recordCreationTime()
auto nodeList = DependencyManager::get<NodeList>();
auto sessionID = nodeList->getSessionUUID();
propertiesWithSimID.setLastEditedBy(sessionID);
bool scalesWithParent = propertiesWithSimID.getScalesWithParent();
@ -805,7 +801,7 @@ QUuid EntityScriptingInterface::editEntity(const QUuid& id, const EntityItemProp
return;
}
if (entity->isAvatarEntity() && entity->getOwningAvatarID() != sessionID && entity->getOwningAvatarID() != AVATAR_SELF_ID) {
if (entity->isAvatarEntity() && !entity->isMyAvatarEntity()) {
// don't edit other avatar's avatarEntities
properties = EntityItemProperties();
return;
@ -825,7 +821,7 @@ QUuid EntityScriptingInterface::editEntity(const QUuid& id, const EntityItemProp
// flag for simulation ownership, or upgrade existing ownership priority
// (actual bids for simulation ownership are sent by the PhysicalEntitySimulation)
entity->upgradeScriptSimulationPriority(properties.computeSimulationBidPriority());
if (simulationOwner.getID() == sessionID) {
if (entity->isLocalEntity() || entity->isMyAvatarEntity() || simulationOwner.getID() == sessionID) {
// we own the simulation --> copy ALL restricted properties
properties.copySimulationRestrictedProperties(entity);
} else {
@ -970,43 +966,43 @@ void EntityScriptingInterface::deleteEntity(const QUuid& id) {
_activityTracking.deletedEntityCount++;
EntityItemID entityID(id);
bool shouldSendDeleteToServer = true;
// If we have a local entity tree set, then also update it.
if (_entityTree) {
_entityTree->withWriteLock([&] {
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
if (entity) {
auto nodeList = DependencyManager::get<NodeList>();
const QUuid myNodeID = nodeList->getSessionUUID();
if (entity->isAvatarEntity() && entity->getOwningAvatarID() != myNodeID) {
// don't delete other avatar's avatarEntities
shouldSendDeleteToServer = false;
return;
}
if (entity->getLocked()) {
shouldSendDeleteToServer = false;
} else {
// only delete local entities, server entities will round trip through the server filters
if (!entity->isDomainEntity() || _entityTree->isServerlessMode()) {
shouldSendDeleteToServer = false;
_entityTree->deleteEntity(entityID);
if (entity->isAvatarEntity() && getEntityPacketSender()->getMyAvatar()) {
getEntityPacketSender()->getMyAvatar()->clearAvatarEntity(entityID, false);
}
}
}
}
});
if (!_entityTree) {
return;
}
// if at this point, we know the id, and we should still delete the entity, send the update to the entity server
if (shouldSendDeleteToServer) {
getEntityPacketSender()->queueEraseEntityMessage(entityID);
EntityItemID entityID(id);
// If we have a local entity tree set, then also update it.
std::vector<EntityItemPointer> entitiesToDeleteImmediately;
_entityTree->withWriteLock([&] {
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
if (entity) {
if (entity->isAvatarEntity() && !entity->isMyAvatarEntity()) {
// don't delete other avatar's avatarEntities
return;
}
if (entity->getLocked()) {
return;
}
// Deleting an entity has consequences for linked children: some can be deleted but others can't.
// Local- and my-avatar-entities can be deleted immediately, but other-avatar-entities can't be deleted
// by this context, and a domain-entity must rountrip through the entity-server for authorization.
if (entity->isDomainEntity()) {
getEntityPacketSender()->queueEraseEntityMessage(id);
} else {
entitiesToDeleteImmediately.push_back(entity);
const auto sessionID = DependencyManager::get<NodeList>()->getSessionUUID();
entity->collectChildrenForDelete(entitiesToDeleteImmediately, sessionID);
_entityTree->deleteEntitiesByPointer(entitiesToDeleteImmediately);
}
}
});
for (auto entity : entitiesToDeleteImmediately) {
if (entity->isMyAvatarEntity()) {
getEntityPacketSender()->getMyAvatar()->clearAvatarEntity(entity->getID(), false);
}
}
}
@ -1653,12 +1649,9 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID,
return false;
}
auto nodeList = DependencyManager::get<NodeList>();
const QUuid myNodeID = nodeList->getSessionUUID();
EntityItemPointer entity;
bool doTransmit = false;
_entityTree->withWriteLock([this, &entity, entityID, myNodeID, &doTransmit, actor] {
_entityTree->withWriteLock([this, &entity, entityID, &doTransmit, actor] {
EntitySimulationPointer simulation = _entityTree->getSimulation();
entity = _entityTree->findEntityByEntityItemID(entityID);
if (!entity) {
@ -1671,7 +1664,7 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID,
return;
}
if (entity->isAvatarEntity() && entity->getOwningAvatarID() != myNodeID) {
if (entity->isAvatarEntity() && !entity->isMyAvatarEntity()) {
return;
}

View file

@ -19,41 +19,37 @@
void EntitySimulation::setEntityTree(EntityTreePointer tree) {
if (_entityTree && _entityTree != tree) {
_mortalEntities.clear();
_nextExpiry = std::numeric_limits<uint64_t>::max();
_entitiesToUpdate.clear();
_entitiesToSort.clear();
_simpleKinematicEntities.clear();
_changedEntities.clear();
_entitiesToUpdate.clear();
_mortalEntities.clear();
_nextExpiry = std::numeric_limits<uint64_t>::max();
}
_entityTree = tree;
}
void EntitySimulation::updateEntities() {
PerformanceTimer perfTimer("EntitySimulation::updateEntities");
QMutexLocker lock(&_mutex);
uint64_t now = usecTimestampNow();
PerformanceTimer perfTimer("EntitySimulation::updateEntities");
// these methods may accumulate entries in _entitiesToBeDeleted
expireMortalEntities(now);
callUpdateOnEntitiesThatNeedIt(now);
moveSimpleKinematics(now);
updateEntitiesInternal(now);
sortEntitiesThatMoved();
processDeadEntities();
}
void EntitySimulation::takeDeadEntities(SetOfEntities& entitiesToDelete) {
QMutexLocker lock(&_mutex);
entitiesToDelete.swap(_deadEntities);
_deadEntities.clear();
}
void EntitySimulation::removeEntityInternal(EntityItemPointer entity) {
// remove from all internal lists except _deadEntities
_mortalEntities.remove(entity);
_entitiesToUpdate.remove(entity);
void EntitySimulation::removeEntityFromInternalLists(EntityItemPointer entity) {
// protected: _mutex lock is guaranteed
// remove from all internal lists except _deadEntitiesToRemoveFromTree
_entitiesToSort.remove(entity);
_simpleKinematicEntities.remove(entity);
_allEntities.remove(entity);
_entitiesToUpdate.remove(entity);
_mortalEntities.remove(entity);
entity->setSimulated(false);
}
@ -62,10 +58,9 @@ void EntitySimulation::prepareEntityForDelete(EntityItemPointer entity) {
assert(entity->isDead());
if (entity->isSimulated()) {
QMutexLocker lock(&_mutex);
entity->clearActions(getThisPointer());
removeEntityInternal(entity);
removeEntityFromInternalLists(entity);
if (entity->getElement()) {
_deadEntities.insert(entity);
_deadEntitiesToRemoveFromTree.insert(entity);
_entityTree->cleanupCloneIDs(entity->getEntityItemID());
}
}
@ -149,10 +144,8 @@ void EntitySimulation::sortEntitiesThatMoved() {
_entitiesToSort.clear();
}
void EntitySimulation::addEntity(EntityItemPointer entity) {
QMutexLocker lock(&_mutex);
assert(entity);
entity->deserializeActions();
void EntitySimulation::addEntityToInternalLists(EntityItemPointer entity) {
// protected: _mutex lock is guaranteed
if (entity->isMortal()) {
_mortalEntities.insert(entity);
uint64_t expiry = entity->getExpiry();
@ -163,10 +156,14 @@ void EntitySimulation::addEntity(EntityItemPointer entity) {
if (entity->needsToCallUpdate()) {
_entitiesToUpdate.insert(entity);
}
addEntityInternal(entity);
_allEntities.insert(entity);
entity->setSimulated(true);
}
void EntitySimulation::addEntity(EntityItemPointer entity) {
QMutexLocker lock(&_mutex);
assert(entity);
addEntityToInternalLists(entity);
// DirtyFlags are used to signal changes to entities that have already been added,
// so we can clear them for this entity which has just been added.
@ -218,16 +215,14 @@ void EntitySimulation::processChangedEntity(const EntityItemPointer& entity) {
void EntitySimulation::clearEntities() {
QMutexLocker lock(&_mutex);
_mortalEntities.clear();
_nextExpiry = std::numeric_limits<uint64_t>::max();
_entitiesToUpdate.clear();
_entitiesToSort.clear();
_simpleKinematicEntities.clear();
clearEntitiesInternal();
_changedEntities.clear();
_allEntities.clear();
_deadEntities.clear();
_deadEntitiesToRemoveFromTree.clear();
_entitiesToUpdate.clear();
_mortalEntities.clear();
_nextExpiry = std::numeric_limits<uint64_t>::max();
}
void EntitySimulation::moveSimpleKinematics(uint64_t now) {
@ -263,25 +258,19 @@ void EntitySimulation::moveSimpleKinematics(uint64_t now) {
}
}
void EntitySimulation::addDynamic(EntityDynamicPointer dynamic) {
QMutexLocker lock(&_dynamicsMutex);
_dynamicsToAdd += dynamic;
}
void EntitySimulation::removeDynamic(const QUuid dynamicID) {
QMutexLocker lock(&_dynamicsMutex);
_dynamicsToRemove += dynamicID;
}
void EntitySimulation::removeDynamics(QList<QUuid> dynamicIDsToRemove) {
QMutexLocker lock(&_dynamicsMutex);
foreach(QUuid uuid, dynamicIDsToRemove) {
_dynamicsToRemove.insert(uuid);
void EntitySimulation::processDeadEntities() {
if (_deadEntitiesToRemoveFromTree.empty()) {
return;
}
}
void EntitySimulation::applyDynamicChanges() {
QMutexLocker lock(&_dynamicsMutex);
_dynamicsToAdd.clear();
_dynamicsToRemove.clear();
std::vector<EntityItemPointer> entitiesToDeleteImmediately;
entitiesToDeleteImmediately.reserve(_deadEntitiesToRemoveFromTree.size());
QUuid nullSessionID;
foreach (auto entity, _deadEntitiesToRemoveFromTree) {
entitiesToDeleteImmediately.push_back(entity);
entity->collectChildrenForDelete(entitiesToDeleteImmediately, nullSessionID);
}
if (_entityTree) {
_entityTree->deleteEntitiesByPointer(entitiesToDeleteImmediately);
}
_deadEntitiesToRemoveFromTree.clear();
}

View file

@ -16,17 +16,14 @@
#include <unordered_set>
#include <QtCore/QObject>
#include <QSet>
#include <QVector>
#include <PerfStat.h>
#include "EntityDynamicInterface.h"
#include "EntityItem.h"
#include "EntityTree.h"
using EntitySimulationPointer = std::shared_ptr<EntitySimulation>;
using SetOfEntities = QSet<EntityItemPointer>;
using VectorOfEntities = QVector<EntityItemPointer>;
// the EntitySimulation needs to know when these things change on an entity,
@ -47,8 +44,8 @@ const int DIRTY_SIMULATION_FLAGS =
class EntitySimulation : public QObject, public std::enable_shared_from_this<EntitySimulation> {
public:
EntitySimulation() : _mutex(QMutex::Recursive), _entityTree(NULL), _nextExpiry(std::numeric_limits<uint64_t>::max()) { }
virtual ~EntitySimulation() { setEntityTree(NULL); }
EntitySimulation() : _mutex(QMutex::Recursive), _nextExpiry(std::numeric_limits<uint64_t>::max()), _entityTree(nullptr) { }
virtual ~EntitySimulation() { setEntityTree(nullptr); }
inline EntitySimulationPointer getThisPointer() const {
return std::const_pointer_cast<EntitySimulation>(shared_from_this());
@ -57,12 +54,12 @@ public:
/// \param tree pointer to EntityTree which is stored internally
void setEntityTree(EntityTreePointer tree);
void updateEntities();
virtual void updateEntities();
virtual void addDynamic(EntityDynamicPointer dynamic);
virtual void removeDynamic(const QUuid dynamicID);
virtual void removeDynamics(QList<QUuid> dynamicIDsToRemove);
virtual void applyDynamicChanges();
// FIXME: remove these
virtual void addDynamic(EntityDynamicPointer dynamic) {}
virtual void removeDynamic(const QUuid dynamicID) {}
virtual void applyDynamicChanges() {};
/// \param entity pointer to EntityItem to be added
/// \sideeffect sets relevant backpointers in entity, but maybe later when appropriate data structures are locked
@ -72,27 +69,22 @@ public:
/// call this whenever an entity was changed from some EXTERNAL event (NOT by the EntitySimulation itself)
void changeEntity(EntityItemPointer entity);
void clearEntities();
virtual void clearEntities();
void moveSimpleKinematics(uint64_t now);
EntityTreePointer getEntityTree() { return _entityTree; }
virtual void takeDeadEntities(SetOfEntities& entitiesToDelete);
/// \param entity pointer to EntityItem that needs to be put on the entitiesToDelete list and removed from others.
virtual void prepareEntityForDelete(EntityItemPointer entity);
void processChangedEntities();
virtual void queueEraseDomainEntity(const QUuid& id) const { }
protected:
// These pure virtual methods are protected because they are not to be called will-nilly. The base class
// calls them in the right places.
virtual void updateEntitiesInternal(uint64_t now) = 0;
virtual void addEntityInternal(EntityItemPointer entity) = 0;
virtual void removeEntityInternal(EntityItemPointer entity);
virtual void addEntityToInternalLists(EntityItemPointer entity);
virtual void removeEntityFromInternalLists(EntityItemPointer entity);
virtual void processChangedEntity(const EntityItemPointer& entity);
virtual void clearEntitiesInternal() = 0;
virtual void processDeadEntities();
void expireMortalEntities(uint64_t now);
void callUpdateOnEntitiesThatNeedIt(uint64_t now);
@ -102,27 +94,21 @@ protected:
SetOfEntities _entitiesToSort; // entities moved by simulation (and might need resort in EntityTree)
SetOfEntities _simpleKinematicEntities; // entities undergoing non-colliding kinematic motion
QList<EntityDynamicPointer> _dynamicsToAdd;
QSet<QUuid> _dynamicsToRemove;
QMutex _dynamicsMutex { QMutex::Recursive };
protected:
SetOfEntities _deadEntities; // dead entities that might still be in the _entityTree
SetOfEntities _deadEntitiesToRemoveFromTree;
private:
void moveSimpleKinematics();
// back pointer to EntityTree structure
EntityTreePointer _entityTree;
// We maintain multiple lists, each for its distinct purpose.
// An entity may be in more than one list.
std::unordered_set<EntityItemPointer> _changedEntities; // all changes this frame
SetOfEntities _allEntities; // tracks all entities added the simulation
SetOfEntities _entitiesToUpdate; // entities that need to call EntityItem::update()
SetOfEntities _mortalEntities; // entities that have an expiry
uint64_t _nextExpiry;
SetOfEntities _entitiesToUpdate; // entities that need to call EntityItem::update()
// back pointer to EntityTree structure
EntityTreePointer _entityTree;
};
#endif // hifi_EntitySimulation_h

View file

@ -84,7 +84,7 @@ void EntityTree::eraseDomainAndNonOwnedEntities() {
emit clearingEntities();
if (_simulation) {
// local entities are not in the simulation, so we clear ALL
// local-entities are not in the simulation, so we clear ALL
_simulation->clearEntities();
}
@ -98,14 +98,15 @@ void EntityTree::eraseDomainAndNonOwnedEntities() {
if (element) {
element->cleanupDomainAndNonOwnedEntities();
}
if (entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getMyAvatarSessionUUID())) {
savedEntities[entity->getEntityItemID()] = entity;
} else {
int32_t spaceIndex = entity->getSpaceIndex();
if (spaceIndex != -1) {
// stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
if (!getIsServer()) {
if (entity->isLocalEntity() || entity->isMyAvatarEntity()) {
savedEntities[entity->getEntityItemID()] = entity;
} else {
int32_t spaceIndex = entity->getSpaceIndex();
if (spaceIndex != -1) {
// stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
}
}
}
}
@ -121,7 +122,7 @@ void EntityTree::eraseDomainAndNonOwnedEntities() {
foreach (EntityItemWeakPointer entityItem, _needsParentFixup) {
auto entity = entityItem.lock();
if (entity && (entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getMyAvatarSessionUUID()))) {
if (entity && (entity->isLocalEntity() || entity->isMyAvatarEntity())) {
needParentFixup.push_back(entityItem);
}
}
@ -144,10 +145,12 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) {
if (element) {
element->cleanupEntities();
}
int32_t spaceIndex = entity->getSpaceIndex();
if (spaceIndex != -1) {
// assume stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
if (!getIsServer()) {
int32_t spaceIndex = entity->getSpaceIndex();
if (spaceIndex != -1) {
// assume stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
}
}
}
});
@ -605,61 +608,21 @@ void EntityTree::setSimulation(EntitySimulationPointer simulation) {
}
void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ignoreWarnings) {
EntityTreeElementPointer containingElement = getContainingElement(entityID);
if (!containingElement) {
if (!ignoreWarnings) {
qCWarning(entities) << "EntityTree::deleteEntity() on non-existent entityID=" << entityID;
}
return;
}
EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID);
if (!existingEntity) {
if (!ignoreWarnings) {
qCWarning(entities) << "EntityTree::deleteEntity() on non-existant entity item with entityID=" << entityID;
}
return;
}
if (existingEntity->getLocked() && !force) {
if (!ignoreWarnings) {
qCDebug(entities) << "ERROR! EntityTree::deleteEntity() trying to delete locked entity. entityID=" << entityID;
}
return;
}
cleanupCloneIDs(entityID);
unhookChildAvatar(entityID);
emit deletingEntity(entityID);
emit deletingEntityPointer(existingEntity.get());
// NOTE: callers must lock the tree before using this method
DeleteEntityOperator theOperator(getThisPointer(), entityID);
existingEntity->forEachDescendant([&](SpatiallyNestablePointer descendant) {
auto descendantID = descendant->getID();
theOperator.addEntityIDToDeleteList(descendantID);
emit deletingEntity(descendantID);
EntityItemPointer descendantEntity = std::dynamic_pointer_cast<EntityItem>(descendant);
if (descendantEntity) {
emit deletingEntityPointer(descendantEntity.get());
}
});
recurseTreeWithOperator(&theOperator);
processRemovedEntities(theOperator);
_isDirty = true;
// NOTE: can be called without lock because deleteEntitiesByID() will lock
std::vector<EntityItemID> ids;
ids.push_back(entityID);
deleteEntitiesByID(ids, force, ignoreWarnings);
}
void EntityTree::unhookChildAvatar(const EntityItemID entityID) {
EntityItemPointer entity = findEntityByEntityItemID(entityID);
entity->forEachDescendant([&](SpatiallyNestablePointer child) {
if (child->getNestableType() == NestableType::Avatar) {
child->setParentID(nullptr);
}
});
if (!getIsServer()) {
EntityItemPointer entity = findEntityByEntityItemID(entityID);
entity->forEachDescendant([&](SpatiallyNestablePointer child) {
if (child->getNestableType() == NestableType::Avatar) {
child->setParentID(nullptr);
}
});
}
}
void EntityTree::cleanupCloneIDs(const EntityItemID& entityID) {
@ -684,39 +647,104 @@ void EntityTree::cleanupCloneIDs(const EntityItemID& entityID) {
}
}
void EntityTree::deleteEntities(QSet<EntityItemID> entityIDs, bool force, bool ignoreWarnings) {
// NOTE: callers must lock the tree before using this method
void EntityTree::recursivelyFilterAndCollectForDelete(const EntityItemPointer& entity, std::vector<EntityItemPointer>& entitiesToDelete, bool force) const {
// tree must be read-locked before calling this method
//TODO: assert(treeIsLocked);
assert(entity);
if (entity->getElement() && (std::find(entitiesToDelete.begin(), entitiesToDelete.end(), entity) == entitiesToDelete.end())) {
// filter
bool allowed = force;
if (!allowed) {
bool wasChanged = false;
auto startFilter = usecTimestampNow();
EntityItemProperties dummyProperties;
allowed = filterProperties(entity, dummyProperties, dummyProperties, wasChanged, FilterType::Delete);
auto endFilter = usecTimestampNow();
_totalFilterTime += endFilter - startFilter;
}
if (allowed) {
entitiesToDelete.push_back(entity);
for (SpatiallyNestablePointer child : entity->getChildren()) {
if (child && child->getNestableType() == NestableType::Entity) {
EntityItemPointer childEntity = std::static_pointer_cast<EntityItem>(child);
recursivelyFilterAndCollectForDelete(childEntity, entitiesToDelete, force);
}
}
}
}
}
void EntityTree::deleteEntitiesByID(const std::vector<EntityItemID>& ids, bool force, bool ignoreWarnings) {
// this method has two paths:
// (a) entity-server: applies delete filter
// (b) interface-client: deletes local- and my-avatar-entities immediately, submits domainEntity deletes to the entity-server
if (getIsServer()) {
withWriteLock([&] {
std::vector<EntityItemPointer> entitiesToDelete;
entitiesToDelete.reserve(ids.size());
for (auto id : ids) {
EntityItemPointer entity;
{
QReadLocker locker(&_entityMapLock);
entity = _entityMap.value(id);
}
if (entity) {
recursivelyFilterAndCollectForDelete(entity, entitiesToDelete, force);
}
}
if (!entitiesToDelete.empty()) {
deleteEntitiesByPointer(entitiesToDelete);
}
});
} else {
std::vector<EntityItemID> domainEntitiesIDs;
std::vector<EntityItemPointer> entitiesToDelete;
entitiesToDelete.reserve(ids.size());
QUuid sessionID = DependencyManager::get<NodeList>()->getSessionUUID();
withWriteLock([&] {
for (auto id : ids) {
EntityItemPointer entity;
{
QReadLocker locker(&_entityMapLock);
entity = _entityMap.value(id);
}
if (entity) {
if (entity->isDomainEntity()) {
// domain-entity deletes must round-trip through entity-server
domainEntitiesIDs.push_back(id);
} else if (force || entity->isLocalEntity() || entity->isMyAvatarEntity()) {
entitiesToDelete.push_back(entity);
entity->collectChildrenForDelete(entitiesToDelete, sessionID);
}
}
}
if (!entitiesToDelete.empty()) {
deleteEntitiesByPointer(entitiesToDelete);
}
});
if (!domainEntitiesIDs.empty() && _simulation) {
for (auto id : domainEntitiesIDs) {
_simulation->queueEraseDomainEntity(id);
}
}
}
}
void EntityTree::deleteEntitiesByPointer(const std::vector<EntityItemPointer>& entities) {
// tree must be write-locked before calling this method
//TODO: assert(treeIsLocked);
// NOTE: there is no entity validation (i.e. is entity in tree?) nor snarfing of children beyond this point.
// Get those done BEFORE calling this method.
for (auto entity : entities) {
cleanupCloneIDs(entity->getID());
}
DeleteEntityOperator theOperator(getThisPointer());
foreach(const EntityItemID& entityID, entityIDs) {
EntityTreeElementPointer containingElement = getContainingElement(entityID);
if (!containingElement) {
if (!ignoreWarnings) {
qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entityID=" << entityID;
}
continue;
for (auto entity : entities) {
if (entity->getElement()) {
theOperator.addEntityToDeleteList(entity);
emit deletingEntity(entity->getID());
emit deletingEntityPointer(entity.get());
}
EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID);
if (!existingEntity) {
if (!ignoreWarnings) {
qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entity item with entityID=" << entityID;
}
continue;
}
if (existingEntity->getLocked() && !force) {
if (!ignoreWarnings) {
qCDebug(entities) << "ERROR! EntityTree::deleteEntities() trying to delete locked entity. entityID=" << entityID;
}
continue;
}
// tell our delete operator about this entityID
cleanupCloneIDs(entityID);
unhookChildAvatar(entityID);
theOperator.addEntityIDToDeleteList(entityID);
emit deletingEntity(entityID);
emit deletingEntityPointer(existingEntity.get());
}
if (!theOperator.getEntities().empty()) {
@ -727,23 +755,11 @@ void EntityTree::deleteEntities(QSet<EntityItemID> entityIDs, bool force, bool i
}
void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) {
// NOTE: assume tree already write-locked because this method only called in deleteEntitiesByPointer()
quint64 deletedAt = usecTimestampNow();
const RemovedEntities& entities = theOperator.getEntities();
foreach(const EntityToDeleteDetails& details, entities) {
EntityItemPointer theEntity = details.entity;
if (getIsServer()) {
QSet<EntityItemID> childrenIDs;
theEntity->forEachChild([&](SpatiallyNestablePointer child) {
if (child->getNestableType() == NestableType::Entity) {
childrenIDs += child->getID();
}
});
deleteEntities(childrenIDs, true, true);
}
theEntity->die();
if (getIsServer()) {
removeCertifiedEntityOnServer(theEntity);
@ -751,19 +767,24 @@ void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator)
QWriteLocker recentlyDeletedEntitiesLocker(&_recentlyDeletedEntitiesLock);
_recentlyDeletedEntityItemIDs.insert(deletedAt, theEntity->getEntityItemID());
} else {
theEntity->forEachDescendant([&](SpatiallyNestablePointer child) {
if (child->getNestableType() == NestableType::Avatar) {
child->setParentID(nullptr);
}
});
// on the client side, we also remember that we deleted this entity, we don't care about the time
trackDeletedEntity(theEntity->getEntityItemID());
}
int32_t spaceIndex = theEntity->getSpaceIndex();
if (spaceIndex != -1) {
// stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
}
}
if (theEntity->isSimulated()) {
_simulation->prepareEntityForDelete(theEntity);
}
int32_t spaceIndex = theEntity->getSpaceIndex();
if (spaceIndex != -1) {
// stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
}
}
}
@ -1369,7 +1390,7 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList<Q
}
bool EntityTree::filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) {
bool EntityTree::filterProperties(const EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) const {
bool accepted = true;
auto entityEditFilters = DependencyManager::get<EntityEditFilters>();
if (entityEditFilters) {
@ -1749,9 +1770,9 @@ void EntityTree::processChallengeOwnershipPacket(ReceivedMessage& message, const
}
}
// NOTE: Caller must lock the tree before calling this.
int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength,
const SharedNodePointer& senderNode) {
if (!getIsServer()) {
qCWarning(entities) << "EntityTree::processEditPacketData() should only be called on a server tree.";
return 0;
@ -2216,9 +2237,19 @@ void EntityTree::fixupNeedsParentFixups() {
}
void EntityTree::deleteDescendantsOfAvatar(QUuid avatarID) {
if (_childrenOfAvatars.contains(avatarID)) {
deleteEntities(_childrenOfAvatars[avatarID]);
_childrenOfAvatars.remove(avatarID);
QHash<QUuid, QSet<EntityItemID>>::const_iterator itr = _childrenOfAvatars.constFind(avatarID);
if (itr != _childrenOfAvatars.end()) {
if (!itr.value().empty()) {
std::vector<EntityItemID> ids;
ids.reserve(itr.value().size());
for (const auto id : itr.value()) {
ids.push_back(id);
}
bool force = true;
bool ignoreWarnings = true;
deleteEntitiesByID(ids, force, ignoreWarnings);
}
_childrenOfAvatars.erase(itr);
}
}
@ -2249,22 +2280,6 @@ void EntityTree::update(bool simulate) {
if (simulate && _simulation) {
withWriteLock([&] {
_simulation->updateEntities();
{
PROFILE_RANGE(simulation_physics, "Deletes");
SetOfEntities deadEntities;
_simulation->takeDeadEntities(deadEntities);
if (!deadEntities.empty()) {
// translate into list of ID's
QSet<EntityItemID> idsToDelete;
for (auto entity : deadEntities) {
idsToDelete.insert(entity->getEntityItemID());
}
// delete these things the roundabout way
deleteEntities(idsToDelete, true);
}
}
});
}
}
@ -2353,9 +2368,13 @@ bool EntityTree::shouldEraseEntity(EntityItemID entityID, const SharedNodePointe
return allowed;
}
// TODO: consider consolidating processEraseMessageDetails() and processEraseMessage()
int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode) {
// NOTE: this is only called by the interface-client on receipt of deleteEntity message from entity-server.
// Which means this is a state synchronization message from the the entity-server. It is saying
// "The following domain-entities have already been deleted". While need to perform sanity checking
// (e.g. verify these are domain entities) permissions need NOT checked for the domain-entities.
assert(!getIsServer());
// TODO: remove this stuff out of EntityTree:: and into interface-client code.
#ifdef EXTRA_ERASE_DEBUGGING
qCDebug(entities) << "EntityTree::processEraseMessage()";
#endif
@ -2366,10 +2385,9 @@ int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePo
message.readPrimitive(&numberOfIDs);
if (numberOfIDs > 0) {
QSet<EntityItemID> entityItemIDsToDelete;
QSet<EntityItemID> idsToDelete;
for (size_t i = 0; i < numberOfIDs; i++) {
if (NUM_BYTES_RFC4122_UUID > message.getBytesLeftToRead()) {
qCDebug(entities) << "EntityTree::processEraseMessage().... bailing because not enough bytes in buffer";
break; // bail to prevent buffer overflow
@ -2381,64 +2399,87 @@ int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePo
#endif
EntityItemID entityItemID(entityID);
idsToDelete << entityItemID;
}
if (shouldEraseEntity(entityID, sourceNode)) {
entityItemIDsToDelete << entityItemID;
cleanupCloneIDs(entityItemID);
// domain-entity deletion can trigger deletion of other entities the entity-server doesn't know about
// so we must recurse down the children and collect consequential deletes however
// we must first identify all domain-entities in idsToDelete so as to not overstep entity-server's authority
std::vector<EntityItemPointer> domainEntities;
domainEntities.reserve(idsToDelete.size());
for (auto id : idsToDelete) {
EntityItemPointer entity = _entityMap.value(id);
if (entity && entity->isDomainEntity()) {
domainEntities.push_back(entity);
}
}
deleteEntities(entityItemIDsToDelete, true, true);
// now we recurse domain-entities children and snarf consequential entities
// which are nomally just local-entities and myAvatar-entities
auto nodeList = DependencyManager::get<NodeList>();
QUuid sessionID = nodeList->getSessionUUID();
// NOTE: normally a null sessionID would be bad, as that would cause the collectDhildrenForDelete() method below
// to snarf domain-entities for which the interface-client is not authorized to delete without explicit instructions
// from the entity-server, however it is ok here because that would mean:
// (a) interface-client is not connected to a domain which means...
// (b) we should never get here (since this would correspond to a message from the entity-server) but...
// (c) who cares? When not connected to a domain the interface-client can do whatever it wants.
std::vector<EntityItemPointer> entitiesToDelete;
entitiesToDelete.reserve(domainEntities.size());
for (auto entity : domainEntities) {
entitiesToDelete.push_back(entity);
entity->collectChildrenForDelete(entitiesToDelete, sessionID);
}
if (!entitiesToDelete.empty()) {
deleteEntitiesByPointer(entitiesToDelete);
}
}
});
return message.getPosition();
}
// This version skips over the header
// NOTE: Caller must lock the tree before calling this.
// TODO: consider consolidating processEraseMessageDetails() and processEraseMessage()
// NOTE: Caller must write-lock the tree before calling this.
int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode) {
// NOTE: this is called on entity-server when receiving a delete request from an interface-client or agent
//TODO: assert(treeIsLocked);
assert(getIsServer());
#ifdef EXTRA_ERASE_DEBUGGING
qCDebug(entities) << "EntityTree::processEraseMessageDetails()";
#endif
const unsigned char* packetData = (const unsigned char*)dataByteArray.constData();
const unsigned char* dataAt = packetData;
size_t packetLength = dataByteArray.size();
size_t processedBytes = 0;
uint16_t numberOfIds = 0; // placeholder for now
memcpy(&numberOfIds, dataAt, sizeof(numberOfIds));
dataAt += sizeof(numberOfIds);
memcpy(&numberOfIds, dataByteArray.constData(), sizeof(numberOfIds));
processedBytes += sizeof(numberOfIds);
if (numberOfIds > 0) {
QSet<EntityItemID> entityItemIDsToDelete;
std::vector<EntityItemID> ids;
ids.reserve(numberOfIds);
// extract ids from packet
for (size_t i = 0; i < numberOfIds; i++) {
if (processedBytes + NUM_BYTES_RFC4122_UUID > packetLength) {
qCDebug(entities) << "EntityTree::processEraseMessageDetails().... bailing because not enough bytes in buffer";
break; // bail to prevent buffer overflow
}
QByteArray encodedID = dataByteArray.mid((int)processedBytes, NUM_BYTES_RFC4122_UUID);
QUuid entityID = QUuid::fromRfc4122(encodedID);
dataAt += encodedID.size();
QUuid id = QUuid::fromRfc4122(encodedID);
processedBytes += encodedID.size();
#ifdef EXTRA_ERASE_DEBUGGING
qCDebug(entities) << " ---- EntityTree::processEraseMessageDetails() contains id:" << entityID;
qCDebug(entities) << " ---- EntityTree::processEraseMessageDetails() contains id:" << id;
#endif
EntityItemID entityItemID(entityID);
if (shouldEraseEntity(entityID, sourceNode)) {
entityItemIDsToDelete << entityItemID;
cleanupCloneIDs(entityItemID);
}
EntityItemID entityID(id);
ids.push_back(entityID);
}
deleteEntities(entityItemIDsToDelete, true, true);
bool force = sourceNode->isAllowedEditor();
bool ignoreWarnings = true;
deleteEntitiesByID(ids, force, ignoreWarnings);
}
return (int)processedBytes;
}

View file

@ -126,7 +126,9 @@ public:
void unhookChildAvatar(const EntityItemID entityID);
void cleanupCloneIDs(const EntityItemID& entityID);
void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true);
void deleteEntities(QSet<EntityItemID> entityIDs, bool force = false, bool ignoreWarnings = true);
void deleteEntitiesByID(const std::vector<EntityItemID>& entityIDs, bool force = false, bool ignoreWarnings = true);
void deleteEntitiesByPointer(const std::vector<EntityItemPointer>& entities);
EntityItemPointer findEntityByID(const QUuid& id) const;
EntityItemPointer findEntityByEntityItemID(const EntityItemID& entityID) const;
@ -291,6 +293,7 @@ signals:
protected:
void recursivelyFilterAndCollectForDelete(const EntityItemPointer& entity, std::vector<EntityItemPointer>& entitiesToDelete, bool force) const;
void processRemovedEntities(const DeleteEntityOperator& theOperator);
bool updateEntity(EntityItemPointer entity, const EntityItemProperties& properties,
const SharedNodePointer& senderNode = SharedNodePointer(nullptr));
@ -339,12 +342,12 @@ protected:
int _totalEditMessages = 0;
int _totalUpdates = 0;
int _totalCreates = 0;
quint64 _totalDecodeTime = 0;
quint64 _totalLookupTime = 0;
quint64 _totalUpdateTime = 0;
quint64 _totalCreateTime = 0;
quint64 _totalLoggingTime = 0;
quint64 _totalFilterTime = 0;
mutable quint64 _totalDecodeTime = 0;
mutable quint64 _totalLookupTime = 0;
mutable quint64 _totalUpdateTime = 0;
mutable quint64 _totalCreateTime = 0;
mutable quint64 _totalLoggingTime = 0;
mutable quint64 _totalFilterTime = 0;
// these performance statistics are only used in the client
void resetClientEditStats();
@ -364,7 +367,7 @@ protected:
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
bool filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType);
bool filterProperties(const EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) const;
bool _hasEntityEditFilter{ false };
QStringList _entityScriptSourceWhitelist;

View file

@ -197,7 +197,7 @@ EntityItemID EntityTreeElement::evalDetailedRayIntersection(const glm::vec3& ori
// only called if we do intersect our bounding cube, but find if we actually intersect with entities...
EntityItemID entityID;
forEachEntity([&](EntityItemPointer entity) {
if (entity->getIgnorePickIntersection()) {
if (entity->getIgnorePickIntersection() && !searchFilter.bypassIgnore()) {
return;
}
@ -341,7 +341,7 @@ EntityItemID EntityTreeElement::evalDetailedParabolaIntersection(const glm::vec3
// only called if we do intersect our bounding cube, but find if we actually intersect with entities...
EntityItemID entityID;
forEachEntity([&](EntityItemPointer entity) {
if (entity->getIgnorePickIntersection()) {
if (entity->getIgnorePickIntersection() && !searchFilter.bypassIgnore()) {
return;
}
@ -705,7 +705,7 @@ void EntityTreeElement::cleanupDomainAndNonOwnedEntities() {
withWriteLock([&] {
EntityItems savedEntities;
foreach(EntityItemPointer entity, _entityItems) {
if (!(entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) {
if (!(entity->isLocalEntity() || entity->isMyAvatarEntity())) {
entity->preDelete();
entity->_element = NULL;
} else {

View file

@ -47,14 +47,17 @@ void SimpleEntitySimulation::clearOwnership(const QUuid& ownerID) {
}
}
void SimpleEntitySimulation::updateEntitiesInternal(uint64_t now) {
void SimpleEntitySimulation::updateEntities() {
EntitySimulation::updateEntities();
QMutexLocker lock(&_mutex);
uint64_t now = usecTimestampNow();
expireStaleOwnerships(now);
stopOwnerlessEntities(now);
}
void SimpleEntitySimulation::addEntityInternal(EntityItemPointer entity) {
void SimpleEntitySimulation::addEntityToInternalLists(EntityItemPointer entity) {
EntitySimulation::addEntityToInternalLists(entity);
if (entity->getSimulatorID().isNull()) {
QMutexLocker lock(&_mutex);
if (entity->getDynamic()) {
// we don't allow dynamic objects to move without an owner so nothing to do here
} else if (entity->isMovingRelativeToParent()) {
@ -65,7 +68,6 @@ void SimpleEntitySimulation::addEntityInternal(EntityItemPointer entity) {
}
}
} else {
QMutexLocker lock(&_mutex);
_entitiesWithSimulationOwner.insert(entity);
_nextStaleOwnershipExpiry = glm::min(_nextStaleOwnershipExpiry, entity->getSimulationOwnershipExpiry());
@ -79,10 +81,10 @@ void SimpleEntitySimulation::addEntityInternal(EntityItemPointer entity) {
}
}
void SimpleEntitySimulation::removeEntityInternal(EntityItemPointer entity) {
EntitySimulation::removeEntityInternal(entity);
void SimpleEntitySimulation::removeEntityFromInternalLists(EntityItemPointer entity) {
_entitiesWithSimulationOwner.remove(entity);
_entitiesThatNeedSimulationOwner.remove(entity);
EntitySimulation::removeEntityFromInternalLists(entity);
}
void SimpleEntitySimulation::processChangedEntity(const EntityItemPointer& entity) {
@ -135,10 +137,11 @@ void SimpleEntitySimulation::processChangedEntity(const EntityItemPointer& entit
entity->clearDirtyFlags();
}
void SimpleEntitySimulation::clearEntitiesInternal() {
void SimpleEntitySimulation::clearEntities() {
QMutexLocker lock(&_mutex);
_entitiesWithSimulationOwner.clear();
_entitiesThatNeedSimulationOwner.clear();
EntitySimulation::clearEntities();
}
void SimpleEntitySimulation::sortEntitiesThatMoved() {

View file

@ -23,16 +23,16 @@ using SimpleEntitySimulationPointer = std::shared_ptr<SimpleEntitySimulation>;
class SimpleEntitySimulation : public EntitySimulation {
public:
SimpleEntitySimulation() : EntitySimulation() { }
~SimpleEntitySimulation() { clearEntitiesInternal(); }
~SimpleEntitySimulation() { clearEntities(); }
void clearOwnership(const QUuid& ownerID);
void clearEntities() override;
void updateEntities() override;
protected:
void updateEntitiesInternal(uint64_t now) override;
void addEntityInternal(EntityItemPointer entity) override;
void removeEntityInternal(EntityItemPointer entity) override;
void addEntityToInternalLists(EntityItemPointer entity) override;
void removeEntityFromInternalLists(EntityItemPointer entity) override;
void processChangedEntity(const EntityItemPointer& entity) override;
void clearEntitiesInternal() override;
void sortEntitiesThatMoved() override;

View file

@ -258,8 +258,8 @@ scriptable::ScriptableMeshPointer GraphicsScriptingInterface::newMesh(const QVar
return scriptable::make_scriptowned<scriptable::ScriptableMesh>(mesh, nullptr);
}
QString GraphicsScriptingInterface::exportModelToOBJ(const scriptable::ScriptableModel& _in) {
const auto& in = _in.getConstMeshes();
QString GraphicsScriptingInterface::exportModelToOBJ(const scriptable::ScriptableModelPointer& model) {
const auto& in = model->getConstMeshes();
if (in.size()) {
QList<scriptable::MeshPointer> meshes;
foreach (auto meshProxy, in) {

View file

@ -93,7 +93,7 @@ public slots:
* @param {Graphics.Model} model
* @returns {string}
*/
QString exportModelToOBJ(const scriptable::ScriptableModel& in);
QString exportModelToOBJ(const scriptable::ScriptableModelPointer& model);
private:
scriptable::ModelProviderPointer getModelProvider(const QUuid& uuid);

View file

@ -135,6 +135,7 @@ public:
AudioSoloRequest,
BulkAvatarTraitsAck,
StopInjector,
AvatarZonePresence,
NUM_PACKET_TYPE
};
@ -185,7 +186,8 @@ public:
<< PacketTypeEnum::Value::OctreeFileReplacement << PacketTypeEnum::Value::ReplicatedMicrophoneAudioNoEcho
<< PacketTypeEnum::Value::ReplicatedMicrophoneAudioWithEcho << PacketTypeEnum::Value::ReplicatedInjectAudio
<< PacketTypeEnum::Value::ReplicatedSilentAudioFrame << PacketTypeEnum::Value::ReplicatedAvatarIdentity
<< PacketTypeEnum::Value::ReplicatedKillAvatar << PacketTypeEnum::Value::ReplicatedBulkAvatarData;
<< PacketTypeEnum::Value::ReplicatedKillAvatar << PacketTypeEnum::Value::ReplicatedBulkAvatarData
<< PacketTypeEnum::Value::AvatarZonePresence;
return NON_SOURCED_PACKETS;
}

View file

@ -62,7 +62,7 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer
// rather than pass the legit shape pointer to the ObjectMotionState ctor above.
setShape(shape);
if (_entity->isAvatarEntity() && _entity->getOwningAvatarID() != Physics::getSessionUUID()) {
if (_entity->isAvatarEntity() && !_entity->isMyAvatarEntity()) {
// avatar entities are always thus, so we cache this fact in _ownershipState
_ownershipState = EntityMotionState::OwnershipState::Unownable;
}
@ -407,8 +407,8 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) {
// NOTE: we expect _entity and _body to be valid in this context, since shouldSendUpdate() is only called
// after doesNotNeedToSendUpdate() returns false and that call should return 'true' if _entity or _body are NULL.
// this case is prevented by setting _ownershipState to UNOWNABLE in EntityMotionState::ctor
assert(!(_entity->isAvatarEntity() && _entity->getOwningAvatarID() != Physics::getSessionUUID()));
// this case is prevented by setting _ownershipState to OwnershipState::Unownable in EntityMotionState::ctor
assert(!(_entity->isAvatarEntity() && !_entity->isMyAvatarEntity()));
if (_entity->getTransitingWithAvatar()) {
return false;
@ -768,7 +768,7 @@ uint8_t EntityMotionState::computeFinalBidPriority() const {
}
bool EntityMotionState::isLocallyOwned() const {
return _entity->getSimulatorID() == Physics::getSessionUUID();
return _entity->getSimulatorID() == Physics::getSessionUUID() || _entity->isMyAvatarEntity();
}
bool EntityMotionState::isLocallyOwnedOrShouldBe() const {
@ -786,13 +786,21 @@ void EntityMotionState::setRegion(uint8_t region) {
}
void EntityMotionState::initForBid() {
assert(_ownershipState != EntityMotionState::OwnershipState::Unownable);
_ownershipState = EntityMotionState::OwnershipState::PendingBid;
if (_ownershipState != EntityMotionState::OwnershipState::Unownable) {
_ownershipState = EntityMotionState::OwnershipState::PendingBid;
}
}
void EntityMotionState::initForOwned() {
assert(_ownershipState != EntityMotionState::OwnershipState::Unownable);
_ownershipState = EntityMotionState::OwnershipState::LocallyOwned;
if (_ownershipState != EntityMotionState::OwnershipState::Unownable) {
_ownershipState = EntityMotionState::OwnershipState::LocallyOwned;
}
}
void EntityMotionState::clearOwnershipState() {
if (_ownershipState != OwnershipState::Unownable) {
_ownershipState = OwnershipState::NotLocallyOwned;
}
}
void EntityMotionState::clearObjectVelocities() const {

View file

@ -107,7 +107,7 @@ protected:
uint64_t getNextBidExpiry() const { return _nextBidExpiry; }
void initForBid();
void initForOwned();
void clearOwnershipState() { _ownershipState = OwnershipState::NotLocallyOwned; }
void clearOwnershipState();
void updateServerPhysicsVariables();
bool remoteSimulationOutOfSync(uint32_t simulationStep);

View file

@ -40,14 +40,9 @@ void PhysicalEntitySimulation::init(
}
// begin EntitySimulation overrides
void PhysicalEntitySimulation::updateEntitiesInternal(uint64_t now) {
// Do nothing here because the "internal" update the PhysicsEngine::stepSimulation() which is done elsewhere.
}
void PhysicalEntitySimulation::addEntityInternal(EntityItemPointer entity) {
QMutexLocker lock(&_mutex);
assert(entity);
assert(!entity->isDead());
void PhysicalEntitySimulation::addEntityToInternalLists(EntityItemPointer entity) {
EntitySimulation::addEntityToInternalLists(entity);
entity->deserializeActions(); // TODO: do this elsewhere
uint8_t region = _space->getRegion(entity->getSpaceIndex());
bool maybeShouldBePhysical = (region < workload::Region::R3 || region == workload::Region::UNKNOWN) && entity->shouldBePhysical();
bool canBeKinematic = region <= workload::Region::R3;
@ -66,23 +61,20 @@ void PhysicalEntitySimulation::addEntityInternal(EntityItemPointer entity) {
}
}
void PhysicalEntitySimulation::removeEntityInternal(EntityItemPointer entity) {
if (entity->isSimulated()) {
EntitySimulation::removeEntityInternal(entity);
_entitiesToAddToPhysics.remove(entity);
EntityMotionState* motionState = static_cast<EntityMotionState*>(entity->getPhysicsInfo());
if (motionState) {
removeOwnershipData(motionState);
_entitiesToRemoveFromPhysics.insert(entity);
}
if (entity->isDead() && entity->getElement()) {
_deadEntities.insert(entity);
}
void PhysicalEntitySimulation::removeEntityFromInternalLists(EntityItemPointer entity) {
_entitiesToAddToPhysics.remove(entity);
EntityMotionState* motionState = static_cast<EntityMotionState*>(entity->getPhysicsInfo());
if (motionState) {
removeOwnershipData(motionState);
_entitiesToRemoveFromPhysics.insert(entity);
}
if (entity->isDead() && entity->getElement()) {
_deadEntitiesToRemoveFromTree.insert(entity);
}
if (entity->isAvatarEntity()) {
_deadAvatarEntities.insert(entity);
}
EntitySimulation::removeEntityFromInternalLists(entity);
}
void PhysicalEntitySimulation::removeOwnershipData(EntityMotionState* motionState) {
@ -115,18 +107,6 @@ void PhysicalEntitySimulation::clearOwnershipData() {
_bids.clear();
}
void PhysicalEntitySimulation::takeDeadEntities(SetOfEntities& deadEntities) {
QMutexLocker lock(&_mutex);
for (auto entity : _deadEntities) {
EntityMotionState* motionState = static_cast<EntityMotionState*>(entity->getPhysicsInfo());
if (motionState) {
_entitiesToRemoveFromPhysics.insert(entity);
}
}
_deadEntities.swap(deadEntities);
_deadEntities.clear();
}
void PhysicalEntitySimulation::takeDeadAvatarEntities(SetOfEntities& deadEntities) {
_deadAvatarEntities.swap(deadEntities);
_deadAvatarEntities.clear();
@ -190,11 +170,43 @@ void PhysicalEntitySimulation::processChangedEntity(const EntityItemPointer& ent
}
}
void PhysicalEntitySimulation::clearEntitiesInternal() {
void PhysicalEntitySimulation::processDeadEntities() {
// Note: this override is a complete rewite of the base class's method because we cannot assume all entities
// are domain entities, and the consequence of trying to delete a domain-entity in this case is very different.
if (_deadEntitiesToRemoveFromTree.empty()) {
return;
}
PROFILE_RANGE(simulation_physics, "Deletes");
std::vector<EntityItemPointer> entitiesToDeleteImmediately;
entitiesToDeleteImmediately.reserve(_deadEntitiesToRemoveFromTree.size());
QUuid sessionID = Physics::getSessionUUID();
QMutexLocker lock(&_mutex);
for (auto entity : _deadEntitiesToRemoveFromTree) {
EntityMotionState* motionState = static_cast<EntityMotionState*>(entity->getPhysicsInfo());
if (motionState) {
_entitiesToRemoveFromPhysics.insert(entity);
}
if (entity->isDomainEntity()) {
// interface-client can't delete domainEntities outright, they must roundtrip through the entity-server
_entityPacketSender->queueEraseEntityMessage(entity->getID());
} else if (entity->isLocalEntity() || entity->isMyAvatarEntity()) {
entitiesToDeleteImmediately.push_back(entity);
entity->collectChildrenForDelete(entitiesToDeleteImmediately, sessionID);
}
}
_deadEntitiesToRemoveFromTree.clear();
if (!entitiesToDeleteImmediately.empty()) {
getEntityTree()->deleteEntitiesByPointer(entitiesToDeleteImmediately);
}
}
void PhysicalEntitySimulation::clearEntities() {
// TODO: we should probably wait to lock the _physicsEngine so we don't mess up data structures
// while it is in the middle of a simulation step. As it is, we're probably in shutdown mode
// anyway, so maybe the simulation was already properly shutdown? Cross our fingers...
QMutexLocker lock(&_mutex);
// remove the objects (aka MotionStates) from physics
_physicsEngine->removeSetOfObjects(_physicalObjects);
@ -216,11 +228,20 @@ void PhysicalEntitySimulation::clearEntitiesInternal() {
_entitiesToAddToPhysics.clear();
_incomingChanges.clear();
_entitiesToDeleteLater.clear();
EntitySimulation::clearEntities();
}
void PhysicalEntitySimulation::queueEraseDomainEntity(const QUuid& id) const {
if (_entityPacketSender) {
_entityPacketSender->queueEraseEntityMessage(id);
}
}
// virtual
void PhysicalEntitySimulation::prepareEntityForDelete(EntityItemPointer entity) {
// this can be called on any thread
// DANGER! this can be called on any thread
// do no dirty deeds here --> assemble list for later
assert(entity);
assert(entity->isDead());
QMutexLocker lock(&_mutex);
@ -228,11 +249,11 @@ void PhysicalEntitySimulation::prepareEntityForDelete(EntityItemPointer entity)
}
void PhysicalEntitySimulation::removeDeadEntities() {
// only ever call this on the main thread
// DANGER! only ever call this on the main thread
QMutexLocker lock(&_mutex);
for (auto& entity : _entitiesToDeleteLater) {
entity->clearActions(getThisPointer());
removeEntityInternal(entity);
EntitySimulation::prepareEntityForDelete(entity);
}
_entitiesToDeleteLater.clear();
}
@ -647,10 +668,16 @@ void PhysicalEntitySimulation::addDynamic(EntityDynamicPointer dynamic) {
"dynamic that was already in _physicsEngine";
}
}
EntitySimulation::addDynamic(dynamic);
QMutexLocker lock(&_dynamicsMutex);
_dynamicsToAdd += dynamic;
}
}
void PhysicalEntitySimulation::removeDynamic(const QUuid dynamicID) {
QMutexLocker lock(&_dynamicsMutex);
_dynamicsToRemove += dynamicID;
}
void PhysicalEntitySimulation::applyDynamicChanges() {
QList<EntityDynamicPointer> dynamicsFailedToAdd;
if (_physicsEngine) {
@ -665,8 +692,8 @@ void PhysicalEntitySimulation::applyDynamicChanges() {
}
}
}
// applyDynamicChanges will clear _dynamicsToRemove and _dynamicsToAdd
EntitySimulation::applyDynamicChanges();
_dynamicsToAdd.clear();
_dynamicsToRemove.clear();
}
// put back the ones that couldn't yet be added

View file

@ -19,6 +19,7 @@
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/CollisionDispatch/btGhostObject.h>
#include <EntityDynamicInterface.h>
#include <EntityItem.h>
#include <EntitySimulation.h>
#include <workload/Space.h>
@ -58,22 +59,24 @@ public:
void init(EntityTreePointer tree, PhysicsEnginePointer engine, EntityEditPacketSender* packetSender);
void setWorkloadSpace(const workload::SpacePointer space) { _space = space; }
virtual void addDynamic(EntityDynamicPointer dynamic) override;
virtual void applyDynamicChanges() override;
void addDynamic(EntityDynamicPointer dynamic) override;
void removeDynamic(const QUuid dynamicID) override;
void applyDynamicChanges() override;
virtual void takeDeadEntities(SetOfEntities& deadEntities) override;
void takeDeadAvatarEntities(SetOfEntities& deadEntities);
virtual void clearEntities() override;
void queueEraseDomainEntity(const QUuid& id) const override;
signals:
void entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision);
protected: // only called by EntitySimulation
// overrides for EntitySimulation
virtual void updateEntitiesInternal(uint64_t now) override;
virtual void addEntityInternal(EntityItemPointer entity) override;
virtual void removeEntityInternal(EntityItemPointer entity) override;
void addEntityToInternalLists(EntityItemPointer entity) override;
void removeEntityFromInternalLists(EntityItemPointer entity) override;
void processChangedEntity(const EntityItemPointer& entity) override;
virtual void clearEntitiesInternal() override;
void processDeadEntities() override;
void removeOwnershipData(EntityMotionState* motionState);
void clearOwnershipData();
@ -121,8 +124,13 @@ private:
VectorOfEntityMotionStates _owned;
VectorOfEntityMotionStates _bids;
SetOfEntities _deadAvatarEntities;
SetOfEntities _deadAvatarEntities; // to remove from Avatar's lists
std::vector<EntityItemPointer> _entitiesToDeleteLater;
QList<EntityDynamicPointer> _dynamicsToAdd;
QSet<QUuid> _dynamicsToRemove;
QMutex _dynamicsMutex { QMutex::Recursive };
workload::SpacePointer _space;
uint64_t _nextBidExpiry;
uint32_t _lastStepSendPackets { 0 };

View file

@ -192,7 +192,9 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c
ShapeKey::Builder(), ShapeKey::Builder().withFade(),
ShapeKey::Builder().withDeformed(), ShapeKey::Builder().withDeformed().withFade(),
ShapeKey::Builder().withDeformed().withDualQuatSkinned(), ShapeKey::Builder().withDeformed().withDualQuatSkinned().withFade(),
ShapeKey::Builder().withOwnPipeline(), ShapeKey::Builder().withOwnPipeline().withFade()
ShapeKey::Builder().withOwnPipeline(), ShapeKey::Builder().withOwnPipeline().withFade(),
ShapeKey::Builder().withDeformed().withOwnPipeline(), ShapeKey::Builder().withDeformed().withOwnPipeline().withFade(),
ShapeKey::Builder().withDeformed().withDualQuatSkinned().withOwnPipeline(), ShapeKey::Builder().withDeformed().withDualQuatSkinned().withOwnPipeline().withFade(),
};
std::vector<std::vector<ShapeKey>> sortedShapeKeys(keys.size());

View file

@ -10,10 +10,12 @@
//
#include "PhysicsHelpers.h"
#include "NumericalConstants.h"
#include <QUuid>
#include "NumericalConstants.h"
#include "PhysicsCollisionGroups.h"
#include "SharedUtil.h"
// This chunk of code was copied from Bullet-2.82, so we include the Bullet license here:
/*
@ -91,7 +93,7 @@ int32_t Physics::getDefaultCollisionMask(int32_t group) {
QUuid _sessionID;
void Physics::setSessionUUID(const QUuid& sessionID) {
_sessionID = sessionID;
_sessionID = sessionID.isNull() ? AVATAR_SELF_ID : sessionID;
}
const QUuid& Physics::getSessionUUID() {

View file

@ -10,6 +10,7 @@
#define hifi_PickFilter_h
#include <bitset>
#include <iostream> // adebug
class PickFilter {
public:
@ -60,6 +61,8 @@ public:
// NOT YET IMPLEMENTED
PICK_ALL_INTERSECTIONS, // if not set, returns closest intersection, otherwise, returns list of all intersections
PICK_BYPASS_IGNORE, // for debug purposes
NUM_FLAGS, // Not a valid flag
};
typedef std::bitset<NUM_FLAGS> Flags;
@ -93,6 +96,8 @@ public:
bool doesWantAllIntersections() const { return _flags[PICK_ALL_INTERSECTIONS]; }
bool bypassIgnore() const { return _flags[PICK_BYPASS_IGNORE]; }
// Helpers for RayPickManager
Flags getEntityFlags() const {
unsigned int toReturn = 0;

View file

@ -21,7 +21,7 @@ using SpatiallyNestableWeakPointer = std::weak_ptr<SpatiallyNestable>;
using SpatiallyNestablePointer = std::shared_ptr<SpatiallyNestable>;
class SpatialParentTree {
public:
virtual SpatiallyNestablePointer findByID(const QUuid& id) const { return nullptr; }
virtual SpatiallyNestablePointer findByID(const QUuid& id) const = 0;
};
class SpatialParentFinder : public Dependency {

View file

@ -11,6 +11,8 @@
#include "FileTypeProfile.h"
#include <set>
#include <mutex>
#include <QtQml/QQmlContext>
#include "RequestFilters.h"
@ -18,14 +20,28 @@
#if !defined(Q_OS_ANDROID)
static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine";
static std::set<FileTypeProfile*> FileTypeProfile_instances;
static std::mutex FileTypeProfile_mutex;
FileTypeProfile::FileTypeProfile(QQmlContext* parent) :
ContextAwareProfile(parent)
{
static const QString WEB_ENGINE_USER_AGENT = "Chrome/48.0 (HighFidelityInterface)";
setHttpUserAgent(WEB_ENGINE_USER_AGENT);
setStorageName(QML_WEB_ENGINE_STORAGE_NAME);
setOffTheRecord(false);
auto requestInterceptor = new RequestInterceptor(this);
setRequestInterceptor(requestInterceptor);
std::lock_guard<std::mutex> lock(FileTypeProfile_mutex);
FileTypeProfile_instances.insert(this);
}
FileTypeProfile::~FileTypeProfile() {
std::lock_guard<std::mutex> lock(FileTypeProfile_mutex);
FileTypeProfile_instances.erase(this);
}
void FileTypeProfile::RequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) {
@ -37,5 +53,11 @@ void FileTypeProfile::registerWithContext(QQmlContext* context) {
context->setContextProperty("FileTypeProfile", new FileTypeProfile(context));
}
void FileTypeProfile::clearCache() {
std::lock_guard<std::mutex> lock(FileTypeProfile_mutex);
foreach (auto instance, FileTypeProfile_instances) {
instance->clearHttpCache();
}
}
#endif

View file

@ -25,8 +25,12 @@ class FileTypeProfile : public ContextAwareProfile {
public:
static void registerWithContext(QQmlContext* parent);
static void clearCache();
protected:
FileTypeProfile(QQmlContext* parent);
virtual ~FileTypeProfile();
class RequestInterceptor : public Parent::RequestInterceptor {
public:
RequestInterceptor(ContextAwareProfile* parent) : Parent::RequestInterceptor(parent) {}

View file

@ -11,6 +11,8 @@
#include "HFWebEngineProfile.h"
#include <set>
#include <mutex>
#include <QtQml/QQmlContext>
#include "RequestFilters.h"
@ -19,12 +21,24 @@
static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine";
static std::set<HFWebEngineProfile*> HFWebEngineProfile_instances;
static std::mutex HFWebEngineProfile_mutex;
HFWebEngineProfile::HFWebEngineProfile(QQmlContext* parent) : Parent(parent)
{
setStorageName(QML_WEB_ENGINE_STORAGE_NAME);
setOffTheRecord(false);
// we use the HFWebEngineRequestInterceptor to make sure that web requests are authenticated for the interface user
setRequestInterceptor(new RequestInterceptor(this));
std::lock_guard<std::mutex> lock(HFWebEngineProfile_mutex);
HFWebEngineProfile_instances.insert(this);
}
HFWebEngineProfile::~HFWebEngineProfile() {
std::lock_guard<std::mutex> lock(HFWebEngineProfile_mutex);
HFWebEngineProfile_instances.erase(this);
}
void HFWebEngineProfile::RequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) {
@ -35,4 +49,11 @@ void HFWebEngineProfile::registerWithContext(QQmlContext* context) {
context->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(context));
}
void HFWebEngineProfile::clearCache() {
std::lock_guard<std::mutex> lock(HFWebEngineProfile_mutex);
foreach (auto instance, HFWebEngineProfile_instances) {
instance->clearHttpCache();
}
}
#endif

View file

@ -23,8 +23,12 @@ class HFWebEngineProfile : public ContextAwareProfile {
public:
static void registerWithContext(QQmlContext* parent);
static void clearCache();
protected:
HFWebEngineProfile(QQmlContext* parent);
virtual ~HFWebEngineProfile();
class RequestInterceptor : public Parent::RequestInterceptor {
public:
RequestInterceptor(ContextAwareProfile* parent) : Parent::RequestInterceptor(parent) {}

View file

@ -127,6 +127,18 @@ uint32_t Space::copyProxyValues(Proxy* proxies, uint32_t numDestProxies) const {
return numCopied;
}
uint32_t Space::copySelectedProxyValues(Proxy::Vector& proxies, const workload::indexed_container::Indices& indices) const {
std::unique_lock<std::mutex> lock(_proxiesMutex);
uint32_t numCopied = 0;
for (auto index : indices) {
if (isAllocatedID(index) && (index < (Index)_proxies.size())) {
proxies.push_back(_proxies[index]);
++numCopied;
}
}
return numCopied;
}
const Owner Space::getOwner(int32_t proxyID) const {
std::unique_lock<std::mutex> lock(_proxiesMutex);
if (isAllocatedID(proxyID) && (proxyID < (Index)_proxies.size())) {

View file

@ -47,6 +47,7 @@ public:
void categorizeAndGetChanges(std::vector<Change>& changes);
uint32_t copyProxyValues(Proxy* proxies, uint32_t numDestProxies) const;
uint32_t copySelectedProxyValues(Proxy::Vector& proxies, const workload::indexed_container::Indices& indices) const;
const Owner getOwner(int32_t proxyID) const;
uint8_t getRegion(int32_t proxyID) const;

4
screenshare/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
hifi-screenshare-*/
hifi-screenshare*.zip
screenshare*.zip
screenshare-*/

View file

@ -0,0 +1,38 @@
set(TARGET_NAME screenshare)
add_custom_target(${TARGET_NAME}-npm-install
COMMAND npm install
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_target(${TARGET_NAME}
COMMAND npm run packager -- --out ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${TARGET_NAME}-npm-install
)
set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "screenshare")
set_target_properties(${TARGET_NAME}-npm-install PROPERTIES FOLDER "hidden/screenshare")
if (WIN32)
set(PACKAGED_SCREENSHARE_FOLDER "hifi-screenshare-win32-x64")
set(SCREENSHARE_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_SCREENSHARE_FOLDER}")
install(
DIRECTORY "${SCREENSHARE_DESTINATION}/"
DESTINATION ${SCREENSHARE_INSTALL_DIR}
)
set(EXECUTABLE_PATH "${SCREENSHARE_DESTINATION}/${SCREENSHARE_EXEC_NAME}")
optional_win_executable_signing()
elseif (APPLE)
set(PACKAGED_SCREENSHARE_FOLDER "hifi-screenshare-darwin-x64/${SCREENSHARE_EXEC_NAME}")
install(
DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_SCREENSHARE_FOLDER}"
USE_SOURCE_PERMISSIONS
DESTINATION ${SCREENSHARE_INSTALL_DIR}
)
endif()
# DO build the Screenshare Electron app when building the `ALL_BUILD` target.
# DO build the Screenshare Electron app when a user selects "Build Solution" from within Visual Studio.
set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL FALSE EXCLUDE_FROM_DEFAULT_BUILD FALSE)
set_target_properties(${TARGET_NAME}-npm-install PROPERTIES EXCLUDE_FROM_ALL FALSE EXCLUDE_FROM_DEFAULT_BUILD FALSE)

16
screenshare/README.md Normal file
View file

@ -0,0 +1,16 @@
# Screen Sharing within High Fidelity
This Screen Share app, built using Electron, allows for easy desktop screen sharing when used in conjuction with various scripts in the `hifi-content` repository.
# Screen Sharing Source Files
## `packager.js`
Calling npm run packager will use this file to create the actual Electron `hifi-screenshare` executable.
It will kick out a folder `hifi-screenshare-<platform>` which contains an executable.
## `src/screenshareApp.js`
The main process file to configure the electron app.
## `src/screenshareMainProcess.js`
The render file to display the app's UI.
## `screenshareApp.html`
The HTML that displays the screen selection UI and the confirmation screen UI.

2289
screenshare/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
screenshare/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "highfidelity_screenshare",
"version": "1.0.0",
"description": "High Fidelity Screenshare",
"main": "src/screenshareMainProcess.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"packager": "node packager.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/highfidelity/hifi.git"
},
"author": "High Fidelity",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/highfidelity/hifi/issues"
},
"homepage": "https://github.com/highfidelity/hifi#readme",
"devDependencies": {
"electron": "^6.0.12",
"electron-packager": "^14.0.6"
},
"dependencies": {
"yargs": "^14.2.0"
}
}

50
screenshare/packager.js Normal file
View file

@ -0,0 +1,50 @@
var packager = require('electron-packager');
var osType = require('os').type();
var argv = require('yargs').argv;
var platform = null;
if (osType == "Darwin" || osType == "Linux") {
platform = osType.toLowerCase();
} else if (osType == "Windows_NT") {
platform = "win32"
}
var NAME = "hifi-screenshare";
var options = {
dir: __dirname,
name: NAME,
version: "0.1.0",
overwrite: true,
prune: true,
arch: "x64",
platform: platform,
ignore: "electron-packager|README.md|CMakeLists.txt|packager.js|.gitignore"
};
// setup per OS options
if (osType == "Darwin") {
options["app-bundle-id"] = "com.highfidelity.hifi-screenshare";
} else if (osType == "Windows_NT") {
options["version-string"] = {
CompanyName: "High Fidelity, Inc.",
FileDescription: "High Fidelity Screenshare",
ProductName: NAME,
OriginalFilename: NAME + ".exe"
}
}
// check if we were passed a custom out directory, pass it along if so
if (argv.out) {
options.out = argv.out
}
// call the packager to produce the executable
packager(options)
.then(appPath => {
console.log("Wrote new app to " + appPath);
})
.catch(error => {
console.error("There was an error writing the packaged console: " + error.message);
process.exit(1);
});

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

View file

@ -0,0 +1,52 @@
<!--
screenshareApp.html
Created by Milad Nazeri, Rebecca Stankus, and Zach Fox 2019/11/13
Copyright 2019 High Fidelity, Inc.
Distributed under the Apache License, Version 2.0.
See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-->
<html>
<head>
<link href="styles.css" rel="stylesheet">
</head>
<body id="main">
<div id="title" class="text_title">
<h1>Share your screen</h1>
<h3 id="subtitle">Please select the content you'd like to share.</h3>
</div>
<button id="screenshare" onclick="stopSharing()" style="display: none;">Stop Screenshare</button>
<div id="select_screen">
<div class="scrollbar" id="style-1">
<div class="force-overflow">
<div id="selects">
</div>
</div>
</div>
</div>
<div id="confirmation_screen" style="display: none;">
<div id="share_pick">
</div> <!-- share_pick -->
<div id="confirmation_text" style="color: white;">
<p>
Are you sure you'd like to share <span id="content_name">Content Name</span>?
</p>
<p>
Others will be able to see everything contained within this view.
</p>
</div>
<div id="button_selection">
<div id="yes" class="button_confirmation grey_background" onclick="screenConfirmed(true)">
YES, SHARE THIS CONTENT
</div>
<div id="no" class="button_confirmation" onclick="screenConfirmed(false)">
No, don't share this content
</div>
</div>
</div> <!-- confirmation screen -->
<script src="screenshareApp.js"></script>
<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,299 @@
'use strict';
// screenshareApp.js
//
// Created by Milad Nazeri, Rebecca Stankus, and Zach Fox 2019/11/13
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// Helpers
function handleError(error) {
if (error) {
console.error(error);
}
}
// When an application is picked, make sure we clear out the previous pick, toggle the page,
// and add the correct source
let currentScreensharePickID = "";
function screensharePicked(id) {
currentScreensharePickID = id;
document.getElementById("share_pick").innerHTML = "";
togglePage();
addSource(sourceMap[id], "share_pick");
}
// Once we have confirmed that we want to share, prepare the tokbox publishing initiating
// and toggle back to the selects page
function screenConfirmed(isConfirmed) {
document.getElementById("selects").innerHTML = "";
if (isConfirmed === true){
onAccessApproved(currentScreensharePickID);
}
togglePage();
}
// Hide/show the select page or the confirmation page
let currentPage = "mainPage";
function togglePage(){
if (currentPage === "mainPage") {
currentPage = "confirmationPage";
document.getElementById("select_screen").style.display = "none";
document.getElementById("subtitle").innerHTML = "Confirm that you'd like to share this content.";
document.getElementById("confirmation_screen").style.display = "block";
} else {
showSources();
currentPage = "mainPage";
document.getElementById("select_screen").style.display = "block";
document.getElementById("subtitle").innerHTML = "Please select the content you'd like to share.";
document.getElementById("confirmation_screen").style.display = "none";
}
}
// UI
// Render the html properly and append that to the correct parent
function addSource(source, type) {
let renderedHTML = renderSourceHTML(source);
if (type === "selects") {
document.getElementById("selects").appendChild(renderedHTML);
} else {
document.getElementById("share_pick").appendChild(renderedHTML);
document.getElementById("content_name").innerHTML = source.name;
}
}
// Get the html created from the source. Alter slightly depending on whether this source
// is on the selects screen, or the confirmation screen. Mainly removing highlighting.
// If there is an app Icon, then add it.
function renderSourceHTML(source) {
let type = currentPage === "confirmationPage" ? "share_pick" : "selects";
let sourceBody = document.createElement('div')
let thumbnail = source.thumbnail.toDataURL();
sourceBody.classList.add("box")
if (type === "share_pick") {
sourceBody.style.marginLeft = "0px";
}
let image = "";
if (source.appIcon) {
image = `<img class="icon" src="${source.appIcon.toDataURL()}" />`;
}
sourceBody.innerHTML = `
<div class="heading">
${image}
<span class="screen_label">${source.name}</span>
</div>
<div class="${type === "share_pick" ? "image_no_hover" : "image"}" onclick="screensharePicked('${source.id}')">
<img src="${thumbnail}" />
</div>
`
return sourceBody;
}
// Separate out the screenshares and applications
// Make sure the screens are labeled in order
// Concact the two arrays back together and return
function sortSources() {
let screenSources = [];
let applicationSources = [];
// Difference with Mac selects:
// 1 screen = "Enitre Screen", more than one like PC "Screen 1, Screen 2..."
screenshareSourceArray.forEach((source) => {
if (source.name.match(/(entire )?screen( )?([0-9]?)/i)) {
screenSources.push(source);
} else {
applicationSources.push(source)
}
});
screenSources.sort((a, b) => {
let aNumber = a.name.replace(/[^\d]/, "");
let bNumber = b.name.replace(/[^\d]/, "");
return aNumber - bNumber;
});
let finalSources = [...screenSources, ...applicationSources];
return finalSources;
}
// Setup sorting the selection array, add individual sources, and update the sourceMap
function addSources() {
screenshareSourceArray = sortSources();
for (let i = 0; i < screenshareSourceArray.length; i++) {
addSource(screenshareSourceArray[i], "selects");
sourceMap[screenshareSourceArray[i].id] = screenshareSourceArray[i];
}
}
// 1. Get the screens and window that are available from electron
// 2. Remove the screenshare app itself
// 3. Create a source map to help grab the correct source when picked
// 4. push all the sources for sorting to the source array
// 5. Add thse sources
const electron = require('electron');
const SCREENSHARE_TITLE = "Screen share";
const SCREENSHARE_TITLE_REGEX = new RegExp("^" + SCREENSHARE_TITLE + "$");
const IMAGE_WIDTH = 265;
const IMAGE_HEIGHT = 165;
let screenshareSourceArray = [];
let sourceMap = {};
function showSources() {
screenshareSourceArray = [];
electron.desktopCapturer.getSources({
types:['window', 'screen'],
thumbnailSize: {
width: IMAGE_WIDTH,
height: IMAGE_HEIGHT
},
fetchWindowIcons: true
}, (error, sources) => {
if (error) {
console.log("Error getting sources", error);
}
for (let source of sources) {
if (source.name.match(SCREENSHARE_TITLE_REGEX)){
continue;
}
sourceMap[source.id] = source;
screenshareSourceArray.push(source);
}
addSources();
});
}
// Stop the localstream and end the tokrok publishing
let localStream;
let desktopSharing;
function stopSharing() {
desktopSharing = false;
if (localStream) {
localStream.getTracks()[0].stop();
localStream = null;
}
document.getElementById('screenshare').style.display = "none";
stopTokBoxPublisher();
}
// Callback to start publishing after we have setup the chromium stream
function gotStream(stream) {
localStream = stream;
startTokboxPublisher(localStream);
stream.onended = () => {
if (desktopSharing) {
togglePage();
}
};
}
// After we grant access to electron, create a stream and using the callback
// start the tokbox publisher
function onAccessApproved(desktop_id) {
if (!desktop_id) {
console.log('Desktop Capture access rejected.');
return;
}
document.getElementById('screenshare').style.visibility = "block";
desktopSharing = true;
navigator.webkitGetUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: desktop_id,
minWidth: 1280,
maxWidth: 1280,
minHeight: 720,
maxHeight: 720
}
}
}, gotStream, handleError);
}
// Tokbox
// Once we have the connection info, this will create the session which will allow
// us to publish a stream when we are ready
function initializeTokboxSession() {
session = OT.initSession(projectAPIKey, sessionID);
session.on('sessionDisconnected', (event) => {
console.log('You were disconnected from the session.', event.reason);
});
// Connect to the session
session.connect(token, (error) => {
if (error) {
handleError(error);
}
});
}
// Init the tokbox publisher with our newly created stream
var publisher;
function startTokboxPublisher(stream) {
publisher = document.createElement("div");
var publisherOptions = {
videoSource: stream.getVideoTracks()[0],
audioSource: null,
insertMode: 'append',
width: 1280,
height: 720
};
publisher = OT.initPublisher(publisher, publisherOptions, function(error){
if (error) {
console.log("ERROR: " + error);
} else {
session.publish(publisher, function(error) {
if (error) {
console.log("ERROR FROM Session.publish: " + error);
return;
}
})
}
});
}
// Kills the streaming being sent to tokbox
function stopTokBoxPublisher() {
publisher.destroy();
}
// When the app is ready, we get this info from the command line arguments.
const ipcRenderer = electron.ipcRenderer;
let projectAPIKey;
let sessionID;
let token;
let session;
ipcRenderer.on('connectionInfo', function(event, message) {
const connectionInfo = JSON.parse(message);
projectAPIKey = connectionInfo.projectAPIKey;
sessionID = connectionInfo.sessionID;
token = connectionInfo.token;
initializeTokboxSession();
});
// Show the initial sources after the dom has loaded
// Sources come from electron.desktopCapturer
document.addEventListener("DOMContentLoaded", () => {
showSources();
});

View file

@ -0,0 +1,74 @@
'use strict';
// screenshareMainProcess.js
//
// Milad Nazeri and Zach Fox 2019/11/13
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
const {app, BrowserWindow, ipcMain} = require('electron');
const gotTheLock = app.requestSingleInstanceLock()
const argv = require('yargs').argv;
const connectionInfo = {
token: argv.token || "token",
projectAPIKey: argv.projectAPIKey || "projectAPIKey",
sessionID: argv.sessionID || "sessionID"
}
// Mac and PC need slightly different width and height sizes.
const osType = require('os').type();
let width;
let height;
if (osType == "Darwin" || osType == "Linux") {
width = 960;
height = 660;
} else if (osType == "Windows_NT") {
width = 973;
height = 740;
}
if (!gotTheLock) {
console.log("Another instance of the screenshare is already running - this instance will quit.");
app.exit(0);
return;
}
let window;
const zoomFactor = 1.0;
function createWindow(){
window = new BrowserWindow({
backgroundColor: "#000000",
width: width,
height: height,
center: true,
frame: true,
useContentSize: true,
zoomFactor: zoomFactor,
resizable: false,
webPreferences: {
nodeIntegration: true
},
icon: __dirname + `/resources/interface.png`,
skipTaskbar: false,
title: "Screen share"
});
window.loadURL('file://' + __dirname + '/screenshareApp.html');
window.setMenu(null);
window.webContents.on("did-finish-load", () => {
window.webContents.send('connectionInfo', JSON.stringify(connectionInfo));
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function() {
createWindow();
window.webContents.send('connectionInfo', JSON.stringify(connectionInfo))
});

217
screenshare/src/styles.css Normal file
View file

@ -0,0 +1,217 @@
body {
background-color: black;
box-sizing: border-box;
font-family: "Graphik";
margin: 0px 22px 10px 22px;
}
#confirmation_screen {
width: 100%;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
}
#confirmation_text {
margin-top: 65px;
font-size: 25px;
line-height: 25px;
}
#confirmation_text p {
margin: 0px;
}
#button_selection {
margin-top: 25px;
width: 100%;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
flex-direction: column;
}
.button_confirmation {
margin: 4px;
cursor: pointer;
width: 300px;
height: 32px;
line-height: 32px;
text-align: center;
vertical-align: middle;
color: white
}
.grey_background {
background-color: #191919;
}
#no {
color: rgba(1, 152, 203,0.7);
}
#no:hover {
color: rgba(1, 152, 203,1);
}
#yes {
outline: solid white 2px;
}
#yes:hover {
background: #0198CB;
}
yes:hover + #yes_background {
display: block;
}
#share_pick {
margin-top: 60px;
}
.text_title {
color: white;
}
@font-face {
font-family: "Graphik";
src: url("./resources/Graphik-Regular.ttf");
}
#title {
margin-top: 21px;
}
h1 {
line-height: 48px;
font-size: 48px;
margin: 0px;
}
h3 {
line-height: 24px;
font-size: 24px;
margin: 9px 0px 0px 0px;
}
#publisher {
visibility: hidden;
width: 0px;
height: 0px;
bottom: 10px;
left: 10px;
z-index: 100;
border: 3px solid white;
border-radius: 3px;
}
#selects {
margin-right: 19px;
padding-left: 3px;
}
.screen_label {
max-width: 220px;
font-size: 25px;
line-height: 25px;
color: white;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.icon {
display: inline-block;
background: #000000;
width: 20px;
height: 20px;
margin-right: 15px;
}
.box {
height: 165px;
width: 265px;
display: inline-block;
margin-left: 35px;
margin-top: 60px;
cursor: pointer;
}
.box:nth-child(1) {
margin-top: 0 !important;
}
.box:nth-child(2) {
margin-top: 0 !important;
}
.box:nth-child(3) {
margin-top: 0 !important;
}
.box:nth-child(3n) {
margin-right: 0 !important;
}
.box:nth-child(3n+1) {
margin-left: 0 !important;
}
.heading {
height: 35px;
display: flex;
align-items: center;
}
.image {
width: 265px;
height: 165px;
max-height: 165px;
max-width: 265px;
margin: 0px;
}
.image:hover {
outline: solid white 3px;
}
.image_no_hover {
width: 265px;
height: 165px;
max-height: 165px;
max-width: 265px;
}
img {
width: 265px;
height: 165px;
margin: 0px;
}
.scrollbar {
float: right;
height: 500px;
width: 100%;
overflow-y: scroll;
margin-top: 40px;
}
#style-1::-webkit-scrollbar {
width: 9px;
overflow: scroll;
overflow-x: hidden;
}
#style-1::-webkit-scrollbar-thumb {
background-color: #0198CB;
width: 9px;
}
#style-1::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: #848484;
width: 9px;
}

View file

@ -0,0 +1,124 @@
//
// debugWorkloadWithMouseHover.js - render workload proxy for entity under mouse hover
//
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
"use strict";
(function() {
Script.scriptEnding.connect(function () {
});
// Create a Laser pointer used to pick and add entity to selection
var END_DIMENSIONS = { x: 0.05, y: 0.05, z: 0.05 };
var COLOR1 = {red: 255, green: 0, blue: 255}; // magenta
var COLOR2 = {red: 255, green: 255, blue: 0}; // yellow
var end1 = {
type: "sphere",
dimensions: END_DIMENSIONS,
color: COLOR1,
ignorePickIntersection: true
}
var end2 = {
type: "sphere",
dimensions: END_DIMENSIONS,
color: COLOR2,
ignorePickIntersection: true
}
var laser = Pointers.createPointer(PickType.Ray, {
joint: "Mouse",
filter: Picks.PICK_ENTITIES | Picks.PICK_BYPASS_IGNORE | Picks.PICK_INCLUDE_COLLIDABLE | Picks.PICK_INCLUDE_NONCOLLIDABLE,
renderStates: [{name: "one", end: end1}],
defaultRenderStates: [{name: "one", end: end2, distance: 2.0}],
enabled: true
});
Pointers.setRenderState(laser, "one");
var hoveredObject = undefined;
var SelectionListName = "DebugWorkloadSelection"; // sekret undocumented selection list (hard coded in C++)
var selectionStyle = {
isOutlineSmooth: true,
outlineWidth: 5,
outlineUnoccludedColor: {red: 255, green: 128, blue: 128},
outlineUnoccludedAlpha: 0.88,
outlineOccludedColor: {red: 255, green: 128, blue: 128},
outlineOccludedAlpha:0.5,
fillUnoccludedColor: {red: 26, green: 0, blue: 0},
fillUnoccludedAlpha: 0.0,
fillOccludedColor: {red: 26, green: 0, blue: 0},
fillOccludedAlpha: 0.0
}
Selection.enableListHighlight(SelectionListName, selectionStyle)
var isSelectionEnabled = false
function setSelectionEnabled(enabled) {
if (isSelectionEnabled != enabled) {
isSelectionEnabled = enabled;
//print("isSelectionEnabled set to " + isSelectionEnabled.toString())
if (isSelectionEnabled) {
Pointers.enablePointer(laser)
} else {
Pointers.disablePointer(laser)
Selection.clearSelectedItemsList(SelectionListName)
}
}
}
setSelectionEnabled(true);
function getIntersectionTypeString(type) {
if (type === Picks.INTERSECTED_ENTITY) {
return "entity";
} else if (type === Picks.INTERSECTED_OVERLAY) {
return "overlay";
} else if (type === Picks.INTERSECTED_AVATAR) {
return "avatar";
}
}
function update() {
var result = Pointers.getPrevPickResult(laser);
if (result.intersects) {
if (hoveredObject !== undefined && result.objectID !== hoveredObject.objectID) {
// Hovering on something different
if (isSelectionEnabled) {
Selection.removeFromSelectedItemsList(SelectionListName, getIntersectionTypeString(hoveredObject.type), hoveredObject.objectID)
//print("remove helloDebugHighlight " + hoveredObject.objectID.toString());
}
}
if (isSelectionEnabled) {
if (hoveredObject === undefined || result.objectID !== hoveredObject.objectID) {
// Hovering over something new
Selection.addToSelectedItemsList(SelectionListName, getIntersectionTypeString(result.type), result.objectID);
hoveredObject = result;
//print("add helloDebugHighlight " + hoveredObject.objectID.toString() + " type = '" + getIntersectionTypeString(result.type) + "'");
}
}
} else if (hoveredObject !== undefined) {
// Stopped hovering
if (isSelectionEnabled) {
Selection.removeFromSelectedItemsList(SelectionListName, getIntersectionTypeString(hoveredObject.type), hoveredObject.objectID)
hoveredObject = undefined;
//print("clear helloDebugHighlight");
}
}
}
Script.update.connect(update);
function cleanup() {
Pointers.removePointer(laser);
Selection.disableListHighlight(SelectionListName)
Selection.removeListFromMap(SelectionListName)
}
Script.scriptEnding.connect(cleanup);
}());

View file

@ -0,0 +1,428 @@
//
// Created by Luis Cuenca on 11/14/19
// Copyright 2019 High Fidelity, Inc.
//
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
(function() {
var ZoomStatus = {
"zoomingIn" : 0,
"zoomingOut" : 1,
"zoomedIn" : 2,
"zoomedOut" : 4,
"consumed" : 5
}
var FocusType = {
"avatar" : 0,
"entity" : 1
}
var EasingFunctions = {
easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
// accelerating from zero velocity
easeInCubic: function (t) { return t*t*t },
// decelerating to zero velocity
easeOutCubic: function (t) { return (--t)*t*t+1 },
// acceleration until halfway, then deceleration
easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
// accelerating from zero velocity
easeInQuart: function (t) { return t*t*t*t },
// decelerating to zero velocity
easeOutQuart: function (t) { return 1-(--t)*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
// accelerating from zero velocity
easeInQuint: function (t) { return t*t*t*t*t },
// decelerating to zero velocity
easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
};
var ZoomData = function(type, lookAt, focusNormal, focusDimensions, velocity, maxDuration) {
var self = this;
this.focusType = type;
this.lookAt = lookAt;
this.focusDimensions = focusDimensions;
this.focusNormal = focusNormal;
this.velocity = velocity;
this.maxDuration = maxDuration;
this.initialPos = Camera.getPosition();
this.initialRot = Camera.getOrientation();
this.interpolatedPos = this.initialPos;
this.interpolatedRot = this.initialRot;
this.initialMode = Camera.mode;
this.initialOffset = Vec3.distance(self.initialPos, MyAvatar.getDefaultEyePosition());
this.finalPos = Vec3.ZERO;
this.finalRot = Quat.IDENTITY;
this.direction = Vec3.ZERO;
this.distance = Vec3.ZERO;
this.totalTime = 0.0;
this.elapsedTime = 0.0;
this.maxZoomInAmount = 0.6;
this.maxZoomOutAmount = 0.2;
this.currentZoomAmount = 0.0;
this.zoomPanOffset = {x: 0.5 * Window.innerWidth, y: 0.5 * Window.innerHeight};
this.zoomPanDelta = {x: 0.0, y: 0.0};
this.status = ZoomStatus.zoomedOut;
var OWN_CAMERA_CHANGE_MAX_FRAMES = 30;
this.ownCameraChangeElapseTime = 0.0;
this.ownCameraChange = false;
this.applyZoomPan = function() {
self.zoomPanOffset.x += self.zoomPanDelta.x;
self.zoomPanOffset.y += self.zoomPanDelta.y;
}
this.setZoomPanDelta = function(x, y) {
self.zoomPanDelta.x = x;
self.zoomPanDelta.y = y;
var totalX = self.zoomPanOffset.x + self.zoomPanDelta.x;
var totalY = self.zoomPanOffset.y + self.zoomPanDelta.y;
totalX = Math.min(Math.max(totalX, 0.0), Window.innerWidth);
totalY = Math.min(Math.max(totalY, 0.0), Window.innerHeight);
self.zoomPanDelta.x = totalX - self.zoomPanOffset.x;
self.zoomPanDelta.y = totalY - self.zoomPanOffset.y;
self.updateSuperPan(totalX, totalY);
}
this.getFocusDistance = function(zoomDims) {
var objAspect = zoomDims.x / zoomDims.y;
var camAspect = Camera.frustum.aspectRatio;
var m = 0.0;
if (objAspect < camAspect) {
m = zoomDims.y;
} else {
m = zoomDims.x / camAspect;
}
var DEGREE_TO_RADIAN = 0.0174533;
var fov = DEGREE_TO_RADIAN * Camera.frustum.fieldOfView;
return (0.5 * m) / Math.tan(0.5 * fov);
}
this.finalPos = Vec3.sum(this.lookAt, Vec3.multiply(this.getFocusDistance(this.focusDimensions), this.focusNormal));
this.finalRot = Quat.lookAtSimple(this.finalPos, this.lookAt);
this.computeRouteIn = function() {
var railVector = Vec3.subtract(self.finalPos, self.initialPos);
self.direction = Vec3.normalize(railVector);
self.distance = Vec3.length(railVector);
self.totalTime = self.distance / self.velocity;
self.totalTime = self.totalTime > self.maxDuration ? self.maxDuration : self.totalTime;
}
this.computeRouteOut = function() {
self.finalPos = Camera.getPosition();
var camOffset = Vec3.ZERO;
var myAvatarRotation = MyAvatar.orientation;
if (self.initialMode.indexOf("first") != -1) {
self.initialRot = myAvatarRotation;
} else {
var lookAtPoint = MyAvatar.getDefaultEyePosition();
var lookFromSign = self.initialMode.indexOf("selfie") != -1 ? 1 : -1;
var lookFromPoint = Vec3.sum(lookAtPoint, Vec3.multiply(self.initialOffset * lookFromSign, Quat.getFront(myAvatarRotation)));
self.initialRot = Quat.lookAtSimple(lookFromPoint, lookAtPoint);
self.initialPos = lookFromPoint;
}
self.computeRouteIn();
}
this.initZoomIn = function() {
if (self.status === ZoomStatus.zoomedOut) {
self.computeRouteIn();
self.status = ZoomStatus.zoomingIn;
self.changeCameraMode("independent");
self.elapsedTime = 0.0;
}
}
this.initZoomOut = function() {
if (self.status === ZoomStatus.zoomedIn) {
self.computeRouteOut();
self.status = ZoomStatus.zoomingOut;
self.changeCameraMode("independent");
self.elapsedTime = 0.0;
}
}
this.needsUpdate = function() {
return self.status === ZoomStatus.zoomingIn || self.status === ZoomStatus.zoomingOut;
}
this.updateZoom = function(deltaTime) {
if (self.ownCameraChange) {
self.ownCameraChange = self.ownCameraChangeElapseTime < OWN_CAMERA_CHANGE_MAX_FRAMES * deltaTime;
self.ownCameraChangeElapseTime += deltaTime;
}
if (!self.needsUpdate) {
return;
}
if (self.elapsedTime < self.totalTime) {
var ratio = EasingFunctions.easeInOutQuart(self.elapsedTime / self.totalTime);
if (self.status === ZoomStatus.zoomingIn) {
var curDist = self.distance * ratio;
var addition = Vec3.multiply(curDist, self.direction);
self.interpolatedPos = Vec3.sum(self.initialPos, addition);
self.interpolatedRot = Quat.mix(self.initialRot, self.finalRot, ratio);
} else if (self.status === ZoomStatus.zoomingOut) {
self.interpolatedPos = Vec3.sum(self.finalPos, Vec3.multiply(self.distance * ratio, Vec3.multiply(-1, self.direction)));
self.interpolatedRot = Quat.mix(self.finalRot, self.initialRot, ratio);
}
self.elapsedTime += deltaTime;
Camera.setPosition(self.interpolatedPos);
Camera.setOrientation(self.interpolatedRot);
} else {
Camera.setPosition(self.finalPos);
Camera.setOrientation(self.finalRot);
if (self.status === ZoomStatus.zoomingIn) {
self.status = ZoomStatus.zoomedIn;
} else if (self.status === ZoomStatus.zoomingOut) {
self.status = ZoomStatus.consumed;
self.changeCameraMode(self.initialMode);
}
}
}
this.resetZoomAspect = function() {
self.computeRouteIn();
Camera.setPosition(self.finalPos);
}
this.updateSuperZoom = function(delta) {
var ZOOM_STEP = 0.1;
self.currentZoomAmount = self.currentZoomAmount + (delta < 0.0 ? -1 : 1) * ZOOM_STEP;
self.currentZoomAmount = Math.min(Math.max(self.currentZoomAmount, -self.maxZoomOutAmount), self.maxZoomInAmount);
self.updateSuperPan(self.zoomPanOffset.x, self.zoomPanOffset.y);
}
this.updateSuperPan = function(x, y) {
var zoomOffset = Vec3.multiply(self.currentZoomAmount, Vec3.subtract(self.lookAt, self.finalPos));
var xRatio = 0.5 - x / Window.innerWidth;
var yRatio = 0.5 - y / Window.innerHeight;
var cameraOrientation = Camera.getOrientation();
var cameraY = Quat.getUp(cameraOrientation);
var cameraX = Vec3.multiply(-1, Quat.getRight(cameraOrientation));
var xOffset = Vec3.multiply(xRatio * self.focusDimensions.x, cameraX);
var yOffset = Vec3.multiply(yRatio * self.focusDimensions.y, cameraY);
zoomOffset = Vec3.sum(zoomOffset, xOffset);
zoomOffset = Vec3.sum(zoomOffset, yOffset);
Camera.setPosition(Vec3.sum(self.finalPos, zoomOffset));
}
this.abort = function() {
self.changeCameraMode(self.initialMode);
}
this.changeCameraMode = function(mode) {
self.ownCameraChange = true;
self.ownCameraChangeElapseTime = 0.0;
Camera.mode = mode;
}
}
var ZoomOnAnything = function() {
var self = this;
this.zoomEntityID;
this.zoomEntityData;
this.zoomCameraPos;
var ZOOM_MAX_VELOCITY = 15.0; // meters per second
var ZOOM_MAX_DURATION = 1.0;
this.zoomDelta = {x: 0.0, y: 0.0};
this.isPanning = false;
this.screenPointRef = {x: 0.0, y: 0.0};
this.getEntityDimsFromNormal = function(dims, rot, normal) {
var zoomXNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_X);
var zoomYNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_Y);
var zoomZNormal = Vec3.multiplyQbyV(rot, Vec3.UNIT_Z);
var affinities = [
{axis: "x", normal: zoomXNormal, affin: Math.abs(Vec3.dot(zoomXNormal, normal)), dims: {x: dims.z, y: dims.y}},
{axis: "y", normal: zoomYNormal, affin: Math.abs(Vec3.dot(zoomYNormal, normal)), dims: {x: dims.x, y: dims.z}},
{axis: "z", normal: zoomZNormal, affin: Math.abs(Vec3.dot(zoomZNormal, normal)), dims: {x: dims.x, y: dims.y}}
];
affinities.sort(function(a, b) {
return b.affin - a.affin;
});
return affinities[0];
}
this.getAvatarFocusPoint = function(avatar) {
var rEyeIndex = avatar.getJointIndex("RightEye");
var lEyeIndex = avatar.getJointIndex("LeftEye");
var headIndex = avatar.getJointIndex("Head");
var focusPoint = Vec3.ZERO;
var validPoint = false;
var count = 0;
if (rEyeIndex != -1) {
focusPoint = Vec3.sum(focusPoint, avatar.getJointPosition(rEyeIndex));
validPoint = true;
count++;
}
if (lEyeIndex != -1) {
var leftEyePos = avatar.getJointPosition(lEyeIndex);
var NORMAL_EYE_DISTANCE = 0.1;
focusPoint = Vec3.sum(focusPoint, leftEyePos);
validPoint = true;
count++;
}
if (headIndex != -1) {
focusPoint = Vec3.sum(focusPoint, avatar.getJointPosition(headIndex));
validPoint = true;
count++;
}
if (!validPoint) {
focusPoint = avatar.getJointPosition("Hips");
count++;
}
return Vec3.multiply(1.0/count, focusPoint);
}
this.getZoomDataFromAvatar = function(avatarID, skinToBoneDist, zoomVelocity, maxDuration) {
var headDiam = 2.0 * skinToBoneDist;
headDiam = headDiam < 0.5 ? 0.5 : headDiam;
var avatar = AvatarList.getAvatar(avatarID);
var focusPoint = self.getAvatarFocusPoint(avatar);
var focusDims = {x: headDiam, y: headDiam};
var focusNormal = Quat.getFront(avatar.orientation);
var zoomData = new ZoomData(FocusType.avatar, focusPoint, focusNormal, focusDims, zoomVelocity, maxDuration);
return zoomData;
}
this.getZoomDataFromEntity = function(intersection, objectProps, zoomVelocity, maxDuration) {
var position = objectProps.position;
var dimensions = objectProps.dimensions;
var rotation = objectProps.rotation;
var focusNormal = intersection.surfaceNormal;
var dimsResult = self.getEntityDimsFromNormal(dimensions, rotation, focusNormal);
var focusDims = dimsResult.dims;
var focusDepth = Vec3.dot(Vec3.subtract(intersection.intersection, position), dimsResult.normal);
var newPosition = Vec3.sum(position, Vec3.multiply(focusDepth, dimsResult.normal));
var zoomData = new ZoomData(FocusType.entity, newPosition, focusNormal, focusDims, zoomVelocity, maxDuration);
return zoomData;
}
this.zoomOnEntity = function(intersection, objectProps) {
self.zoomEntityData = self.getZoomDataFromEntity(intersection, objectProps, ZOOM_MAX_VELOCITY, ZOOM_MAX_DURATION);
self.zoomEntityData.initZoomIn();
}
this.zoomOnAvatar = function(avatarID, skinToBoneDist) {
self.zoomEntityData = self.getZoomDataFromAvatar(avatarID, skinToBoneDist, ZOOM_MAX_VELOCITY, ZOOM_MAX_DURATION);
self.zoomEntityData.initZoomIn();
}
this.updateZoom = function(deltaTime) {
if (self.zoomEntityData && self.zoomEntityData.needsUpdate()) {
self.zoomEntityData.updateZoom(deltaTime);
if (self.zoomEntityData.status === ZoomStatus.consumed) {
self.zoomEntityData = undefined;
}
}
}
this.mouseDoublePressEvent = function(event) {
if (event.isLeftButton) {
if (!self.zoomEntityData) {
var pickRay = Camera.computePickRay(event.x, event.y);
var intersection = AvatarManager.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, [], [MyAvatar.sessionUUID], false);
zoomingAtAvatarID = intersection.intersects ? intersection.avatarID : undefined;
if (!zoomingAtAvatarID) {
intersection = Entities.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, true);
self.zoomEntityID = intersection.entityID;
var entityProps = Entities.getEntityProperties(intersection.entityID);
if (entityProps.type === "Shape") {
var FIND_SHAPES_DISTANCE = 10.0;
var shapes = Entities.findEntitiesByType("Shape", intersection.intersection, FIND_SHAPES_DISTANCE);
intersection = Entities.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, true, [], shapes);
self.zoomEntityID = intersection.entityID;
entityProps = Entities.getEntityProperties(intersection.entityID);
}
if (!entityProps.dimensions) {
return;
}
self.zoomOnEntity(intersection, entityProps);
} else {
var avatar = AvatarList.getAvatar(zoomingAtAvatarID);
var skinToBoneDist = Vec3.distance(intersection.intersection, avatar.getJointPosition(intersection.jointIndex));
self.zoomOnAvatar(zoomingAtAvatarID, skinToBoneDist);
}
} else if (!self.zoomEntityData.needsUpdate()){
self.zoomEntityData.initZoomOut();
}
}
}
this.mousePressEvent = function(event) {
if (event.isRightButton) {
self.isPanning = true;
self.screenPointRef = {x: event.x, y: event.y};
}
}
this.mouseReleaseEvent = function(event) {
if (event.isRightButton) {
if (self.zoomEntityData) {
self.zoomEntityData.applyZoomPan();
self.isPanning = false;
self.screenPointRef = {x: 0, y: 0};
}
}
}
this.mouseMoveEvent = function(event) {
if (event.isRightButton) {
if (self.isPanning && self.zoomEntityData) {
self.zoomEntityData.setZoomPanDelta(event.x - self.screenPointRef.x, event.y - self.screenPointRef.y);
}
}
}
this.mouseWheel = function(event) {
if (self.zoomEntityData) {
self.zoomEntityData.updateSuperZoom(event.delta);
}
}
this.abort = function() {
self.zoomEntityData.abort();
self.zoomEntityData = undefined;
}
}
var zoomOE = new ZoomOnAnything();
Window.geometryChanged.connect(function() {
if (zoomOE.zoomEntityData){
zoomOE.zoomEntityData.resetZoomAspect();
}
});
Camera.modeUpdated.connect(function(mode) {
if (zoomOE.zoomEntityData && !zoomOE.zoomEntityData.ownCameraChange) {
zoomOE.abort();
}
});
Controller.mousePressEvent.connect(zoomOE.mousePressEvent);
Controller.mouseDoublePressEvent.connect(zoomOE.mouseDoublePressEvent);
Controller.mouseMoveEvent.connect(zoomOE.mouseMoveEvent);
Controller.mouseReleaseEvent.connect(zoomOE.mouseReleaseEvent);
Controller.wheelEvent.connect(zoomOE.mouseWheel);
Script.update.connect(zoomOE.updateZoom);
Script.scriptEnding.connect(function() {
if (zoomOE.zoomEntityData) {
zoomOE.abort();
}
});
})();

View file

@ -17,7 +17,8 @@ var currentlyRunningScripts = ScriptDiscoveryService.getRunning();
var DEFAULT_SCRIPTS_SEPARATE = [
"system/controllers/controllerScripts.js",
"simplifiedUI/ui/simplifiedUI.js"
"simplifiedUI/ui/simplifiedUI.js",
"simplifiedUI/clickToZoom/clickToZoom.js"
];
function loadSeparateDefaults() {
for (var i = 0; i < DEFAULT_SCRIPTS_SEPARATE.length; i++) {

View file

@ -558,9 +558,6 @@
"gravity": {
"tooltip": "The acceleration due to gravity that the entity should move with, in world space."
},
"acceleration": {
"tooltip": "A acceleration that the entity should move with, in world space."
},
"renderLayer": {
"tooltip": "The layer on which this entity is rendered."
},

View file

@ -569,7 +569,59 @@ var toolBar = (function () {
}
}
if (type === "Model") {
properties.visible = false;
}
entityID = Entities.addEntity(properties);
var dimensionsCheckCallback = function(){
var POST_ADJUST_ENTITY_TYPES = ["Model"];
if (POST_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) {
// Adjust position of entity per bounding box after it has been created and auto-resized.
var initialDimensions = Entities.getEntityProperties(entityID, ["dimensions"]).dimensions;
var DIMENSIONS_CHECK_INTERVAL = 200;
var MAX_DIMENSIONS_CHECKS = 10;
var dimensionsCheckCount = 0;
var dimensionsCheckFunction = function () {
dimensionsCheckCount++;
var properties = Entities.getEntityProperties(entityID, ["dimensions", "registrationPoint", "rotation"]);
if (!Vec3.equal(properties.dimensions, initialDimensions)) {
position = adjustPositionPerBoundingBox(position, direction, properties.registrationPoint,
properties.dimensions, properties.rotation);
position = grid.snapToSurface(grid.snapToGrid(position, false, properties.dimensions),
properties.dimensions);
Entities.editEntity(entityID, {
position: position
});
selectionManager._update(false, this);
} else if (dimensionsCheckCount < MAX_DIMENSIONS_CHECKS) {
Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL);
}
};
Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL);
}
}
// Make sure the entity is loaded before we try to figure out
// its dimensions.
var MAX_LOADED_CHECKS = 10;
var LOADED_CHECK_INTERVAL = 100;
var isLoadedCheckCount = 0;
var entityIsLoadedCheck = function() {
isLoadedCheckCount++;
if (isLoadedCheckCount === MAX_LOADED_CHECKS || Entities.isLoaded(entityID)) {
var naturalDimensions = Entities.getEntityProperties(entityID, "naturalDimensions").naturalDimensions
Entities.editEntity(entityID, {
visible: true,
dimensions: naturalDimensions
})
dimensionsCheckCallback();
return;
}
Script.setTimeout(entityIsLoadedCheck, LOADED_CHECK_INTERVAL);
}
Script.setTimeout(entityIsLoadedCheck, LOADED_CHECK_INTERVAL);
SelectionManager.addEntity(entityID, false, this);
SelectionManager.saveProperties();
pushCommandForSelections([{
@ -577,31 +629,6 @@ var toolBar = (function () {
properties: properties
}], [], true);
var POST_ADJUST_ENTITY_TYPES = ["Model"];
if (POST_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) {
// Adjust position of entity per bounding box after it has been created and auto-resized.
var initialDimensions = Entities.getEntityProperties(entityID, ["dimensions"]).dimensions;
var DIMENSIONS_CHECK_INTERVAL = 200;
var MAX_DIMENSIONS_CHECKS = 10;
var dimensionsCheckCount = 0;
var dimensionsCheckFunction = function () {
dimensionsCheckCount++;
var properties = Entities.getEntityProperties(entityID, ["dimensions", "registrationPoint", "rotation"]);
if (!Vec3.equal(properties.dimensions, initialDimensions)) {
position = adjustPositionPerBoundingBox(position, direction, properties.registrationPoint,
properties.dimensions, properties.rotation);
position = grid.snapToSurface(grid.snapToGrid(position, false, properties.dimensions),
properties.dimensions);
Entities.editEntity(entityID, {
position: position
});
selectionManager._update(false, this);
} else if (dimensionsCheckCount < MAX_DIMENSIONS_CHECKS) {
Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL);
}
};
Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL);
}
} else {
Window.notifyEditError("Can't create " + properties.type + ": " +
properties.type + " would be out of bounds.");

View file

@ -1545,16 +1545,6 @@ const GROUPS = [
unit: "m/s<sup>2</sup>",
propertyID: "gravity",
},
{
label: "Acceleration",
type: "vec3",
vec3Type: "xyz",
subLabels: [ "x", "y", "z" ],
step: 0.1,
decimals: 4,
unit: "m/s<sup>2</sup>",
propertyID: "acceleration",
},
]
},
];

View file

@ -363,7 +363,8 @@ void EntityTests::entityTreeTests(bool verbose) {
}
quint64 startDelete = usecTimestampNow();
tree.deleteEntity(entityID);
bool force = true;
tree.deleteEntity(entityID, force);
quint64 endDelete = usecTimestampNow();
totalElapsedDelete += (endDelete - startDelete);
@ -433,12 +434,13 @@ void EntityTests::entityTreeTests(bool verbose) {
quint64 totalElapsedFind = 0;
for (int i = 0; i < TEST_ITERATIONS; i++) {
QSet<EntityItemID> entitiesToDelete;
std::vector<EntityItemID> entitiesToDelete;
entitiesToDelete.reserve(ENTITIES_PER_ITERATION);
for (int j = 0; j < ENTITIES_PER_ITERATION; j++) {
//uint32_t id = 2 + (i * ENTITIES_PER_ITERATION) + j; // These are the entities we added above
QUuid id = QUuid::createUuid();// make sure it doesn't collide with previous entity ids
EntityItemID entityID(id);
entitiesToDelete << entityID;
entitiesToDelete.push_back(entityID);
}
if (extraVerbose) {
@ -446,7 +448,9 @@ void EntityTests::entityTreeTests(bool verbose) {
}
quint64 startDelete = usecTimestampNow();
tree.deleteEntities(entitiesToDelete);
bool force = true;
bool ignoreWarnings = true;
tree.deleteEntitiesByID(entitiesToDelete, force, ignoreWarnings);
quint64 endDelete = usecTimestampNow();
totalElapsedDelete += (endDelete - startDelete);