#include "RobloxView.h" #include "Roblox.h" #include "GfxBase/ViewBase.h" #include "v8datamodel/datamodel.h" #include "v8datamodel/workspace.h" #include "v8datamodel/camera.h" #include "v8datamodel/game.h" #include "v8datamodel/InputObject.h" #include "v8datamodel/GuiService.h" #include "FunctionMarshaller.h" #include "Util/StandardOut.h" #include "Util/FileSystem.h" #include "rbx/SystemUtil.h" #include "rbx/Tasks/Coordinator.h" #include "UserInput.h" #include "Util/IMetric.h" #include "Util/Object.h" #include "GfxBase/RenderSettings.h" #include "GfxBase/FrameRateManager.h" #include "v8datamodel/BaseRenderJob.h" #include "v8datamodel/UserController.h" #include "v8datamodel/UserInputService.h" #include "Util/Statistics.h" #include "v8datamodel/ContentProvider.h" #include "script/ScriptContext.h" #include "v8xml/Serializer.h" #include "rbx/CEvent.h" #include "GameVerbs.h" #include "Network/Players.h" #include "../ClientBase/RenderSettingsItem.h" #include #include "V8DataModel/GameBasicSettings.h" LOGGROUP(PlayerShutdownLuaTimeoutSeconds) // This job calls ViewBase::render(), which needs to be done exclusive to the DataModel. // This is why it has the RBX::DataModelJob::Render enum, which prevents concurrent writes to DataModel. // It also needs to run in the view's thread for OpenGL // TODO: Can Ogre be modified to not require the thread? class RobloxView::RenderJob : public RBX::BaseRenderJob , public RBX::IMetric { RBX::FunctionMarshaller* marshaller; weak_ptr dataModel; RBX::ViewBase* view; RBX::CEvent renderEvent; public: RenderJob(RBX::ViewBase* view, RBX::FunctionMarshaller* marshaller, shared_ptr dataModel) :RBX::BaseRenderJob(CRenderSettingsItem::singleton().getMinFrameRate(), CRenderSettingsItem::singleton().getMaxFrameRate(), dataModel), view(view), dataModel(dataModel), marshaller(marshaller), renderEvent(false) { cyclicExecutive = true; } RBX::Time::Interval sleepTime(const Stats& stats) { if (isAwake) return computeStandardSleepTime(stats, CRenderSettingsItem::singleton().getMaxFrameRate()); else return RBX::Time::Interval::max(); } static void scheduleRenderPerform(const weak_ptr& selfWeak, ViewBase* view, double timeJobStart) { if (shared_ptr self = selfWeak.lock()) { view->renderPerform(timeJobStart); self->wake(); } } void doDataModelRenderStep(DataModel* dm, const float secondsElapsed) { dm->renderStep(secondsElapsed); isAwake = false; marshaller->Execute(boost::bind(&RBX::ViewBase::renderPrepare, view, this), &renderEvent); } virtual RBX::TaskScheduler::StepResult stepDataModelJob(const Stats& stats) { DataModel* dm = view->getDataModel(); if (!dm) return RBX::TaskScheduler::Done; double timeJobStart = RBX::Time::nowFastSec(); try { const float secondsElapsed = view->getFrameRateManager()->GetFrameTimeStats().getLatest() / 1000.f; lastRenderTime = RBX::Time::now(); // TODO: Can we fix Ogre so that it can be called from this thread, rather than marshalled? RBX::DataModel::scoped_write_request request(dm); doDataModelRenderStep(dm, secondsElapsed); marshaller->Submit(boost::bind(&scheduleRenderPerform, weak_from(this), view, timeJobStart)); } catch (std::exception& e) { RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, e); } return RBX::TaskScheduler::Stepped; } void abortRender() { renderEvent.Set(); } // IMetric /*override*/ double getMetricValue(const std::string& metric) const { RBX::FrameRateManager* frm = view ? view->getFrameRateManager() : 0; if (metric == "Render FPS") { return averageStepsPerSecond(); } if (metric == "Render Duty") { return averageDutyCycle(); } if (metric == "Render Nominal FPS") { return frm ? 1000.0 / frm->GetRenderTimeAverage() : 0.0; } if(metric == "Delta Between Renders") { return view->getMetricValue(metric); } else if (metric == "Total Render") { return view->getMetricValue(metric); } else if (metric == "Present Time") { return view->getMetricValue(metric); } else if (metric == "GPU Delay") { return view->getMetricValue(metric); } else if (metric == "Video Memory") { return (double)RBX::SystemUtil::getVideoMemory(); } return 0.0; } /*override*/ std::string getMetric(const std::string& metric) const { if (! view ) { return "No View"; } if (metric == "Graphics Mode") { return ""; // RBX::Reflection::EnumDesc::singleton().convertToString(graphicsMode); } RBX::FrameRateManager* frm = view ? view->getFrameRateManager() : 0; if (metric == "FRM") { return (frm && frm->IsBlockCullingEnabled()) ? "On" : "Off"; } if (metric == "Anti-Aliasing") { return (frm && frm->getAntialiasingMode() == RBX::CRenderSettings::AntialiasingOn) ? "On" : "Off"; } RBXASSERT(0); return ""; } }; class RobloxView::UserInputJob : public RBX::DataModelJob { RobloxView* wnd; RBX::DataModel* dataModel; public: UserInputJob(RobloxView* wnd, shared_ptr dataModel) :RBX::DataModelJob("UserInput", RBX::DataModelJob::Write, true, dataModel, RBX::Time::Interval(0)), wnd(wnd), dataModel(dataModel.get()) { } RBX::Time::Interval sleepTime(const Stats& stats) { return computeStandardSleepTime(stats, 60); } virtual Job::Error error(const Stats& stats) { Job::Error result = computeStandardError(stats, 60); return result; } virtual RBX::TaskScheduler::StepResult stepDataModelJob(const Stats& stats) { RBX::DataModel::scoped_write_request request(dataModel); if(wnd) wnd->processInput(); return RBX::TaskScheduler::Stepped; } }; static RBX::InputObject::UserInputType viewEventToUserInputType(RobloxView::EventType event) { switch (event) { case RobloxView::MOUSE_MOVE: return RBX::InputObject::TYPE_MOUSEMOVEMENT; case RobloxView::MOUSE_LEFT_BUTTON_DOWN: case RobloxView::MOUSE_LEFT_BUTTON_UP: // intentional fall thru return RBX::InputObject::TYPE_MOUSEBUTTON1; case RobloxView::MOUSE_RIGHT_BUTTON_DOWN: case RobloxView::MOUSE_RIGHT_BUTTON_UP: // intentional fall thru return RBX::InputObject::TYPE_MOUSEBUTTON2; case RobloxView::MOUSE_MIDDLE_BUTTON_DOWN: case RobloxView::MOUSE_MIDDLE_BUTTON_UP: // intentional fall thru return RBX::InputObject::TYPE_MOUSEBUTTON3; case RobloxView::KEY_UP: case RobloxView::KEY_DOWN: // intentional fall thru return RBX::InputObject::TYPE_KEYBOARD; default: return RBX::InputObject::TYPE_NONE; } } static RBX::InputObject::UserInputState viewEventToUserInputState(RobloxView::EventType event) { switch (event) { case RobloxView::MOUSE_MOVE: return RBX::InputObject::INPUT_STATE_CHANGE; case RobloxView::MOUSE_LEFT_BUTTON_DOWN: case RobloxView::MOUSE_RIGHT_BUTTON_DOWN: case RobloxView::MOUSE_MIDDLE_BUTTON_DOWN: case RobloxView::KEY_DOWN: // intentional fall thru return RBX::InputObject::INPUT_STATE_BEGIN; case RobloxView::MOUSE_LEFT_BUTTON_UP: case RobloxView::MOUSE_RIGHT_BUTTON_UP: case RobloxView::MOUSE_MIDDLE_BUTTON_UP: case RobloxView::KEY_UP: // intentional fall thru return RBX::InputObject::INPUT_STATE_END; default: return RBX::InputObject::INPUT_STATE_NONE; } } void RobloxView::processInput() { /*if (userInput && userInput.get()) if(userInput->getWrapMode() == RBX::UserInputBase::WRAP_HYBRID) userInput->ProcessUserInputMessage(RBX::UIEvent::NO_EVENT,0,0);*/ } // Request a shutdown of the client Boolean RobloxView::requestShutdownClient() { if( shared_ptr dataModel = game->getDataModel() ) { // give scripts a deadline to finish if (FLog::PlayerShutdownLuaTimeoutSeconds > 0) if (ScriptContext* scriptContext = game->getDataModel()->find()) scriptContext->setTimeout(FLog::PlayerShutdownLuaTimeoutSeconds); DataModel::RequestShutdownResult requestResult = dataModel->requestShutdown(); return requestResult == DataModel::CLOSE_REQUEST_HANDLED; } return true; } void RobloxView::handleUserMessage(EventType event, unsigned int wParam, unsigned int lParam) { if (userInput) { userInput->PostUserInputMessage(viewEventToUserInputType(event), viewEventToUserInputState(event), wParam, lParam); } } void RobloxView::handleFocus(bool focus) { if (userInput) { userInput->PostUserInputMessage(RBX::InputObject::TYPE_FOCUS, focus ? RBX::InputObject::INPUT_STATE_BEGIN : RBX::InputObject::INPUT_STATE_END, 0, 0); } } void RobloxView::handleMouseInside(bool inside) { if (userInput) { if (inside) userInput->onMouseInside(); else userInput->onMouseLeave(); } } void RobloxView::handleMouse(EventType event, int x, int y, unsigned int modifiers) { if (userInput) { userInput->PostUserInputMessage(viewEventToUserInputType(event), viewEventToUserInputState(event), modifiers, MAKEXYLPARAM((uint) x, (uint) y)); } } void RobloxView::handleKey(EventType event, RBX::KeyCode keyCode, RBX::ModCode modifiers) { if (userInput) { userInput->PostUserInputMessage(viewEventToUserInputType(event), viewEventToUserInputState(event), keyCode, modifiers); } } void RobloxView::handleScrollWheel(float delta, int x, int y) { if (userInput) { userInput->PostUserInputMessage(RBX::InputObject::TYPE_MOUSEWHEEL, RBX::InputObject::INPUT_STATE_CHANGE, delta, MAKEXYLPARAM((uint) x, (uint) y)); } } void RobloxView::leaveGame() { marshaller->Submit(boost::bind(&RobloxView::handleLeaveGame, this)); } void RobloxView::handleLeaveGame() { Roblox::handleLeaveGame(appWindow); } void RobloxView::shutdownClient() { marshaller->Submit(boost::bind(&RobloxView::handleShutdownClient, this)); } void RobloxView::handleShutdownClient() { Roblox::handleShutdownClient(appWindow); } void RobloxView::toggleFullScreen() { if(getDataModel()) getDataModel()->submitTask(boost::bind(&RobloxView::handleToggleFullScreen, this), RBX::DataModelJob::Write); } void RobloxView::handleToggleFullScreen() { Roblox::handleToggleFullScreen(appWindow); RBX::GameBasicSettings::singleton().setFullScreen(Roblox::inFullScreenMode(appWindow)); } extern std::string macBundlePath(); static RBX::ViewBase* createGameWindow(void *wnd) { // static initialization: static boost::once_flag flag = BOOST_ONCE_INIT; boost::call_once(&RBX::ViewBase::InitPluginModules, flag); RBX::OSContext context; context.hWnd = wnd; context.width = 800; context.height = 600; RBX::CRenderSettings& settings = CRenderSettingsItem::singleton(); RBX::CRenderSettings::GraphicsMode mode = RBX::CRenderSettings::OpenGL; RBX::ViewBase* rbxView = RBX::ViewBase::CreateView(mode, &context, &settings); rbxView->initResources(); return rbxView; } class DummyVerb : public RBX::Verb { public: DummyVerb(VerbContainer* container, const char* name) :Verb(container, name) { } virtual bool isEnabled() const {return false;} virtual void doIt(RBX::IDataState* dataState) {} }; RobloxView::RobloxView(void *viewwnd, void *appwnd, boost::shared_ptr game) :view(createGameWindow(viewwnd)) ,appWindow(appwnd) ,viewWindow(viewwnd) ,userInput(new UserInput(this, game)) ,game(game) ,leaveGameVerb(new LeaveGameVerb(this, game->getDataModel().get())) ,fullscreenVerb(new ToggleFullscreenVerb(this, game->getDataModel().get(), NULL)) ,studioVerb(new DummyVerb(game->getDataModel().get(), "TogglePlayMode")) ,screenshotVerb(new DummyVerb(game->getDataModel().get(), "Screenshot")) ,shutdownClientVerb(new ShutdownClientVerb(this, game->getDataModel().get())) ,marshaller(RBX::FunctionMarshaller::GetWindow()) { shared_ptr dataModel = game->getDataModel(); placeIDChangeConnection = dataModel->propertyChangedSignal.connect(boost::bind(&RobloxView::onPlaceIDChanged, this, _1)); // Bind to the Workspace bindToWorkspace(); // Create the rendering jobs renderJob = shared_ptr(new RenderJob(view.get(), marshaller, dataModel)); // Create the user input job userInputJob = shared_ptr(new UserInputJob(this, dataModel)); defineConcurrencyRules(); RBX::TaskScheduler::singleton().add(renderJob); if (userInputJob) { RBX::TaskScheduler::singleton().add(userInputJob); } } void RobloxView::onPlaceIDChanged(const RBX::Reflection::PropertyDescriptor* desc) { bool placeIDChanged = desc->name=="PlaceId"; if( shared_ptr dataModel = game->getDataModel() ) if(placeIDChanged && dataModel->getPlaceID() > 0) Roblox::addBreakPadKeyValue("Place0", dataModel->getPlaceID()); } void RobloxView::defineConcurrencyRules() { { // Force viewUpdateJob and renderJob to happen serially boost::shared_ptr sequence(new RBX::Tasks::ExclusiveSequence()); renderJob->addCoordinator(sequence); } if (CRenderSettingsItem::singleton().isSynchronizedWithPhysics) { // Force rendering and physics to happen in lock-step sequence.reset(new RBX::Tasks::Sequence()); renderJob->addCoordinator(sequence); if( shared_ptr dataModel = game->getDataModel() ) dataModel->create()->getPhysicsJob()->addCoordinator(sequence); } } void RobloxView::doUnbindWorkspace() { if(view) view->bindWorkspace(boost::shared_ptr()); } void RobloxView::initializeInput() { userInput.reset(new UserInput(this, game)); if(userInput) { DataModel::LegacyLock lock(game->getDataModel(), DataModelJob::Write); ControllerService* service = ServiceProvider::create(game->getDataModel().get()); service->setHardwareDevice(userInput.get()); } } void RobloxView::stopJobs() { if (renderJob) { renderJob->abortRender(); boost::function callback = boost::bind(&RBX::FunctionMarshaller::ProcessMessages, marshaller); RBX::TaskScheduler::singleton().removeBlocking(renderJob, callback); } if (userInputJob) { boost::function callback = boost::bind(&FunctionMarshaller::ProcessMessages, marshaller); TaskScheduler::singleton().removeBlocking(userInputJob, callback); } // RenderJob is sure to be completed at this point, since removeBlocking returned - but it might have marshalled // renderPerform asynchronously before exiting, which means that we might still have a callback that uses this view // in the marshaller queue. // This makes sure that all pending marshalled events are processed to avoid a use after free. marshaller->ProcessMessages(); // All render processing is complete; it's safe to reset job pointers now renderJob.reset(); userInputJob.reset(); } void RobloxView::doShutdownDataModel() { if(game) { { RBX::DataModel::LegacyLock lock(game->getDataModel(), RBX::DataModelJob::Write); marshaller->Submit(boost::bind(&RobloxView::doUnbindWorkspace, this)); marshaller->ProcessMessages(); if(shared_ptr dataModel = game->getDataModel()) { if (RBX::RunService* runService = dataModel->find()) runService->stopTasks(); if(RBX::ControllerService* service = RBX::ServiceProvider::create(dataModel.get())) service->setHardwareDevice(NULL); } if (sequence) { if( shared_ptr dataModel = game->getDataModel() ) if (RBX::RunService* rs = dataModel->find()) rs->getPhysicsJob()->removeCoordinator(sequence); } stopJobs(); marshaller->ProcessMessages(); if( shared_ptr dataModel = game->getDataModel() ) { userInput.reset(); leaveGameVerb.reset(); fullscreenVerb.reset(); studioVerb.reset(); screenshotVerb.reset(); shutdownClientVerb.reset(); } game->shutdown(); } game->shutdown(); } // marshaller has not finished unbinding workspace, wait until this is finished if(view->getDataModel()) { while (view->getDataModel()) { sleep(0.016667f); } } RBXASSERT(!view->getDataModel()); } RobloxView::~RobloxView(void) { if (sequence) { if( shared_ptr dataModel = game->getDataModel() ) if (RBX::RunService* rs = dataModel->find()) rs->getPhysicsJob()->removeCoordinator(sequence); } stopJobs(); if( shared_ptr dataModel = game->getDataModel() ) { RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write); RBX::ControllerService* service = RBX::ServiceProvider::create(dataModel.get()); service->setHardwareDevice(NULL); view->bindWorkspace(boost::shared_ptr()); userInput.reset(); leaveGameVerb.reset(); fullscreenVerb.reset(); studioVerb.reset(); screenshotVerb.reset(); shutdownClientVerb.reset(); } RBX::FunctionMarshaller::ReleaseWindow(marshaller); // First destroy the view before closing the DataModel view.reset(); Roblox::relinquishGame(game); } void RobloxView::bindToWorkspace() { shared_ptr dataModel = game->getDataModel(); if(!dataModel) return; // Note that this code needs to be thread-sensitive RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write); view->bindWorkspace(dataModel); view->buildGui(); if (userInput) { RBX::ControllerService* service = RBX::ServiceProvider::create(dataModel.get()); service->setHardwareDevice(userInput.get()); } } void RobloxView::setBounds(unsigned int width, unsigned int height) { this->width = width; this->height = height; if (view) { view->onResize(width, height); } } static void executeScript(boost::shared_ptr game, std::string urlScript, bool isApp, bool isFromProtocolHandler) { RBX::Security::Impersonator impersonate(RBX::Security::COM); std::ostringstream data; if (RBX::ContentProvider::isUrl(urlScript)) { RBX::DataModel::LegacyLock lock(game->getDataModel(), RBX::DataModelJob::Write); std::auto_ptr stream(RBX::ServiceProvider::create(game->getDataModel().get())->getContent(RBX::ContentId(urlScript.c_str()))); boost::iostreams::copy(*stream, data); } else return; // silent failure is harder to hack :) RBX::ProtectedString verifiedSource; try { verifiedSource = ProtectedString::fromTrustedSource(data.str()); ContentProvider::verifyScriptSignature(verifiedSource, true); } catch(std::bad_alloc&) { throw; } catch(std::exception&) { return; } shared_ptr dm = game->getDataModel(); if(!dm) return; RBX::DataModel::LegacyLock lock(dm, RBX::DataModelJob::Write); if (dm->isClosed()) return; if (urlScript.find("join.ashx")) { std::string data = verifiedSource.getSource(); int firstNewLineIndex = data.find("\r\n"); if (data[firstNewLineIndex+2] == '{') { // TODO: create shared enum between PC and Mac // Values taken from SharedLauncher::LaunchMode int launchMode = 0; if (isFromProtocolHandler) launchMode = 1; game->configurePlayer(RBX::Security::COM, data.substr(firstNewLineIndex+2), launchMode); return; } } RBX::ScriptContext* context = dm->create(); context->executeInNewThread(RBX::Security::COM, verifiedSource, "Start Game"); } boost::shared_ptr RobloxView::startGame(std::string urlScript, const bool isApp) { boost::shared_ptr game(Roblox::getpreloadedGame(isApp)); boost::thread(boost::bind(&executeScript, game, urlScript, isApp, false)); return game; } RobloxView *RobloxView::start_game(void *wnd, void *appwnd, std::string urlScript, const bool isApp) { boost::shared_ptr game = startGame(urlScript, isApp); return new RobloxView(wnd, appwnd, game); } RobloxView *RobloxView::init_game(void *wnd, void* appwnd, const bool isApp) { boost::shared_ptr game(Roblox::getpreloadedGame(isApp)); return new RobloxView(wnd, appwnd, game); } void RobloxView::executeJoinScript(const std::string& urlScript, const bool isApp, const bool isFromProtocolHandler) { boost::thread(boost::bind(&executeScript, game, urlScript, isApp, isFromProtocolHandler)); } void RobloxView::setUIMessage(const std::string& message) { if (shared_ptr dm = game->getDataModel()) dm->submitTask(boost::bind(&RobloxView::setUIMessage_, this, message), DataModelJob::Write); } void RobloxView::setUIMessage_(const std::string& message) { if (shared_ptr dm = game->getDataModel()) { if (message.length() > 0) { dm->setUiMessage(message); } else { dm->clearUiMessage(); } if (GuiService*gs = dm->create()) gs->setUiMessage(GuiService::UIMESSAGE_INFO, message); } }