/* Copyright 2003-2006 ROBLOX Corporation, All Rights Reserved */ #include "Server.h" #include "Network/API.h" #include "Network/NetworkClusterPacketCache.h" #include "Network/Players.h" #include "ConcurrentRakPeer.h" #include "NetworkSettings.h" #include "NetworkOwnerJob.h" #include "ServerReplicator.h" #include "Util.h" #include "Script/ModuleScript.h" #include "Script/script.h" #include "Util/http.h" #include "Util/RobloxGoogleAnalytics.h" #include "Util/Statistics.h" #include "Util/SoundService.h" #include "V8DataModel/DataModel.h" #include "V8DataModel/partinstance.h" #include "V8DataModel/Workspace.h" // TODO - move distributed physics switch somewhere else #include "V8DataModel/message.h" #include "V8DataModel/MegaCluster.h" #include "V8datamodel/TimerService.h" #include "V8World/Assembly.h" #include "V8World/Mechanism.h" #include "V8World/Primitive.h" #include "RakPeer.h" #include "GetTime.h" #include #include #if !defined(_WIN32) #include #include #include #endif #include "FastLog.h" #include "script/LuaVM.h" DYNAMIC_LOGGROUP(NetworkJoin) FASTFLAG(DebugLocalRccServerConnection) DYNAMIC_FASTFLAG(DebugDisableTimeoutDisconnect) DYNAMIC_FASTFLAGVARIABLE(RCCSupportCloudEdit, false) DYNAMIC_FASTFLAGVARIABLE(CloudEditGARespectsThrottling, false) DYNAMIC_FASTFLAGVARIABLE(CloudEditCheckClientPresent, false) using namespace RBX; using namespace Network; using namespace RakNet; const char* const Network::sServer = "NetworkServer"; REFLECTION_BEGIN(); static Reflection::BoundFuncDesc server_startFunction(&Server::start, "Start", "port", 0, "threadSleepTime", 20, Security::Plugin); static Reflection::BoundFuncDesc f_disconnect(&Server::stop, "Stop", "blockDuration", 1000, Security::LocalUser); static Reflection::BoundFuncDesc f_GetClientCount(&Server::getClientCount, "GetClientCount", Security::LocalUser); static Reflection::PropDescriptor prop_Port("Port", category_Data, &Server::getPort, NULL); static Reflection::BoundFuncDesc func_SetIsPlayerAuthenticationRequired(&Server::setIsPlayerAuthenticationRequired, "SetIsPlayerAuthenticationRequired", "value", Security::Roblox); static Reflection::BoundFuncDesc func_ConfigureAsCloudEditServer(&Server::configureAsCloudEditServer, "ConfigureAsCloudEditServer", Security::Roblox); static Reflection::EventDesc, FilterResult, shared_ptr, std::string)> desc_dataBasicFiltered(&Server::dataBasicFilteredSignal, "DataBasicFiltered", "peer", "result", "instance", "member", Security::LocalUser); static Reflection::EventDesc, FilterResult, shared_ptr, std::string)> desc_dataCustomFiltered(&Server::dataCustomFilteredSignal, "DataCustomFiltered", "peer", "result", "instance", "member", Security::LocalUser); Reflection::EventDesc)> event_IncommingConnection(&Server::incommingConnectionSignal, "IncommingConnection", "peer", "replicator", Security::RobloxScript); REFLECTION_END(); static const int maxClients = 128; static shared_ptr createReplicator(RakNet::SystemAddress a, Network::Server* s, NetworkSettings* networkSettings) { // Creates an ordinary ServerReplicator without security or cheat handling code return Creatable::create(a, s, networkSettings); } boost::function(RakNet::SystemAddress, Server*, NetworkSettings*)> Server::createReplicator = ::createReplicator; // allowedSecuirtyVersions is modified from a RCCService thread, // we need to protect it with a mutex because this list is checked every time a new client joins the server. static std::vector allowedSecurityVersions; static boost::mutex securityVersionsMutex; struct Accumulator { float total; int num; Accumulator() : total(0), num(0) {} void add(float value) {total += value; num++;} float getAvgValue() {return total / num;} }; static void reportServerStats(weak_ptr server) { shared_ptr sharedServer = server.lock(); if (!sharedServer) return; DataModel* dm = DataModel::get(sharedServer.get()); if (!dm) return; double totalKBytesSendPerSec = 0; double totalDataBytesSendPerSec = 0; double totalPhysicsBytesSendPerSec = 0; int numPlayers = 0; typedef boost::unordered_map PacketLossPercentByPlatforms; PacketLossPercentByPlatforms packetLossPercentByPlatforms; if (sharedServer->getChildren()) { Instances::const_iterator end = sharedServer->getChildren()->end(); for (Instances::const_iterator iter = sharedServer->getChildren()->begin(); iter != end; ++iter) { if (ServerReplicator* rep = Instance::fastDynamicCast(iter->get())) { if (Player* player = rep->getRemotePlayer()) { // log stats only for player that has been in game for more then 5 mins int elapsedTime = (RakNet::GetTimeUS() - rep->stats().peerStats.rakStats.connectionStartTime) / 1e6f; if (elapsedTime > 5 * 60) { std::string osPlatform = player->getOsPlatform(); const ReplicatorStats& stats = rep->stats(); totalKBytesSendPerSec += stats.kiloBytesSentPerSecond; totalDataBytesSendPerSec += stats.dataPacketsSent.rate() * stats.dataPacketsSentSize.value(); totalPhysicsBytesSendPerSec += stats.physicsSenderStats.physicsPacketsSent.rate() * stats.physicsSenderStats.physicsPacketsSentSize.value(); numPlayers++; packetLossPercentByPlatforms[osPlatform].add(rep->stats().peerStats.maxPacketloss); } } } } } if (totalKBytesSendPerSec) { Analytics::EphemeralCounter::reportStats("ServerBytesSentPerSec", totalKBytesSendPerSec); Analytics::EphemeralCounter::reportStats("ServerDataBytesSentPerSec", totalDataBytesSendPerSec); Analytics::EphemeralCounter::reportStats("ServerPhysicsBytesSentPerSec", totalPhysicsBytesSendPerSec); Analytics::EphemeralCounter::reportStats("ServerBytesSentPerSecPerPlayer", totalKBytesSendPerSec / numPlayers); Analytics::EphemeralCounter::reportStats("ServerDataBytesSentPerSecPerPlayer", totalDataBytesSendPerSec / numPlayers); Analytics::EphemeralCounter::reportStats("ServerPhysicsBytesSentPerSecPerPlayer", totalPhysicsBytesSendPerSec / numPlayers); } for (PacketLossPercentByPlatforms::iterator i = packetLossPercentByPlatforms.begin(); i != packetLossPercentByPlatforms.end(); i++) { Analytics::EphemeralCounter::reportStats("ServerPacketLossPercent_"+i->first, i->second.getAvgValue()); } dm->create()->delay(boost::bind(&reportServerStats, server), 10*60); } Server::Server(void) :outgoingPort(0) , isPlayerAuthenticationRequired(false) , networkSettings(&NetworkSettings::singleton()) , isCloudEditServer(false) { Security::Context::current().requirePermission(Security::Plugin, "create a NetworkServer"); setName(sServer); //Allow empty script to always come in registerLegalScript(""); scriptsByCurrentBytecode[""] = 0; scriptsByLegacyBytecode[""] = 0; FASTLOG(FLog::Network, "NetworkServer:Create"); } Server::~Server(void) { FASTLOG(FLog::Network, "NetworkServer:Destroy"); } bool Server::serverIsPresent(const Instance* context, bool testInDatamodel) { const ServiceProvider* serviceProvider = ServiceProvider::findServiceProvider(context); RBXASSERT(!testInDatamodel || serviceProvider!=NULL); return ServiceProvider::find(serviceProvider)!=NULL; } std::vector Server::getAllIPv4Addresses() { std::vector ipAddresses; #if defined(_WIN32) WORD wVersionRequested; WSADATA wsaData; char name[255]; PHOSTENT hostinfo; wVersionRequested = MAKEWORD( 1, 1 ); char *ip; if ( WSAStartup( wVersionRequested, &wsaData ) == 0 ) { if( gethostname ( name, sizeof(name)) == 0) { if((hostinfo = gethostbyname(name)) != NULL) { int nCount = 0; while(hostinfo->h_addr_list[nCount]) { ip = inet_ntoa(*( struct in_addr *)hostinfo->h_addr_list[nCount]); ipAddresses.push_back(std::string(ip)); nCount++; } } } } // ips that we get here are only external, // so add a local address as well // server can't connect to same machine clients otherwise ipAddresses.push_back("127.0.0.1"); #else struct ifaddrs * ifAddrStruct = NULL; struct ifaddrs * ifa = NULL; void * tmpAddrPtr = NULL; getifaddrs(&ifAddrStruct); for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) { if (!ifa->ifa_addr) { continue; } if (ifa->ifa_addr->sa_family == AF_INET) { tmpAddrPtr=&((struct sockaddr_in *)ifa->ifa_addr)->sin_addr; char addressBuffer[INET_ADDRSTRLEN]; inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN); ipAddresses.push_back(std::string(addressBuffer)); } } if (ifAddrStruct!=NULL) freeifaddrs(ifAddrStruct); #endif return ipAddresses; } void Server::start(int port, int threadSleepTime) { if (DFFlag::CloudEditCheckClientPresent && Players::clientIsPresent(this)) throw RBX::runtime_error("Can not call server, client is present."); FASTLOG(FLog::Network, "NetworkServer:Start"); StartupResult res = STARTUP_OTHER_FAILURE; std::vector addresses; #ifdef RBX_STUDIO_BUILD addresses = Server::getAllIPv4Addresses(); shared_ptr sdArray(new RakNet::SocketDescriptor[addresses.size()]); for (unsigned int i = 0; i < addresses.size(); i++) { sdArray.get()[i].port = port; strcpy(sdArray.get()[i].hostAddress, addresses[i].c_str()); } res = rakPeer->rawPeer()->Startup(maxClients, sdArray.get(), addresses.size()); #else RakNet::SocketDescriptor d(port, 0); res = rakPeer->rawPeer()->Startup(maxClients, &d, 1); #endif if (res != RakNet::RAKNET_STARTED) throw std::runtime_error(RBX::format("Failed to start network server, id %d", res)); #ifdef RBX_STUDIO_BUILD for (unsigned int i = 0; i < addresses.size(); i++) { StandardOut::singleton()->printf(MESSAGE_SENSITIVE,"Started network server on %s|%i",addresses[i].c_str(),port); } #else RakNet::SystemAddress address = rakPeer->rawPeer()->GetMyBoundAddress(); outgoingPort = address.GetPort(); StandardOut::singleton()->printf(MESSAGE_SENSITIVE, "Started network server %s", RakNetAddressToString(address).c_str()); #endif DataModel *dataModel = DataModel::get(this); int startupMillis = static_cast((Time::nowFast() - dataModel->getDataModelInitTime()).msec()); RobloxGoogleAnalytics::trackUserTiming(GA_CATEGORY_GAME, "ServerStartTime", startupMillis); if(DFFlag::DebugDisableTimeoutDisconnect) rakPeer->rawPeer()->SetTimeoutTime(10*60*1000, UNASSIGNED_SYSTEM_ADDRESS); dataModel->create()->delay(boost::bind(&reportServerStats, weak_from(this)), 5 * 60); } static bool isReplicator(shared_ptr instance) { return Instance::fastDynamicCast(instance.get())!=NULL; } int Server::getClientCount() { if (DFFlag::CloudEditCheckClientPresent && Players::clientIsPresent(this)) throw RBX::runtime_error("Can not call server, client is present."); if (getChildren()) return std::count_if(getChildren()->begin(), getChildren()->end(), &isReplicator); else return 0; } void Server::stop(int blockDuration) { if (DFFlag::CloudEditCheckClientPresent && Players::clientIsPresent(this)) throw RBX::runtime_error("Can not call server, client is present."); FASTLOG1(FLog::Network, "NetworkServer:Stop blockDuration(%d)", blockDuration); // The following line will remove the Replicators // we have to do this first before shutting down rakpeer because replicator might hold // a list of unprocessed packets that was allocated from a pool inside rakpeer. rakpeer // clears this pool in shutdown. this->visitChildren(boost::bind(&Instance::unlockParent, _1)); this->removeAllChildren(); if (rakPeer->rawPeer()->IsActive()) rakPeer->rawPeer()->Shutdown(blockDuration); } static void reportCloudEditGA(const char* label, int value = 0) { if (DFFlag::CloudEditGARespectsThrottling) { RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "CloudEdit", label, value); } else { RobloxGoogleAnalytics::trackEventWithoutThrottling(GA_CATEGORY_GAME, "CloudEdit", label, value); } } static void reportCloudEditStats(weak_ptr server) { shared_ptr sharedServer = server.lock(); if (!sharedServer) return; DataModel* dm = DataModel::get(sharedServer.get()); if (!dm) return; RBXASSERT(dm->currentThreadHasWriteLock()); int players = -1; if (Players* p = dm->find()) { players = p->numChildren(); } reportCloudEditGA("5 Minute Usage", players); dm->create()->delay(boost::bind(&reportCloudEditStats, server), 5*60); } void Server::configureAsCloudEditServer() { if (DFFlag::CloudEditCheckClientPresent && Players::clientIsPresent(this)) throw RBX::runtime_error("Can not call server, client is present."); if (!DFFlag::RCCSupportCloudEdit) { return; } initWithCloudEditSecurity(); rakPeer->rawPeer()->SetIncomingPassword(Network::versionB.c_str(), Network::versionB.size()); isCloudEditServer = true; reportCloudEditGA("Server Start"); DataModel::get(this)->create()->delay(boost::bind(&reportCloudEditStats, weak_from(this)), 5*60); } void Server::onCreateRakPeer() { Super::onCreateRakPeer(); rakPeer->rawPeer()->SetMaximumIncomingConnections(maxClients); if (FFlag::DebugLocalRccServerConnection) { Network::versionB = "test"; } rakPeer->rawPeer()->SetIncomingPassword(Network::versionB.c_str(), Network::versionB.size()); } void Server::onServiceProvider(ServiceProvider* oldProvider, ServiceProvider* newProvider) { if (oldProvider) { TaskScheduler::singleton().remove(networkOwnerJob); networkOwnerJob.reset(); if (players) { players->setConnection(NULL); stop(); players.reset(); } itemAddedConnection.disconnect(); workspaceLoadedConnection.disconnect(); } if (newProvider && Players::clientIsPresent(newProvider)) throw RBX::runtime_error("Can not create server, client is present."); Super::onServiceProvider(oldProvider, newProvider); if (newProvider) { players = shared_from(ServiceProvider::create(newProvider)); players->setConnection(rakPeer.get()); DataModel* dataModel = boost::polymorphic_downcast(newProvider); if (networkSettings->usePhysicsPacketCache) physicsPacketCache = ServiceProvider::create(newProvider); if (networkSettings->useInstancePacketCache) instancePacketCache = ServiceProvider::create(newProvider); ServiceProvider::create(newProvider); ServiceProvider::create(newProvider); if (networkSettings->distributedPhysicsEnabled) { networkOwnerJob = shared_ptr( new NetworkOwnerJob(shared_from(dataModel) ) ); TaskScheduler::singleton().add(networkOwnerJob); } RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "PlaceID", "none", dataModel->getPlaceID()); if (0 == dataModel->getPlaceID()) { onWorkspaceLoaded(); } else { workspaceLoadedConnection = dataModel->workspaceLoadedSignal.connect(boost::bind(&Server::onWorkspaceLoaded, this)); } } } void Server::onWorkspaceLoaded() { Workspace *workspace = ServiceProvider::find(this); if (workspace->getNetworkStreamingEnabled()) RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "NetworkStreamingEnabled"); { MegaClusterInstance *megaCluster = Instance::fastDynamicCast(workspace->getTerrain()); if (megaCluster && megaCluster->isAllocated()) { char placeId[32]; sprintf(placeId, "%d", DataModel::get(this)->getPlaceID()); if (megaCluster->isSmooth()) { RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SmoothTerrain", placeId); } else { RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "LegacyTerrain", placeId); } } } StarterPlayerService* sps = ServiceProvider::create(this); if (sps) sps->recordSettingsInGA(); if (DataModel* dataModel = DataModel::get(this)) { dataModel->visitDescendants(boost::bind(&Server::onItemAdded, this, _1)); itemAddedConnection = dataModel->onDemandWrite()->descendantAddedSignal.connect(boost::bind(&Server::onItemAdded, this, _1)); } else { RBXASSERT(false); } } void Server::onItemAdded(shared_ptr item) { boost::optional scriptSource = getScriptSourceFromInstance(item.get()); if (scriptSource) registerLegalScript(*scriptSource); } boost::optional Server::getScriptSourceFromInstance(Instance* instance) const { if (const Script* script = Instance::fastDynamicCast