/* Copyright 2003-2005 ROBLOX Corporation, All Rights Reserved */ #include "stdafx.h" #include "V8DataModel/Workspace.h" #include "V8DataModel/PartInstance.h" #include "V8DataModel/Stats.h" #include "V8DataModel/GameBasicSettings.h" #include "V8DataModel/GeometryService.h" #include "V8DataModel/UserController.h" #include "V8DataModel/DebugSettings.h" #include "V8DataModel/Camera.h" #include "V8DataModel/Hopper.h" #include "V8DataModel/Accoutrement.h" #include "V8DataModel/Flag.h" #include "V8DataModel/ContentProvider.h" #include "V8DataModel/ToolsSurface.h" #include "V8DataModel/ImageLabel.h" #include "V8DataModel/PlayerGui.h" #include "V8DataModel/PluginManager.h" #include "V8DataModel/PluginMouse.h" #include "v8datamodel/PhysicsSettings.h" #include "v8datamodel/MegaCluster.h" #include "v8datamodel/UserInputService.h" #include "v8datamodel/SurfaceGui.h" #include "Humanoid/Humanoid.h" #include "Tool/NullTool.h" #include "Tool/ToolsArrow.h" #include "Tool/DropTool.h" #include "Network/Players.h" #include "V8World/World.h" #include "V8World/SleepStage.h" #include "V8World/ContactManager.h" #include "V8World/SpatialFilter.h" #include "V8World/StepJointsStage.h" #include "V8World/AssemblyStage.h" #include "V8Kernel/Kernel.h" #include "V8Xml/Serializer.h" #include "V8Xml/XmlSerializer.h" #include "Util/UserInputBase.h" #include "Util/Profiling.h" #include "Util/NavKeys.h" #include "util/RobloxGoogleAnalytics.h" #include "Util/G3DCore.h" #include "Script/ScriptContext.h" #include "Script/Script.h" #include "Script/CoreScript.h" #include "SelectState.h" #include "GfxBase/IAdornableCollector.h" #include "FastLog.h" // For Tyler's experimental fallen parts deletion #include "Network/NetworkOwner.h" #include "Network/Players.h" #include "Network/api.h" LOGGROUP(MouseCommand) DYNAMIC_FASTFLAG(FixTouchEndedReporting) // Cyclic Executive Experiment Logging (THESE NEED TO BE REMOVED IN THE FUTURE, YAY) DYNAMIC_FASTFLAGVARIABLE(PreventReturnOfElevatedPhysicsFPS, false) DYNAMIC_FASTFLAGVARIABLE(ReportElevatedPhysicsFPSToGA, true) DYNAMIC_FASTINTVARIABLE(ElevatedPhysicsFPSReportThresholdTenths, 610) //END CyclicExecutive Experiment Logging FASTFLAGVARIABLE(PhysicsAnalyzerEnabled, false) FASTFLAGVARIABLE(PGSAlwaysActiveMasterSwitch, false) FASTFLAG(UsePGSSolver) FASTFLAGVARIABLE(LuaControlsDisableMouse2Lock, false) DYNAMIC_FASTFLAG(CreatePlayerGuiLocal) FASTFLAG(FlyCamOnRenderStep) FASTFLAG(PGSSolverFileDump) DYNAMIC_FASTFLAG(UseStarterPlayerCharacter) FASTFLAGVARIABLE(GamepadCursorChanges, false) DYNAMIC_FASTFLAG(FixFallenPartsNotDeleted) DYNAMIC_FASTFLAGVARIABLE(TrackPhysicalPropertiesGA, false); namespace RBX { namespace Reflection { template<> EnumDesc::EnumDesc() :EnumDescriptor("PhysicalPropertiesMode") { addPair(PhysicalPropertiesMode_Default, "Default"); addPair(PhysicalPropertiesMode_Legacy, "Legacy"); addPair(PhysicalPropertiesMode_NewPartProperties, "New"); } } REFLECTION_BEGIN(); static const Reflection::PropDescriptor prop_DistributedGameTime("DistributedGameTime", category_Data, &Workspace::getDistributedGameTime, &Workspace::setDistributedGameTime, Reflection::PropertyDescriptor::STANDARD_NO_REPLICATE); Reflection::PropDescriptor Workspace::prop_StreamingEnabled("StreamingEnabled", category_Behavior, &Workspace::getNetworkStreamingEnabled, &Workspace::setNetworkStreamingEnabled, Reflection::PropertyDescriptor::STANDARD_NO_REPLICATE); Reflection::PropDescriptor Workspace::prop_ExperimentalSolverEnabled("PGSPhysicsSolverEnabled", category_Behavior, &Workspace::getExperimentalSolverEnabled, &Workspace::setExperimentalSolverEnabled, Reflection::PropertyDescriptor::PUBLIC_SERIALIZED); Reflection::PropDescriptor Workspace::prop_ExpSolverEnabled_Replicate("ExpSolverEnabled_Replicate", category_Behavior, &Workspace::getExpSolverEnabled_Replicate, &Workspace::setExpSolverEnabled_Replicate, Reflection::PropertyDescriptor::STREAMING); Reflection::PropDescriptor Workspace::prop_FilteringEnabled("FilteringEnabled", category_Behavior, &Workspace::getNetworkFilteringEnabled, &Workspace::setNetworkFilteringEnabled, Reflection::PropertyDescriptor::STANDARD_NO_REPLICATE); static Reflection::PropDescriptor prop_fallenPartDestroyHeight("FallenPartsDestroyHeight", category_Behavior, &Workspace::getFallenPartDestroyHeight, &Workspace::setFallenPartDestroyHeight, Reflection::PropertyDescriptor::STANDARD_NO_SCRIPTING); Reflection::PropDescriptor Workspace::prop_allowThirdPartySales("AllowThirdPartySales", category_Behavior, &Workspace::getAllowThirdPartySales, &Workspace::setAllowThirdPartySales, Reflection::PropertyDescriptor::STANDARD_NO_REPLICATE); // Use ReleastTest build to ask whether or not we have a certain PhysicalProperties mode enabled #ifndef RBX_TEST_BUILD Reflection::EnumPropDescriptor Workspace::prop_physicalPropertiesMode("PhysicalPropertiesMode", category_Behavior, &Workspace::getPhysicalPropertiesMode, &Workspace::setPhysicalPropertiesMode, Reflection::PropertyDescriptor::STANDARD_NO_SCRIPTING); #else Reflection::EnumPropDescriptor Workspace::prop_physicalPropertiesMode("PhysicalPropertiesMode", category_Behavior, &Workspace::getPhysicalPropertiesMode, &Workspace::setPhysicalPropertiesMode, Reflection::PropertyDescriptor::STANDARD); #endif static Reflection::BoundFuncDesc(int)> func_getIssueGroup(&Workspace::getPhysicsAnalyzerIssue, "GetPhysicsAnalyzerIssue", "index", Security::Plugin); static Reflection::BoundFuncDesc func_setPhysicsAnalyzerBreakOnIssue(&Workspace::setPhysicsAnalyzerBreakOnIssue, "SetPhysicsAnalyzerBreakOnIssue", "enable", Security::Plugin); static Reflection::BoundFuncDesc func_getPhysicsAnalyzerBreakOnIssue(&Workspace::getPhysicsAnalyzerBreakOnIssue, "GetPhysicsAnalyzerBreakOnIssue", Security::Plugin); static Reflection::EventDesc desc_PhysicsAnalyzerIssueFound(&Workspace::luaPhysicsAnalyzerIssuesFound, "PhysicsAnalyzerIssuesFound", "count", Security::Plugin); REFLECTION_END(); bool Workspace::showWorldCoordinateFrame = false; bool Workspace::showHashGrid = false; bool Workspace::showEPhysicsOwners = false; bool Workspace::showEPhysicsRegions = false; bool Workspace::showStreamedRegions = false; bool Workspace::showPartMovementPath = false; bool Workspace::showActiveAnimationAsset = false; float Workspace::gridSizeModifier = 4.0f; const char* const sWorkspace = "Workspace"; // static bool Workspace::serverIsPresent(const Instance* context) // shortcut for RBX::Network::Players::serverIsPresent { return RBX::Network::Players::serverIsPresent(context, false); // if not in the datamodel, will go false } // static bool Workspace::clientIsPresent(const Instance* context) // shortcut for RBX::Network::Players::clientIsPresent { return RBX::Network::Players::clientIsPresent(context, false); // if not in the datamodel, will go false } const Workspace* Workspace::findConstWorkspace(const Instance* context) { return ServiceProvider::find(context); } Workspace* Workspace::findWorkspace(Instance* context) { return ServiceProvider::find(context); } Instance* Workspace::findTopInstance(Instance* context) // finds the top level Instance in the workspace (right below the workspace) { RBXASSERT(getWorldIfInWorkspace(context)); Workspace* workspace = Workspace::getWorkspaceIfInWorkspace(context); RBXASSERT(workspace); RBXASSERT(workspace != context); while (context->getParent() != workspace) { context = context->getParent(); } return context; } Workspace* Workspace::getWorkspaceIfInWorkspace(Instance* context) { if (Workspace* workspace = findWorkspace(context)) { if ((context == workspace) || context->isDescendantOf(workspace)) { return workspace; } } return NULL; } // returns getWorld if context is within the RootInstance World* Workspace::getWorldIfInWorkspace(Instance* context) { if (Workspace* workspace = getWorkspaceIfInWorkspace(context)) { return workspace->getWorld(); } else { return NULL; } } ContactManager* Workspace::getContactManagerIfInWorkspace(Instance* context) { if (World* world = getWorldIfInWorkspace(context)) { return world->getContactManager(); } return NULL; } bool Workspace::contextInWorkspace(const Instance* context) { const Workspace* workspace = findConstWorkspace(context); RBXASSERT(!workspace || (workspace != context)); return (workspace && context->isDescendantOf(workspace)); } REFLECTION_BEGIN(); static Reflection::BoundFuncDesc)> workspace_makeJoints(&Workspace::makeJoints, "MakeJoints", "objects", Security::Plugin); static Reflection::BoundFuncDesc)> workspace_breakJoints(&Workspace::breakJoints, "BreakJoints", "objects", Security::Plugin); // single ignore instance overloads for region/ray queries static Reflection::BoundFuncDesc(Region3, shared_ptr, int)> workspace_FindParts(&Workspace::findPartsInRegion3, "FindPartsInRegion3", "region", "ignoreDescendentsInstance", shared_ptr(), "maxParts", 20, Security::None); static Reflection::BoundFuncDesc(Region3, shared_ptr, int)> workspace_dep_FindParts(&Workspace::findPartsInRegion3, "findPartsInRegion3", "region", "ignoreDescendentsInstance", shared_ptr(), "maxParts", 20, Security::None, Reflection::Descriptor::Attributes::deprecated(workspace_FindParts)); static Reflection::BoundFuncDesc)> workspace_RegionEmpty(&Workspace::isRegion3Empty, "IsRegion3Empty", "region", "ignoreDescendentsInstance", shared_ptr(), Security::None); static Reflection::BoundFuncDesc(RbxRay, shared_ptr, bool, bool)> workspace_FindPartOnRay(&Workspace::getRayHit, "FindPartOnRay", "ray", "ignoreDescendentsInstance", shared_ptr(), "terrainCellsAreCubes", false, "ignoreWater", false, Security::None); static Reflection::BoundFuncDesc(RbxRay, shared_ptr, bool, bool)> dep_FindPartOnRay(&Workspace::getRayHit, "findPartOnRay", "ray", "ignoreDescendentsInstance", shared_ptr(), "terrainCellsAreCubes", false, "ignoreWater", false, Security::None, Reflection::Descriptor::Attributes::deprecated(workspace_FindPartOnRay)); // table of ignore instances overloads for region/ray queries static Reflection::BoundFuncDesc(Region3, shared_ptr, int)> workspace_FindParts2(&Workspace::findPartsInRegion3, "FindPartsInRegion3WithIgnoreList", "region", "ignoreDescendentsTable", "maxParts", 20, Security::None); static Reflection::BoundFuncDesc)> workspace_RegionEmpty2(&Workspace::isRegion3Empty, "IsRegion3EmptyWithIgnoreList", "region", "ignoreDescendentsTable", Security::None); static Reflection::BoundFuncDesc(RbxRay, shared_ptr, bool, bool)> workspace_FindPartOnRay2(&Workspace::getRayHit, "FindPartOnRayWithIgnoreList", "ray", "ignoreDescendentsTable", "terrainCellsAreCubes", false, "ignoreWater", false, Security::None); static Reflection::RefPropDescriptor workspace_Terrain("Terrain", category_Behavior, &Workspace::getTerrain, NULL, Reflection::PropertyDescriptor::UI, Security::None); // Removed - set through debugSettings static Reflection::BoundFuncDesc workspace_SetThrottleEnabled(&Workspace::doNothing, "SetPhysicsThrottleEnabled", "value", Security::LocalUser); // Repressed zoomToExtents to see if anyone is calling it... static Reflection::BoundFuncDesc workspace_zoomToExtents(&Workspace::zoomToExtents, "ZoomToExtents", Security::Plugin); //static Reflection::BoundFuncDesc workspace_setNullMouseCommand(&Workspace::setNullMouseCommand, "SetNullMouseCommand", Security::LocalUser); static Reflection::RefPropDescriptor currentCameraProxyProp("CurrentCamera", category_Data, &Workspace::getCurrentCameraDangerous, &Workspace::setCurrentCamera, Reflection::PropertyDescriptor::STANDARD_NO_REPLICATE); // Metrics static Reflection::BoundFuncDesc getRealPhysicsFPS(&Workspace::getRealPhysicsFPS, "GetRealPhysicsFPS", Security::None); static Reflection::BoundFuncDesc getPhysicsThrottling(&Workspace::getPhysicsThrottling, "GetPhysicsThrottling", Security::None); static Reflection::BoundFuncDesc getNumAwakeParts(&Workspace::getNumAwakeParts, "GetNumAwakeParts", Security::None); static Reflection::BoundFuncDesc experimentalSolverIsEnabled(&Workspace::experimentalSolverIsEnabled, "ExperimentalSolverIsEnabled", Security::TestLocalUser); static Reflection::BoundFuncDesc pgsSolverIsEnabled(&Workspace::experimentalSolverIsEnabled, "PGSIsEnabled", Security::None); static Reflection::BoundFuncDesc, AdvArrowToolBase::JointCreationMode)> workspace_joinToOutsiders(&Workspace::joinToOutsiders, "JoinToOutsiders", "objects", "jointType", Security::None); static Reflection::BoundFuncDesc)> workspace_unjoinFromOutsiders(&Workspace::unjoinFromOutsiders, "UnjoinFromOutsiders", "objects", Security::None); REFLECTION_END(); Workspace::Workspace(IDataState* dataState) : VerbContainer(NULL), flySteps(0), arrowCameraControls(false), dataState(dataState), inRightMousePan(false), inMiddleMouseTrack(false), imageServerViewHack(0), profileDataModelStep(new Profiling::CodeProfiler("DataModel Step")), profileWorkspaceStep(new Profiling::CodeProfiler("Workspace Step")), profileWorkspaceAssemble(new Profiling::CodeProfiler("Workspace Assemble")), utilityCamera(Creatable::create()), distributedGameTime(0.0), statsSyncHttpGetTime(0), statsXMLLoadTime(0), statsJoinAllTime(0), adornableCollector(new IAdornableCollector()), firstPersonCam(false), leftMouseDown(false), show3DGrid(false), showAxisWidget(false), lastComputedWorldExtents(Vector3::zero(), Vector3::zero()), lastComputedWorldExtentsTime(-DBL_MAX), networkStreamingEnabled(false), experimentalSolverEnabled(false), expSolverEnabled_Replicate(false), renderingDistance(10000.f), networkFilteringEnabled(true), allowThirdPartySales(false) { RBXASSERT(dataState!=NULL); setName("Workspace"); FASTLOG1(FLog::GuiTargetLifetime, "Workspace created: %p", this); // Master switch used for turning on the PGS solver regardless of the Workspace toggle or code FFlag - for testing purposes getWorld()->setUsingPGSSolver(FFlag::PGSAlwaysActiveMasterSwitch); } Workspace::~Workspace() { FASTLOG1(FLog::GuiTargetLifetime, "Workspace destroyed: %p", this); } // we template this to avoid heavy code duplication, but still keep the functions fast (IgnoreType is currently either Instance or Instances) template shared_ptr Workspace::getRayHit(RbxRay ray, shared_ptr ignoreInstance, bool terrainCellsAreCubes, bool ignoreWaterCells) { shared_ptr result(new Reflection::Tuple()); if (GeometryService* geometryService = ServiceProvider::create(this)) { shared_ptr found; Vector3 surfaceNormal; PartMaterial surfaceMaterial = AIR_MATERIAL; Vector3 point = geometryService->getHitLocationPartFilterDescendents(ignoreInstance.get(), ray, found, surfaceNormal, surfaceMaterial, terrainCellsAreCubes, ignoreWaterCells); result->values.push_back(shared_static_cast(found)); result->values.push_back(point); result->values.push_back(surfaceNormal); result->values.push_back(surfaceMaterial); } return result; } bool Workspace::isRegion3Empty(Region3 region, shared_ptr ignoreDescendent) { shared_ptr newInstances(new Instances()); if (ignoreDescendent) newInstances->push_back(ignoreDescendent); return isRegion3Empty(region, newInstances); } bool Workspace::isRegion3Empty(Region3 region, shared_ptr ignoreDescendents) { // check to see if we're blocked by a part shared_ptr blockingParts = findPartsInRegion3(region, ignoreDescendents, 1); if (!blockingParts.get()->empty()) return false; // check to see if we're blocked by terrain return (!getWorld()->getContactManager()->terrainCellsInRegion3(region)); } void Workspace::joinToOutsiders(shared_ptr items, AdvArrowToolBase::JointCreationMode joinType) { if (joinType == AdvArrowToolBase::NO_JOIN) return; PartArray partArray; DragUtilities::instancesToParts(*items, partArray); DragUtilities::joinToOutsiders(partArray); if (joinType == AdvArrowToolBase::WELD_ALL) { ManualJointHelper jointHelper; jointHelper.setSelectedPrimitives(*items); jointHelper.setWorkspace(this); jointHelper.findPermissibleJointSurfacePairs(); jointHelper.createJoints(); } } void Workspace::unjoinFromOutsiders(shared_ptr items) { PartArray partArray; DragUtilities::instancesToParts(*items, partArray); DragUtilities::unJoinFromOutsiders(partArray); } shared_ptr Workspace::findPartsInRegion3(Region3 region, shared_ptr ignoreDescendent, int maxCount) { shared_ptr newInstances(new Instances()); if (ignoreDescendent) newInstances->push_back(ignoreDescendent); return findPartsInRegion3(region, newInstances, maxCount); } shared_ptr Workspace::findPartsInRegion3(Region3 region, shared_ptr ignoreDescendents, int maxCount) { static const float volumeLimit = 100000.0f; Extents extents(region.minPos(),region.maxPos()); if(extents.volume() > volumeLimit){ StandardOut::singleton()->printf(MESSAGE_WARNING, "Volume between min and max exceeds limit of %f", volumeLimit); } maxCount = std::min(maxCount, 100); if(GeometryService* geometryService = ServiceProvider::create(this)){ G3D::Array found; geometryService->getPartsTouchingExtentsWithIgnore(extents, ignoreDescendents.get(), maxCount, found); shared_ptr result(new Instances()); for (int i = 0; i < found.size(); ++i) { result->push_back(shared_from(found[i])); } return result; } return shared_ptr(); } Extents Workspace::computeExtentsWorldFast() { // recompute extents if last computed is over 2 seconds old, otherwise just return last computed value if (lastComputedWorldExtentsTime == 0 || distributedGameTime - lastComputedWorldExtentsTime > 2.0f) { lastComputedWorldExtents = RootInstance::computeExtentsWorld(); lastComputedWorldExtentsTime = distributedGameTime; } return lastComputedWorldExtents; } void Workspace::setDistributedGameTime(double value) { if (value != distributedGameTime) { distributedGameTime = value; raiseChanged(prop_DistributedGameTime); } } void Workspace::setDistributedGameTimeNoTransmit(double value) { distributedGameTime = value; } bool Workspace::forceDrawConnectors() const { RBXASSERT(currentCommand.get() != NULL); if (currentCommand.get()) { return currentCommand->drawConnectors(); } else { return false; } } namespace { void sendNetworkFilteringStats() { RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "NetworkFilteringEnabled"); } } // namespace void Workspace::setNetworkFilteringEnabled(bool value) { if (value && Workspace::serverIsPresent(this)) { static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(&sendNetworkFilteringStats, flag); } bool changed = networkFilteringEnabled != value; networkFilteringEnabled = value; if (networkFilteringEnabled && DFFlag::CreatePlayerGuiLocal && Network::Players::frontendProcessing(this)) { if (Network::Player* player = Network::Players::findLocalPlayer(this)) { // this allows guis to be created if you load a character manually later player->createPlayerGui(); } } if (changed && Network::Players::isCloudEdit(this)) { raisePropertyChanged(prop_FilteringEnabled); } } void Workspace::setAllowThirdPartySales(bool value) { bool changed = allowThirdPartySales != value; allowThirdPartySales = value; if (changed && Network::Players::isCloudEdit(this)) { raisePropertyChanged(prop_allowThirdPartySales); } } // Slop - create camera here if we don't have one already // void Workspace::onHeartbeat(const Heartbeat& heartbeat) { // IMovingManager onMovingHeartbeat(); replenishCamera(); if( terrain && terrain->getParent() != this ) { terrain->setAndLockParent(this); } // update deferred terrain contacts/joints world->getContactManager()->applyDeferredTerrainChanges(); // Step the current tool MouseCommand* mouseCommand = getCurrentMouseCommand(); if(mouseCommand && idleMouseEvent && idleMouseEvent->getUserInputType() == InputObject::TYPE_MOUSEIDLE) { mouseCommand->onMouseIdle(idleMouseEvent); if (Network::Players::frontendProcessing(this)) { if (Network::Player* localPlayer = Network::Players::findLocalPlayer(this)) { if (shared_ptr mouse = localPlayer->getMouse()) { mouse->idleSignal(); } } } if (Plugin *activePlugin = PluginManager::singleton()->getActivePlugin(DataModel::get(this))) if (PluginMouse* mouse = activePlugin->getMouse()) mouse->idleSignal(); } if (!FFlag::FlyCamOnRenderStep) { // If no local character, do camera fly with arrow keys if (!Network::Players::findLocalCharacter(this)) { if (ControllerService* service = ServiceProvider::find(this)) { if (const UserInputBase* hardwareDevice = service->getHardwareDevice()) { NavKeys navKeys; if(DataModel* dm = DataModel::get(this)) hardwareDevice->getNavKeys(navKeys,dm->getSharedSuppressNavKeys()); if (navKeys.navKeyDown()) { if (getCurrentMouseCommand()) getCamera()->doFly(navKeys, flySteps++); } else { if(flySteps > 0) getCamera()->pushCameraHistoryStack(); flySteps = 0; } } } } } } bool Workspace::askAddChild(const Instance* instance) const { return dynamic_cast(instance)!=NULL; // TODO: Hmmm. Is this a good choice? What about RBX::Message? } void Workspace::onDescendantRemoving(const shared_ptr& instance) { if (IAdornable* iR = dynamic_cast(instance.get())) { adornableCollector->onRenderableDescendantRemoving(iR); } Super::onDescendantRemoving(instance); } // Note on legacyOffset // // Workspace // TopPVInstance // Child // Child // // If a child is added to TopPVInstance after the TopPVInstance is added to workspace, // an assertion will trigger. // Current code always attaches to topPVInstance last for old files // void Workspace::onDescendantAdded(Instance* instance) { Super::onDescendantAdded(instance); if (IAdornable* iR = dynamic_cast(instance)) { adornableCollector->onRenderableDescendantAdded(iR); } } bool Workspace::startDecalDrag(Decal *decal, RBX::InsertMode insertMode) { ServiceClient< Selection > sel(this); if (decal) { sel->setSelection(decal); } shared_ptr result = Creatable::create(this, decal, insertMode); this->setMouseCommand(result); return result != NULL; } bool Workspace::startPartDropDrag(const Instances& instances, bool suppressPartsAlign) { ServiceClient< Selection > sel(this); shared_ptr selWhenDone; std::vector dragInstances; for(std::vector >::const_iterator iter = instances.begin(); iter != instances.end(); ++iter){ dragInstances.push_back(iter->get()); } shared_ptr result = DropTool::createDropTool(Vector3(0,0,0),dragInstances,this,selWhenDone,suppressPartsAlign); this->setMouseCommand(result, true); return result != NULL; } //////////////////////////////////////////////////////////////////////////////////// // // Camera Camera* Workspace::getCamera() { return const_cast(getConstCamera()); } const Camera* Workspace::getConstCamera() const { return (currentCamera) ? currentCamera.get() : utilityCamera.get(); } // ToDo: Hack - using render3dAdorn as a delayed trigger here // do this on render3dAdorn - if no camera as a descendant, then create one void Workspace::replenishCamera() { if (currentCamera && this->isAncestorOf(currentCamera.get())) { return; // ok - no replenishment necessary } else { shared_ptr childCamera = shared_from(findFirstChildOfType()); if (!childCamera) { childCamera = shared_polymorphic_downcast(utilityCamera->clone(EngineCreator)); childCamera->setParent(this); } setCurrentCamera(childCamera.get()); } } //reflection only - breaks the const barrier Camera* Workspace::getCurrentCameraDangerous() const { return currentCamera.get(); } void destroyIfNotCurrent(shared_ptr destroy, const Camera* current) { RBXASSERT(current); if (Instance::fastDynamicCast(destroy.get())) { if (destroy.get() != current) { destroy->setParent(NULL); } } } // Note - for now only doing children - // TODO: go through everything? void Workspace::setCurrentCamera(Camera *value) { if(!serverIsPresent(this) || Network::Players::isCloudEdit(this)){ if (value != currentCamera.get()) { currentCamera = shared_from(value); this->raisePropertyChanged(currentCameraProxyProp); if (value) { visitChildren(boost::bind(&destroyIfNotCurrent, _1, value)); } currentCameraChangedSignal(currentCamera); } } } void Workspace::setTerrain(Instance* terrain) { FASTLOG1(FLog::MegaClusterInit, "Setting terrain on workspace, %p", terrain); this->terrain = shared_from(terrain); this->raisePropertyChanged(workspace_Terrain); if( terrain ) { this->terrain->lockParent(); this->terrain->setLockedParent(this); } } Instance* Workspace::getTerrain() const { return terrain.get(); } void Workspace::createTerrain() { if(!terrain) { FASTLOG(FLog::MegaClusterInit, "Terrain doesn't exist - creating terrain instance explicitly"); shared_ptr t = Creatable::create(); PartInstance* part = static_cast(t.get()); Vector3 rbxSize = part->getPartSizeXml(); CoordinateFrame clusterFrame(Vector3(-2,rbxSize.y/2, -2)); part->setCoordinateFrame(clusterFrame); t->setAndLockParent(this); RBXASSERT(terrain); } } void Workspace::clearTerrain() { if(terrain) { FASTLOG(FLog::MegaClusterInit, "Clearing terrain (from client replicator?)"); terrain->unlockParent(); terrain->setParent(NULL); terrain.reset(); } } void Workspace::onWrapMouse(const Vector2& wrapMouseDelta) { getCamera()->onMousePan(wrapMouseDelta); } // This is a scripting call - tries to make things pretty as well void Workspace::zoomToExtents() { getCamera()->zoomExtents(); } bool Workspace::setImageServerView(bool bIsPlace) { if(bIsPlace) return false; // place rendering. don't fiddle with the camera. // use the ThumnailCamera if it is the child of the first child of the workspace. Instance *model = NULL; if(numChildren() > 0) { model = getChild(0); } if(model) { Camera *camera = Instance::fastDynamicCast(model->findFirstChildByName("ThumbnailCamera")); if(camera) { setCurrentCamera(camera); return false; // found camera. done. } } // default behavior: replenishCamera(); HopperBin* superHack = NULL; for (size_t i = 0; i < this->numChildren(); ++i) { HopperBin* hopperBin = this->queryTypedChild(i); IHasLocation* iLocation = this->queryTypedChild(i); IAdornable* iRenderable = this->queryTypedChild(i); if (iLocation && iRenderable) { CoordinateFrame modelCoord = iLocation->getLocation(); getCamera()->setImageServerViewNoLerp(modelCoord); } // The hack part #1 - find a hopperBin in the workspace if (hopperBin) { superHack = hopperBin; } } // The hack part #2 - if hopperBin found - put it in the StarterPackService so it will be drawn if (superHack) { StarterPackService* starterPackService = ServiceProvider::create(this); superHack->setParent(starterPackService); } // The hack part #3 - toggle this bit - when drawing, hopperBins will draw full screen in this situation imageServerViewHack = (imageServerViewHack > 0) ? 0 : 1; // super hack - for now, on setting image server view, renders all hopper bins full screen return true; } ///////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////// void Workspace::selectAllTopLevelRenderable() { ServiceClient< Selection > sel(this); sel->clearSelection(); for (size_t i = 0; i < numChildren(); i++) { // 1. Top Level Only Instance* child = this->getChild(i); if (dynamic_cast(child)) { // 2. Only IAdornable if (!PartInstance::getLocked(child)) // 3. Not Locked { sel->addToSelection(child); } } } } void Workspace::joinAllHack() // Joins all primitives - called after a file read { world->joinAll(); } template static void wrapper(shared_ptr instance) { if (PartInstance* p = Instance::fastDynamicCast(instance.get())) if (make) p->join(); else p->destroyJoints(); else instance->visitChildren(&wrapper); } void Workspace::makeJoints(shared_ptr instances) { std::for_each(instances->begin(), instances->end(), &wrapper); } void Workspace::breakJoints(shared_ptr instances) { std::for_each(instances->begin(), instances->end(), &wrapper); } ///////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// void Workspace::start() { assemble(); RBXASSERT(!getCurrentMouseCommand()->captured()); // makes sure building tools are not greyed out shared_ptr coreGuiService = shared_from(ServiceProvider::find(this)); if(coreGuiService) if(RBX::ImageLabel* frame = Instance::fastDynamicCast(coreGuiService->findFirstChildByName2("Frame",true).get())) frame->setZIndex(1); } void Workspace::stop() { // makes sure building tools are grayed out (not simulating, therefore building tools don't work currently) shared_ptr coreGuiService = shared_from(ServiceProvider::find(this)); if(coreGuiService) if(RBX::ImageLabel* frame = Instance::fastDynamicCast(coreGuiService->findFirstChildByName2("Frame",true).get())) frame->setZIndex(10); RBXASSERT(!getCurrentMouseCommand()->captured()); updateDistributedGameTime(); } void Workspace::reset() { stop(); world->reset(); } void Workspace::detachParent(Instance* test) { shared_ptr oldParent = shared_from(test->getParent()); #ifdef _DEBUG std::vector > parts; PartInstance::findParts(test, parts); RBXASSERT(parts.empty()); #endif test->setParent(NULL); clearEmptiedModels(oldParent); } // Will delete test if: // 1. It is not the workspace // 2. It's a model // 3. It's not a character (contains no humanoid) // 4. Contains no parts directly as children // 5. Contains no models directly as children void Workspace::clearEmptiedModels(shared_ptr& test) { if (test.get() == this) // 1. return; if (ModelInstance* model = Instance::fastDynamicCast(test.get())) // 2. { if (!Humanoid::modelIsCharacter(model)) // 3. { if ( !model->findFirstChildOfType() // 4., 5. && !model->findFirstChildOfType() ) { if (DFFlag::UseStarterPlayerCharacter) { Network::Player *pPlayer = Network::Players::getPlayerFromCharacter(model); if (pPlayer) pPlayer->onCharacterDied(); } detachParent(model); } } } if (Accoutrement* accoutrement = Instance::fastDynamicCast(test.get())) { if (!accoutrement->findFirstChildOfType()) // 4. { detachParent(accoutrement); } } if (BackpackItem* backpackItem = Instance::fastDynamicCast(test.get())) { if (!backpackItem->findFirstChildOfType()) // 4. { detachParent(backpackItem); } } } void Workspace::handleFallenParts() { RBXASSERT(fallenParts.size() == 0); RBXASSERT(fallenPrimitives.size() == 0); world->computeFallen(fallenPrimitives); PartInstance::primitivesToParts(fallenPrimitives, fallenParts); // if we're in a client running distributed physics, then we might not have the authority to remove parts // so we just pass them all over to server when they fall off the ends of the world if (Network::Players::getGameMode(this) == Network::DPHYS_CLIENT) { for (size_t i = 0; i < fallenParts.size(); ++i) { shared_ptr part = fallenParts[i]; if (!DFFlag::FixFallenPartsNotDeleted) { // This property does not replicate and means nothing to the Client. //old comment - had good intention: // make sure server has enough time to process the parts we're sending part->resetNetworkOwnerTime(3.0); } part->setNetworkOwnerAndNotify(RBX::Network::NetworkOwner::Server()); } } else // otherwise, we summarily execute them, since we have the authority to do so { for (size_t i = 0; i < fallenParts.size(); ++i) { shared_ptr part = fallenParts[i]; shared_ptr oldParent = shared_from(part->getParent()); part->setParent(NULL); clearEmptiedModels(oldParent); } } fallenParts.resize(0); fallenPrimitives.fastClear(); } void Workspace::assemble() { world->assemble(); RBXASSERT(world->isAssembled()); } void Workspace::updateDistributedGameTime() { RunService* runService = ServiceProvider::create(this); if (serverIsPresent(this)) { setDistributedGameTime(runService->gameTime()); } else { setDistributedGameTimeNoTransmit(runService->gameTime()); // update but don't send } } int Workspace::updatePhysicsStepsRequiredForCyclicExecutive(float timeInterval) { return world->updateStepsRequiredForCyclicExecutive( timeInterval ); } float Workspace::physicsStep(bool longStep, float timeInterval, int numThreads) { RBXASSERT(world->isAssembled()); // testing - assemble in if (longStep) { updateDistributedGameTime(); // update deferred terrain contacts/joints world->getContactManager()->applyDeferredTerrainChanges(); } if( FFlag::PGSSolverFileDump ) { RBX::Network::Player* localPlayer = RBX::Network::Players::findLocalPlayer(DataModel::get( this )); int id = 0; if( localPlayer ) id = localPlayer->getUserID(); getWorld()->setUserId( id ); } world->setPhysicsAnalyzerEnabled( RBX::PhysicsSettings::singleton().getPhysicsAnalyzerState() ); // Step the world timeInterval = world->step(longStep, distributedGameTime, timeInterval, numThreads); if( FFlag::PhysicsAnalyzerEnabled && world->getKernel()->pgsSolver.getInconsistentBodyPairs().size() > 0 ) { luaPhysicsAnalyzerIssuesFound( world->getKernel()->pgsSolver.getInconsistentBodies().size() ); } // Copy all Primitives into a collection of shared_ptr<>. // If we don't do this, then a PartInstance may get collected // during the event firing. This would lead to stale Primitive // pointers. std::vector touchReportingParts; const int size = world->getTouchInfoFromLastStep().size(); { const G3D::Array& source = world->getTouchInfoFromLastStep(); touchReportingParts.resize(size); for (int i = 0; i < size; ++i) { const World::TouchInfo& src = source[i]; TouchPair& dst = touchReportingParts[i]; if (DFFlag::FixTouchEndedReporting) { dst.p1 = src.pi1; dst.p2 = src.pi2; } else { dst.p1 = shared_from(PartInstance::fromPrimitive(src.p1)); dst.p2 = shared_from(PartInstance::fromPrimitive(src.p2)); } dst.type = src.type == World::TouchInfo::Touch ? TouchPair::Touch : TouchPair::Untouch; } } world->clearTouchInfoFromLastStep(); RBXASSERT(world->isAssembled()); handleFallenParts(); for (int i = 0; i < size; ++i) { const TouchPair& info = touchReportingParts[i]; if (info.type == TouchPair::Touch) { info.p1->reportTouch(info.p2); if(info.p1->onDemandRead()) info.p1->onDemandWrite()->localSimulationTouchedSignal(info.p2); } else { info.p1->reportUntouch(info.p2); if(info.p1->onDemandRead()) info.p1->onDemandWrite()->deprecatedStoppedTouchingSignal(info.p2); } stepTouch(info); } return timeInterval; } // Undo any stickiness here void Workspace::setDefaultMouseCommand() { stickyCommand.reset(); setMouseCommand(shared_ptr()); } shared_ptr newNullTool(Workspace* workspace) { return Creatable::create(workspace); } // Undo any stickiness here void Workspace::setNullMouseCommand() { stickyCommand.reset(); currentCommand = shared_ptr(newNullTool(this)); updatePlayerMouseCommand(); } void Workspace::setMouseCommand(shared_ptr newMouseCommand, bool allowPluginOverride) { FASTLOG2(FLog::MouseCommand, "Set mouse command: %p, old command: %p", newMouseCommand.get(), currentCommand.get()); RBX::DataModel *dataModel = (DataModel*)ServiceProvider::findServiceProvider(this); RBX::Plugin *activePlugin = PluginManager::singleton()->getActivePlugin(dataModel); if ((!newMouseCommand || allowPluginOverride) && activePlugin && activePlugin->isTool()) { FASTLOG1(FLog::MouseCommand, "Rejecting because of plugin override, plugin: %p", activePlugin); return; } if (newMouseCommand.get() == NULL) // The MouseCommand has been used. Get a new Tool { if (stickyCommand.get()) { newMouseCommand = stickyCommand.get()->isSticky(); // pull a copy of the stickyCommand - returns a copy if isSticky FASTLOG2(FLog::MouseCommand, "Have sticky command %p, generating new mouse command: %p", stickyCommand.get(), newMouseCommand.get() ); } } if (newMouseCommand.get() == NULL) { if (Network::Players::findLocalPlayer(this) == NULL || Network::Players::isCloudEdit(this)) { newMouseCommand = Creatable::create(this); FASTLOG1(FLog::MouseCommand, "Generating new arrow tool: %p", newMouseCommand.get() ); } else { newMouseCommand = newNullTool(this); FASTLOG1(FLog::MouseCommand, "Setting as null tool: %p", newMouseCommand.get() ); } } RBXASSERT(newMouseCommand.get()); if (newMouseCommand != currentCommand) { RBXASSERT((currentCommand.get() == NULL) || !currentCommand.get()->captured()); FASTLOG2(FLog::MouseCommand, "Current command update, new: %p, old: %p", newMouseCommand.get(), currentCommand.get()); currentCommand = newMouseCommand; shared_ptr sticky = newMouseCommand->isSticky(); if (sticky.get() != NULL) { FASTLOG2(FLog::MouseCommand, "Sticky command replaced too: %p, old: %p", sticky.get(), stickyCommand.get()); stickyCommand = sticky; } if (activePlugin && activePlugin->isTool()) { PluginManager::singleton()->activate(NULL, dataModel); } updatePlayerMouseCommand(); } FASTLOG(FLog::MouseCommand, "Set mouse command: Done"); } void Workspace::updatePlayerMouseCommand() { if (Network::Player* player = Network::Players::findLocalPlayer(this)) { if (shared_ptr playerMouse = player->getMouse()) { playerMouse->setWorkspace(this); } } } void Workspace::render2d(Adorn* adorn) { getCurrentMouseCommand()->render2d(adorn); adornableCollector->render2dItems(adorn); } ContentId Workspace::getCursor() { if (UserInputService* userInputService = ServiceProvider::find(this)) { ContentId gameCursor = userInputService->getCurrentMouseIcon(); if( gameCursor.isNull() ) { return getCurrentMouseCommand()->getCursorId(); } return gameCursor; } else { return getCurrentMouseCommand()->getCursorId(); } } double Workspace::getRealPhysicsFPS(void) { RunService* runService = ServiceProvider::create(this); double realPhysicsFPS = runService->smoothFps() * getWorld()->getEnvironmentSpeed(); double reportedPhysicsFPS = realPhysicsFPS; if (DFFlag::ReportElevatedPhysicsFPSToGA && realPhysicsFPS > ((double) DFInt::ElevatedPhysicsFPSReportThresholdTenths / 10.0)) { if (DFFlag::PreventReturnOfElevatedPhysicsFPS) { reportedPhysicsFPS = 60.0f; } DataModel* dm = DataModel::get(this); if (dm) { int placeID = dm->getPlaceID(); if (realPhysicsFPS > 100.0) { static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(flag, boost::bind(&RobloxGoogleAnalytics::trackEvent, GA_CATEGORY_GAME, "ElevatedPhysicsFPSDetected_100", boost::lexical_cast(placeID).c_str(), 0, false)); } else if (realPhysicsFPS > 90.0) { static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(flag, boost::bind(&RobloxGoogleAnalytics::trackEvent, GA_CATEGORY_GAME, "ElevatedPhysicsFPSDetected_90", boost::lexical_cast(placeID).c_str(), 0, false)); } else if (realPhysicsFPS > 80.0) { static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(flag, boost::bind(&RobloxGoogleAnalytics::trackEvent, GA_CATEGORY_GAME, "ElevatedPhysicsFPSDetected_80", boost::lexical_cast(placeID).c_str(), 0, false)); } else if (realPhysicsFPS > 70.0) { static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(flag, boost::bind(&RobloxGoogleAnalytics::trackEvent, GA_CATEGORY_GAME, "ElevatedPhysicsFPSDetected_70", boost::lexical_cast(placeID).c_str(), 0, false)); } else if (realPhysicsFPS > 65.0) { static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(flag, boost::bind(&RobloxGoogleAnalytics::trackEvent, GA_CATEGORY_GAME, "ElevatedPhysicsFPSDetected_65", boost::lexical_cast(placeID).c_str(), 0, false)); } else { static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(flag, boost::bind(&RobloxGoogleAnalytics::trackEvent, GA_CATEGORY_GAME, "ElevatedPhysicsFPSDetected_SetMinimum", boost::lexical_cast(placeID).c_str(), 0, false)); } } } return reportedPhysicsFPS; } int Workspace::getPhysicsThrottling(void) { return (int)(100 * getWorld()->getEnvironmentSpeed()); } int Workspace::getNumAwakeParts(void) { Kernel* kernel = getWorld()->getKernel(); int awakePartCount = kernel->numFreeFallBodies() + kernel->numContactBodies() + kernel->numJointBodies() + kernel->numRealTimeConnectors() + kernel->numLeafBodies(); return awakePartCount; } void Workspace::render3dAdorn(Adorn* adorn) { // Hack - viewport is updated here viewPort = adorn->getViewport(); // Standard Adornment for all items that have adornment adornableCollector->render3dAdornItems(adorn); if (Workspace::showWorldCoordinateFrame) { adorn->setObjectToWorldMatrix( CoordinateFrame() ); adorn->axes( G3D::Color3::red(), G3D::Color3::green(), G3D::Color3::blue(), 50.0); } if (Workspace::showHashGrid) { Vector3 gridLow = 4.0 * Vector3(7,1,3); Vector3 gridHigh = gridLow + Vector3(4,4,4); adorn->setObjectToWorldMatrix(CoordinateFrame()); adorn->box( AABox(gridLow, gridHigh) ); } Network::Player* localPlayer = Network::Players::findLocalPlayer(this); if(show3DGrid && (!localPlayer || Network::Players::isCloudEdit(this))) RBX::DrawAdorn::zeroPlaneGrid(adorn, *getCamera(), gridSizeModifier, 0.05, Color3(0.3f,0.3f,0.3f), Color3(0.4f,0.4f,0.4f)); if(showAxisWidget) RBX::DrawAdorn::axisWidget(adorn, *getCamera()); } void Workspace::append3dSortedAdorn(std::vector& sortedAdorn) { adornableCollector->append3dSortedAdornItems(sortedAdorn, getConstCamera()); } bool Workspace::hasModalGuiObjects() { bool isModal = false; if (Network::Player* player = Network::Players::findLocalPlayer(this)) if (PlayerGui* playerGui = player->findFirstChildOfType()) isModal = playerGui->findModalGuiObject(); if(!isModal) { shared_ptr coreGuiService = shared_from(ServiceProvider::find(this)); if(coreGuiService) isModal = coreGuiService->findModalGuiObject(); } return isModal; } void Workspace::requestFirstPersonCamera(bool firstPersonOn, bool cameraTransitioning, int controlMode) { ControllerService* service = ServiceProvider::find(this); if (!service) return; UserInputBase* userInput = service->getHardwareDevice(); if (!userInput) return; UserInputService* userInputService = ServiceProvider::find(this); if (!userInputService) return; if (getConstCamera()->getCameraType() == Camera::LOCKED_CAMERA) // let go of mouse pan if we're in LOCKED_CAMERA mode { inRightMousePan = false; userInputService->setMouseWrapMode(UserInputService::WRAP_AUTO); } else if( ( (firstPersonOn && !cameraTransitioning) || RBX::GameBasicSettings::singleton().mouseLockedInMouseLockMode() ) && !hasModalGuiObjects() ) { userInput->centerCursor(); userInputService->setMouseWrapMode(UserInputService::WRAP_CENTER); } else if(RBX::GameBasicSettings::singleton().inHybridMode() && !inRightMousePan && !inMiddleMouseTrack) userInputService->setMouseWrapMode(UserInputService::WRAP_HYBRID); else { if (!RBX::GameBasicSettings::singleton().inMousepanMode() && !inRightMousePan && !inMiddleMouseTrack) userInputService->setMouseWrapMode(UserInputService::WRAP_AUTO); } } void Workspace::setMiddleMouseTrack() { if (this->getConstCamera()->getCameraType() != Camera::LOCKED_CAMERA) { inMiddleMouseTrack = true; if(UserInputService* userInputService = ServiceProvider::find(this)) userInputService->setMouseWrapMode(UserInputService::WRAP_CENTER); } } void Workspace::cancelMiddleMouseTrack() { inMiddleMouseTrack = false; if(!firstPersonCam) // don't change this if we are current in first person (Still need to wrap the same) if(UserInputService* userInputService = ServiceProvider::find(this)) userInputService->setMouseWrapMode(UserInputService::WRAP_AUTO); } void Workspace::setRightMousePan() { if (FFlag::LuaControlsDisableMouse2Lock) { Camera::CameraType camType = this->getConstCamera()->getCameraType(); if (camType != Camera::LOCKED_CAMERA && camType != Camera::CUSTOM_CAMERA) { inRightMousePan = true; if(UserInputService* userInputService = ServiceProvider::find(this)) { userInputService->setMouseWrapMode(UserInputService::WRAP_CENTER); } } } else if (this->getConstCamera()->getCameraType() != Camera::LOCKED_CAMERA) { inRightMousePan = true; if(UserInputService* userInputService = ServiceProvider::find(this)) { userInputService->setMouseWrapMode(UserInputService::WRAP_CENTER); } } } void Workspace::cancelRightMousePan() { inRightMousePan = false; if(UserInputService* userInputService = ServiceProvider::find(this)) { if(!firstPersonCam) // don't change this if we are current in first person (Still need to wrap the same) userInputService->setMouseWrapMode(UserInputService::WRAP_AUTO); } } GuiResponse Workspace::handleSurfaceGui(const shared_ptr& event) { Camera* cam = this->getCamera(); if (SurfaceGui::numInstances() && event->isMouseEvent() && cam) { RBX::Network::Player* player = Network::Players::findLocalPlayer(this); // 'Tool punch-though concept': surfaceGUI is clickable if the character is not wielding a tool // --or-- if the character has a tool active and he is within a certain distance of the SG. bool ignoreSGDist = !(player && player->getCharacter() && player->getCharacter()->findFirstChildOfType()); Instances ignore; // Ignore terrain to avoid expensive raycasts - terrain never has SG objects if (Instance* terrain = getTerrain()) ignore.push_back(shared_from(terrain)); // The ray sometimes hits the character's hat when in first person mode, preventing us from clicking anything on SGs // first person is set in lua, so check distance float distanceToCharacter = (cam->getCameraFocus().translation - cam->getCameraCoordinateFrame().translation).magnitude(); if (player && player->getCharacter() && distanceToCharacter <= 0.5f) { ignore.push_back(shared_from(player->getCharacter())); } // Cast a ray to get the first intersecting part Vector2 p = event->get2DPosition(); Ray ray = cam->worldRay(p.x, p.y); ray.direction() *= 1000.f; GeometryService* geometryService = ServiceProvider::create(this); RBXASSERT(geometryService); shared_ptr part; Vector3 surfaceNormal; PartMaterial surfaceMaterial; Vector3 point = geometryService->getHitLocationPartFilterDescendents(&ignore, ray, part, surfaceNormal, surfaceMaterial, false, false); // Find the SG object on the hit surface SurfaceGui* sg = 0; if (part) { int faceId = 0; Surface surf = part->getSurface(ray, faceId); if (surf.getPartInstance()) sg = SurfaceGui::findSurfaceGui(part.get(), surf.getNormalId()); } // Send various unfocus events to the last active SG if necessary if (SurfaceGui* last = lastSurfaceGUI.lock().get()) if (sg != last && DataModel::get(last)) // if it's inside the dataModel last->unProcess(); lastSurfaceGUI = shared_from(sg); // Send the current event to the found SG if (sg) { GuiResponse resp = sg->process3d(event, point, ignoreSGDist); if (FFlag::GamepadCursorChanges) { return resp; } else { if (resp.wasSunk()) return resp; } } } return GuiResponse::notSunk(); } GuiResponse Workspace::process(const shared_ptr& event) { FASTLOG1(FLog::MouseCommand, "Workspace::Process, eventType: %u", event->getUserInputType()); DataModel *dataModel = (DataModel*)ServiceProvider::findServiceProvider(this); Plugin *activePlugin = PluginManager::singleton()->getActivePlugin(dataModel); if (activePlugin) { FASTLOG1(FLog::UserInputProfile, "Passing event to plugin mouse, plugin: %p", activePlugin); activePlugin->getMouse()->update(event); } GuiResponse resp = handleSurfaceGui(event); if( resp.wasSunk() ) return resp; RBXASSERT(currentCommand.get() != NULL); shared_ptr processingCommand = currentCommand; if (event->isMouseEvent()) // copy last mouse position { idleMouseEvent = Creatable::create(*event); idleMouseEvent->setInputType(InputObject::TYPE_MOUSEIDLE); } switch (event->getUserInputType()) { default: { FASTLOG(FLog::MouseCommand, "Return GuiResponse::notSunk"); return GuiResponse::notSunk(); } case InputObject::TYPE_FOCUS: { cancelRightMousePan(); break; } case InputObject::TYPE_KEYBOARD: { RBXASSERT(event->isKeyDownEvent() || event->isKeyUpEvent()); if (event->isKeyDownEvent()) { FASTLOG(FLog::MouseCommand, "Handling Key up"); processingCommand->onPeekKeyDown(event); // for Hopper keyboard object if (processingCommand->captured()) { setMouseCommand(processingCommand->onKeyDown(event)); break; } else return GuiResponse::notSunk(); } else if(event->isKeyUpEvent()) { FASTLOG(FLog::MouseCommand, "Handling Key up"); processingCommand->onPeekKeyUp(event); // for Hopper keyboard object if (processingCommand->captured()) { setMouseCommand(processingCommand->onKeyUp(event)); break; } else return GuiResponse::notSunk(); } break; } case InputObject::TYPE_MOUSEBUTTON2: { RBXASSERT(event->isRightMouseDownEvent() || event->isRightMouseUpEvent()); if (event->isRightMouseDownEvent()) { setRightMousePan(); if (processingCommand->captured()) // ignore - re-entrant from window break; setMouseCommand(processingCommand->onRightMouseDown(event)); } else if(event->isRightMouseUpEvent()) { FASTLOG(FLog::UserInputProfile, "Canceling Right Mouse pan"); cancelRightMousePan(); #ifdef STUDIO_CAMERA_CONTROL_SHORTCUTS if( Camera* camera = getCamera() ) { FASTLOG(FLog::UserInputProfile, "Pushing camera history"); camera->pushCameraHistoryStack(); } #endif FASTLOG1(FLog::UserInputProfile, "Passing right up to processing command: %p", processingCommand.get()); setMouseCommand(processingCommand->onRightMouseUp(event)); } break; } case InputObject::TYPE_MOUSEBUTTON1: { RBXASSERT(event->isLeftMouseDownEvent() || event->isLeftMouseUpEvent()); if(event->isLeftMouseDownEvent()) { leftMouseDown = true; if (processingCommand->captured()) // ignore - re-entrant from window break; FASTLOG1(FLog::MouseCommand, "Processing Mouse down on %p:", processingCommand.get()); shared_ptr mousedownCommand = processingCommand->onMouseDown(event); setMouseCommand(mousedownCommand); } else if(event->isLeftMouseUpEvent()) { FASTLOG1(FLog::UserInputProfile, "Passing left up to processing command: %p", processingCommand.get()); leftMouseDown = false; FASTLOG1(FLog::MouseCommand, "Processing Mouse up on %p:", processingCommand.get()); shared_ptr mouseupCommand = processingCommand->onMouseUp(event); setMouseCommand(mouseupCommand); } break; } case InputObject::TYPE_MOUSEBUTTON3: { if(event->isMiddleMouseDownEvent()) { setMiddleMouseTrack(); break; } else if (event->isMiddleMouseUpEvent()) { cancelMiddleMouseTrack(); #ifdef STUDIO_CAMERA_CONTROL_SHORTCUTS if( Camera* camera = getCamera() ) { FASTLOG(FLog::UserInputProfile, "Pushing camera history"); camera->pushCameraHistoryStack(); } #endif break; } } case InputObject::TYPE_MOUSEWHEEL: { RBXASSERT(event->isMouseWheelBackward() || event->isMouseWheelForward()); if (event->isMouseWheelForward()) processingCommand->onMouseWheelForward(event); else if(event->isMouseWheelBackward()) processingCommand->onMouseWheelBackward(event); break; } case InputObject::TYPE_MOUSEMOVEMENT: { if (processingCommand->captured()) { FASTLOG1(FLog::UserInputProfile, "Passing mouse move to processing command: %p", processingCommand.get()); processingCommand->onMouseMove(event); } else { FASTLOG1(FLog::UserInputProfile, "Passing mouse hover to processing command: %p", processingCommand.get()); processingCommand->onMouseHover(event); } break; } case InputObject::TYPE_MOUSEDELTA: { if (processingCommand->captured()) processingCommand->onMouseDelta(event); break; } } FASTLOG(FLog::UserInputProfile, "Done with workspace process"); FASTLOG1(FLog::MouseCommand, "Workspace::Process finish, captured: %u", processingCommand->captured()); RBXASSERT(processingCommand.get() != NULL); if (processingCommand->captured()) { return GuiResponse::sunkWithTarget(this); } else { return GuiResponse::sunk(); } } void Workspace::setNetworkStreamingEnabled(bool value) { bool changed = value != networkStreamingEnabled; networkStreamingEnabled = value; if (changed && Network::Players::isCloudEdit(this)) { raisePropertyChanged(prop_StreamingEnabled); } } void Workspace::setExperimentalSolverEnabled(bool value) { if (experimentalSolverEnabled != value) { experimentalSolverEnabled = value; raiseChanged(prop_ExperimentalSolverEnabled); } if (expSolverEnabled_Replicate != experimentalSolverEnabled) { expSolverEnabled_Replicate = experimentalSolverEnabled; raiseChanged(prop_ExpSolverEnabled_Replicate); } } void Workspace::setExpSolverEnabled_Replicate(bool value) { if (expSolverEnabled_Replicate != value) { expSolverEnabled_Replicate = value; raiseChanged(prop_ExpSolverEnabled_Replicate); } if (experimentalSolverEnabled != expSolverEnabled_Replicate) { experimentalSolverEnabled = expSolverEnabled_Replicate; raiseChanged(prop_ExperimentalSolverEnabled); } getWorld()->setUsingPGSSolver(expSolverEnabled_Replicate && FFlag::UsePGSSolver); // Master switch used for turning on the PGS solver regardless of the Workspace toggle or code FFlag - for testing purposes getWorld()->setUsingPGSSolver(getWorld()->getUsingPGSSolver() || FFlag::PGSAlwaysActiveMasterSwitch); if (expSolverEnabled_Replicate && FFlag::UsePGSSolver) { DataModel* dm = DataModel::get(this); if (dm) { int placeID = dm->getPlaceID(); static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(flag, boost::bind(&RobloxGoogleAnalytics::trackEvent, GA_CATEGORY_GAME, "PGSSolverActivated", boost::lexical_cast(placeID).c_str(), 0, false)); } } } // Calling this function CallOnce prevents unecessary logic from being run // multiple times after the call once is expired. void callGAForPhysicalProperties(const DataModel* dm, PhysicalPropertiesMode mode) { int placeID = dm->getPlaceID(); std::string gaMessage = "PhysicalPropertiesMode_"; if (mode == PhysicalPropertiesMode_Legacy) { gaMessage += "Legacy"; } else if (mode == PhysicalPropertiesMode_Default) { gaMessage += "Default"; } else { gaMessage += "New"; } RobloxGoogleAnalytics::trackEvent( GA_CATEGORY_GAME, gaMessage.c_str(), boost::lexical_cast(placeID).c_str()); } bool Workspace::getUsingNewPhysicalProperties() const { if (DFFlag::TrackPhysicalPropertiesGA) { if (const DataModel* dm = DataModel::get(this)) { static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(flag, boost::bind(&callGAForPhysicalProperties, dm, getWorld()->getPhysicalPropertiesMode())); } } return getWorld()->getUsingNewPhysicalProperties(); } void Workspace::setPhysicalPropertiesMode(PhysicalPropertiesMode mode) { RunService* rs = ServiceProvider::find(this); if (getWorld()->getPhysicalPropertiesMode() != mode) { // Prevents the flag from being flipping while in RunTime if (rs && rs->getRunState() != RS_RUNNING && rs->getRunState() != RS_PAUSED) { getWorld()->setPhysicalPropertiesMode(mode); raiseChanged(prop_physicalPropertiesMode); } else { RBX::StandardOut::singleton()->printf(MESSAGE_WARNING, "Cannot change PhysicalPropertiesMode during Runtime"); } } } void Workspace::setPhysicalPropertiesModeNoEvents(PhysicalPropertiesMode mode) { if (getWorld()->getPhysicalPropertiesMode() != mode) { getWorld()->setPhysicalPropertiesMode(mode); } } PhysicalPropertiesMode Workspace::getPhysicalPropertiesMode() const { return getWorld()->getPhysicalPropertiesMode(); } // A bit dangerous, but we know the runService is here during the onServiceProvider call class WorkspaceStatsItem : public Stats::Item { public: WorkspaceStatsItem() { setName("Workspace"); } static shared_ptr create(const Workspace* workspace, const World* world, const RunService* runService) { shared_ptr result = Creatable::create(); result->createChildItem("FPS", boost::bind(&RunService::smoothFps, runService)); result->createChildItem("Environment Speed %", boost::bind(&World::getEnvironmentSpeedPercent, world)); Stats::Item* pDataModelStep = result->createBoundChildItem(*workspace->profileDataModelStep); Stats::Item* pWorkspaceStep = pDataModelStep->createBoundChildItem(*workspace->profileWorkspaceStep); Stats::Item* pWorldStep = pWorkspaceStep->createBoundChildItem(world->getProfileWorldStep()); std::vector worldProfilers; world->loadProfilers(worldProfilers); for (size_t i = 0; i < worldProfilers.size(); ++i) { pWorldStep->createBoundChildItem(*worldProfilers[i]); } pDataModelStep->createBoundChildItem(*workspace->profileWorkspaceAssemble); Stats::Item* w = result->createChildItem("World"); w->createChildItem("Primitives", boost::bind(&World::getNumPrimitives, world)); w->createChildItem("Joints", boost::bind(&World::getNumJoints, world)); w->createChildItem("Contacts", boost::bind(&World::getNumContacts, world)); Stats::Item* contacts = result->createChildItem("Contacts"); contacts->createChildItem("CtctStageCtcts", boost::bind(&World::getMetric, world, IWorldStage::NUM_CONTACTSTAGE_CONTACTS)); contacts->createChildItem("SteppingCtcts", boost::bind(&World::getMetric, world, IWorldStage::NUM_STEPPING_CONTACTS)); contacts->createChildItem("TouchingCtcts", boost::bind(&World::getMetric, world, IWorldStage::NUM_TOUCHING_CONTACTS)); contacts->createChildItem("MaxTreeDepth", boost::bind(&World::getMetric, world, IWorldStage::MAX_TREE_DEPTH)); contacts->createChildItem("# link(p)", boost::bind(&World::getNumLinkCalls, world)); contacts->createChildItem("Hash Nodes Out", boost::bind(&World::getNumHashNodes, world)); contacts->createChildItem("Max Bucket Size", boost::bind(&World::getMaxBucketSize, world)); Stats::Item* kernelItem = result->createChildItem("Kernel"); const Kernel* kernel = world->getKernel(); // First two are here for fun and to throw off the competition kernelItem->createChildItem("SolverIterations", boost::bind(&Kernel::fakeDeceptiveSolverIterations, kernel)); kernelItem->createChildItem("MatrixSize", boost::bind(&Kernel::fakeDeceptiveMatrixSize, kernel)); // kernelItem->createChildItem("projJobs", boost::bind(&Kernel::numProjectileBodies, kernel)); kernelItem->createChildItem("Bodies", boost::bind(&Kernel::numBodies, kernel)); kernelItem->createChildItem("Constraints", boost::bind(&Kernel::numConnectors, kernel)); kernelItem->createChildItem("Points", boost::bind(&Kernel::numPoints, kernel)); Stats::Item* file = result->createChildItem("File Operations"); file->createChildItem("Total Load Time", boost::bind(&Workspace::getStatsFileTimeTotal, workspace)); file->createChildItem("SyncHttpGet Time", boost::bind(&Workspace::getStatsSyncHttpGetTime, workspace)); file->createChildItem("XML Load Time", boost::bind(&Workspace::getStatsXMLLoadTime, workspace)); file->createChildItem("Join All Time", boost::bind(&Workspace::getStatsJoinAllTime, workspace)); return result; } }; void Workspace::onServiceProvider(ServiceProvider* oldProvider, ServiceProvider* newProvider) { if (oldProvider) { heartbeatConnection.disconnect(); setDefaultMouseCommandConnection.disconnect(); if (statsItem) { statsItem->setParent(NULL); statsItem.reset(); } } Super::onServiceProvider(oldProvider, newProvider); if (newProvider) { RunService* runService = ServiceProvider::create(newProvider); RBXASSERT(runService); heartbeatConnection = runService->heartbeatSignal.connect(boost::bind(&Workspace::onHeartbeat, this, _1)); Stats::StatsService* stats = ServiceProvider::create(newProvider); if (stats) { statsItem = WorkspaceStatsItem::create(this, world.get(), runService); statsItem->setParent(stats); } setDefaultMouseCommandConnection = PluginManager::singleton()->allPluginsDeactivatedSignal.connect(boost::bind(&Workspace::setDefaultMouseCommand, this)); } } /* When scripts run in the workspace BaseScript: Backend Processing (runs backend) LocalScript: In local character (runs local) */ bool Workspace::scriptShouldRun(BaseScript* script) { RBXASSERT(isAncestorOf(script)); bool answer = false; if (script->fastDynamicCast()) { { //Either we're in the old mode (LocalScripts run on Client), or this script is eligible to be run ClientSide ModelInstance* character = Network::Players::findLocalCharacter(this); answer = (character && script->isDescendantOf(character)); if(answer){ script->setLocalPlayer(shared_from(Network::Players::getPlayerFromCharacter(character))); } } } else { answer = Network::Players::backendProcessing(this); } return answer; } std::size_t hash_value(const TouchPair& p) { std::size_t result = boost::hash()(p.p1.get()); boost::hash_combine(result, p.p2.get()); boost::hash_combine(result, p.type); return result; } bool Workspace::experimentalSolverIsEnabled() { return getWorld()->getKernel()->getUsingPGSSolver(); } float Workspace::getFallenPartDestroyHeight() const { return getWorld()->getFallenPartDestroyHeight(); } void Workspace::setFallenPartDestroyHeight(float value) { value = G3D::clamp(value, -50000.0f, 50000.0f); if (getWorld()->getFallenPartDestroyHeight() != value) { getWorld()->setFallenPartDestroyHeight(value); raisePropertyChanged(prop_fallenPartDestroyHeight); } } ////////////////////// // // Physics analyzer // shared_ptr Workspace::getPhysicsAnalyzerIssue( int group ) { shared_ptr< Instances > instances(new Instances); if( !getPhysicsAnalyzerBreakOnIssue() ) { return instances; } const auto& islands = getWorld()->getKernel()->pgsSolver.getInconsistentBodies(); if( islands.size() > ( size_t )group ) { const auto& bodies = islands[ group ]; for( size_t i = 0; i < bodies.size(); i++ ) { boost::uint64_t id = bodies[ i ]; auto* prim = getWorld()->getPrimitiveFromBodyUID( id ); shared_ptr< Instance > partInstance = shared_from( static_cast( PartInstance::fromPrimitive(prim) ) ); instances->push_back( partInstance ); } } return instances; } void Workspace::setPhysicsAnalyzerBreakOnIssue( bool enable ) { if( getExperimentalSolverEnabled() ) { getWorld()->getKernel()->pgsSolver.setPhysicsAnalyzerBreakOnIssue( enable ); } } bool Workspace::getPhysicsAnalyzerBreakOnIssue( ) { if( getExperimentalSolverEnabled() ) { return getWorld()->getKernel()->pgsSolver.getPhysicsAnalyzerBreakOnIssue( ); } return false; } } // namespace