/* Copyright 2003-2007 ROBLOX Corporation, All Rights Reserved */ #include "Network/Players.h" #include "Network/Player.h" #include "V8DataModel/FriendService.h" #include "Replicator.h" #include "GuidRegistryService.h" #include "Client.h" #include "Server.h" #include "Streaming.h" #include "Util.h" #include "util/RobloxGoogleAnalytics.h" #include "WebChatFilter.h" #include "RakPeerInterface.h" #include "PacketIds.h" #include "BitStream.h" #include "RakNet/Source/PluginInterface2.h" #include "ConcurrentRakPeer.h" #include "PhysicsSender.h" #include "PhysicsReceiver.h" #include "Humanoid/Humanoid.h" #include "V8DataModel/CharacterAppearance.h" #include "V8DataModel/DataModelMesh.h" #include "V8DataModel/Decal.h" #include "V8DataModel/Backpack.h" #include "V8DataModel/DebugSettings.h" #include "V8DataModel/GameSettings.h" #include "V8DataModel/Hopper.h" #include "V8DataModel/InsertService.h" #include "V8DataModel/Modelinstance.h" #include "V8DataModel/PlayerGui.h" #include "V8DataModel/SafeChat.h" #include "V8DataModel/Stats.h" #include "V8DataModel/Teams.h" #include "V8DataModel/Workspace.h" #include "V8DataModel/ReplicatedFirst.h" #include "v8datamodel/HttpRbxApiService.h" #include "Util/LuaWebService.h" #include "Util/SystemAddress.h" #include "V8Xml/WebParser.h" #include "V8World/DistributedPhysics.h" #include "Util/Statistics.h" #include "Script/ScriptContext.h" #include "Script/Script.h" #include "V8Xml/XmlSerializer.h" #include "Util/SafeToLower.h" #include "rbx/RbxDbgInfo.h" #include "Util/http.h" #include "Util/Region2.h" #include "Network/NetworkOwner.h" #include #include const char* const RBX::Network::sPlayers = "Players"; static const char* const kPlaceIdUrlParamFormat = "&serverplaceid=%d"; using namespace RBX; using namespace RBX::Network; using namespace RakNet; LOGGROUP(GoldenHashes) LOGGROUP(Network) LOGGROUP(US14116) FASTFLAG(DebugLocalRccServerConnection) DYNAMIC_FASTFLAG(RCCSupportCloudEdit) DYNAMIC_LOGGROUP(WebChatFiltering) DYNAMIC_FASTFLAGVARIABLE(GetCharacterAppearanceEnabled, false) DYNAMIC_FASTFLAGVARIABLE(CreatePlayerGuiLocal, false) DYNAMIC_FASTFLAGVARIABLE(FirePlayerAddedAndPlayerRemovingOnClient, false) FASTSTRINGVARIABLE(GetUserIdUrl, "users/get-by-username?username=%s") FASTSTRINGVARIABLE(GetUserNameUrl, "users/%i") FASTSTRINGVARIABLE(GetFriendsUrl, "%susers/%i/friends") DYNAMIC_FASTFLAGVARIABLE(LoadGuisWithoutChar, false) DYNAMIC_FASTFLAGVARIABLE(FilterInvalidWhisper, true) DYNAMIC_FASTFLAGVARIABLE(CloudEditSupportPlayersKickAndShutdown, true) REFLECTION_BEGIN(); static Reflection::PropDescriptor propPlayerCount("NumPlayers", category_Data, &Players::getNumPlayers, NULL, Reflection::PropertyDescriptor::UI); static Reflection::PropDescriptor depPlayerCount("numPlayers", category_Data, &Players::getNumPlayers, NULL, Reflection::PropertyDescriptor::Attributes::deprecated(propPlayerCount, Reflection::PropertyDescriptor::HIDDEN_SCRIPTING)); static Reflection::PropDescriptor propPlayerMaxCount("MaxPlayers", category_Data, &Players::getMaxPlayers, NULL, Reflection::PropertyDescriptor::UI); static Reflection::PropDescriptor propPlayerMaxCountInternal("MaxPlayersInternal", category_Data, &Players::getMaxPlayers, &Players::setMaxPlayers, Reflection::PropertyDescriptor::STANDARD, Security::LocalUser); static Reflection::PropDescriptor propPlayerPreferredCount("PreferredPlayers", category_Data, &Players::getPreferredPlayers, NULL, Reflection::PropertyDescriptor::UI); static Reflection::PropDescriptor propPlayerPreferredCountInternal("PreferredPlayersInternal", category_Data, &Players::getPreferredPlayers, &Players::setPreferredPlayers, Reflection::PropertyDescriptor::STANDARD, Security::LocalUser); Reflection::RefPropDescriptor Players::propLocalPlayer("LocalPlayer", category_Data, &Players::getLocalPlayerDangerous, NULL, Reflection::PropertyDescriptor::UI); Reflection::RefPropDescriptor dep_propLocalPlayer("localPlayer", category_Data, &Players::getLocalPlayerDangerous, NULL, Reflection::PropertyDescriptor::Attributes::deprecated(Players::propLocalPlayer, Reflection::PropertyDescriptor::HIDDEN_SCRIPTING)); static Reflection::BoundFuncDesc(int)> func_GetPlayerByID(&Players::getPlayerInstanceByID, "GetPlayerById", "userId", Security::LocalUser); static Reflection::BoundFuncDesc(int)> func_GetPlayerByIdDeprecated(&Players::getPlayerInstanceByID, "GetPlayerByID", "userID", Security::LocalUser, Reflection::PropertyDescriptor::Attributes::deprecated(func_GetPlayerByID)); Reflection::PropDescriptor Players::propCharacterAutoSpawn("CharacterAutoLoads", category_Behavior, &Players::getCharacterAutoSpawnProperty, &Players::setCharacterAutoSpawnProperty, Reflection::PropertyDescriptor::STANDARD_NO_REPLICATE); static Reflection::BoundFuncDesc funcChat(&Players::chat, "Chat", "message", Security::Plugin); static Reflection::BoundFuncDesc funcTeamChat(&Players::teamChat, "TeamChat", "message", Security::Plugin); static Reflection::BoundFuncDesc)> funcPlayerChat(&Players::whisperChat, "WhisperChat", "message", "player", Security::LocalUser); static Reflection::EventDesc, std::string, shared_ptr)> event_Chatted(&Players::playerChattedSignal, "PlayerChatted", "chatType", "player", "message", "targetPlayer", Security::LocalUser); static Reflection::EventDesc event_GameAnnounce(&Players::gameAnnounceSignal, "GameAnnounce", "message", Security::RobloxScript); static Reflection::BoundFuncDesc, std::string, std::string)> funcReportAbuse(&Players::reportAbuseLua, "ReportAbuse", "player", "reason", "optionalMessage", Security::LocalUser); static Reflection::BoundFuncDesc()> func_GetPlayers(&Players::getPlayers, "GetPlayers", Security::None); static Reflection::BoundFuncDesc()> dep_players(&Players::getPlayers, "players", Security::None, Reflection::Descriptor::Attributes::deprecated(func_GetPlayers)); static Reflection::BoundFuncDesc()> dep_getPlayers(&Players::getPlayers, "getPlayers", Security::None, Reflection::Descriptor::Attributes::deprecated(func_GetPlayers)); static Reflection::BoundFuncDesc(int, bool)> func_createLocalPlayer(&Players::createLocalPlayer, "CreateLocalPlayer", "userId", "isTeleport", false, Security::Plugin); static Reflection::BoundFuncDesc func_setAbuseReportUrl(&Players::setAbuseReportUrl, "SetAbuseReportUrl", "url", Security::Roblox); static Reflection::BoundFuncDesc func_setChatFilterUrl(&Players::setChatFilterUrl, "SetChatFilterUrl", "url", Security::Roblox); static Reflection::BoundFuncDesc func_setBuildUserPermissionsUrl(&Players::setBuildUserPermissionsUrl, "SetBuildUserPermissionsUrl", "url", Security::Roblox); static Reflection::BoundFuncDesc(shared_ptr)> func_playerFromCharacter(&Players::playerFromCharacter, "GetPlayerFromCharacter", "character", Security::None); static Reflection::BoundFuncDesc(shared_ptr)> func_playerFromCharacterOld(&Players::playerFromCharacter, "playerFromCharacter", "character", Security::None, Reflection::Descriptor::Attributes::deprecated(func_playerFromCharacter)); static Reflection::BoundFuncDesc(shared_ptr)> dep_playerFromCharacter(&Players::playerFromCharacter, "getPlayerFromCharacter", "character", Security::None, Reflection::Descriptor::Attributes::deprecated(func_playerFromCharacter)); static Reflection::EventDesc, shared_ptr, FriendService::FriendEventType)> event_FriendRequestEvent(&Players::friendRequestEvent, "FriendRequestEvent", "player", "player", "friendRequestEvent", Security::RobloxScript); static Reflection::EventDesc)> event_PlayerAddedEarly(&Players::playerAddedEarlySignal, "PlayerAddedEarly", "player", Security::LocalUser); static Reflection::EventDesc)> event_PlayerAdded(&Players::playerAddedSignal, "PlayerAdded", "player"); static Reflection::EventDesc)> event_PlayerRemoving(&Players::playerRemovingSignal, "PlayerRemoving", "player"); static Reflection::EventDesc)> event_PlayerRemovingLate(&Players::playerRemovingLateSignal, "PlayerRemovingLate", "player", Security::LocalUser); static Reflection::BoundFuncDesc func_setSysStatsUrl(&Players::setSysStatsUrl, "SetSysStatsUrl", "url", Security::LocalUser); static Reflection::BoundFuncDesc func_setSysHash(&Players::setSysHash, "SetSysStatsUrlId", "urlId", Security::LocalUser); static Reflection::BoundFuncDesc func_setLoadDataUrl(&Players::setLoadDataUrl, "SetLoadDataUrl", "url", Security::LocalUser); static Reflection::BoundFuncDesc func_setSaveDataUrl(&Players::setSaveDataUrl, "SetSaveDataUrl", "url", Security::LocalUser); static Reflection::BoundFuncDesc func_setSaveLeaderboardDataUrl(&Players::setSaveLeaderboardDataUrl, "SetSaveLeaderboardDataUrl", "url", Security::LocalUser); static Reflection::BoundFuncDesc func_addLeaderboardKey(&Players::addLeaderboardKey, "AddLeaderboardKey", "key", Security::LocalUser); static Reflection::BoundFuncDesc func_setChatOption(&Players::setChatOption, "SetChatStyle", "style", Players::CLASSIC_CHAT, Security::Plugin); Reflection::PropDescriptor propClassicChat("ClassicChat", category_Data, &Players::getClassicChat, NULL, Reflection::PropertyDescriptor::UI); Reflection::PropDescriptor propBubbleChat("BubbleChat", category_Data, &Players::getBubbleChat, NULL, Reflection::PropertyDescriptor::UI); static Reflection::BoundFuncDesc func_coreScriptHealthBar(&Players::getUseCoreScriptHealthBar, "GetUseCoreScriptHealthBar", Security::RobloxScript); // Chat Ignore Support static Reflection::RemoteEventDesc event_serverFinishedBlockUser(&Players::blockUserFinishedFromServerSignal, "BlockedRequestFinishedSignal", "blockerUserId", "blockeeUserId", "didDoRequest", Security::Roblox, Reflection::RemoteEventCommon::REPLICATE_ONLY, Reflection::RemoteEventCommon::BROADCAST); static Reflection::RemoteEventDesc event_blockUserFromClient(&Players::blockUserRequestFromClientSignal, "BlockUserSignal", "blockerUserId", "blockeeUserId", "blocking", Security::Roblox, Reflection::RemoteEventCommon::REPLICATE_ONLY, Reflection::RemoteEventCommon::CLIENT_SERVER); static Reflection::BoundYieldFuncDesc func_blockUser(&Players::blockUser, "BlockUser", "blockerUserId", "blockeeUserId", Security::RobloxScript); static Reflection::BoundYieldFuncDesc func_unblockUser(&Players::unblockUser, "UnblockUser", "exblockerUserId", "exblockeeUserId", Security::RobloxScript); //UserName to userId, userId to UserName static Reflection::BoundYieldFuncDesc func_getUserIdFromName(&Players::getUserIdFromName, "GetUserIdFromNameAsync", "userName", Security::None); static Reflection::BoundYieldFuncDesc func_getNameFromUserId(&Players::getNameFromUserId, "GetNameFromUserIdAsync", "userId", Security::None); static Reflection::BoundYieldFuncDesc(int)> func_getFriends(&Players::getFriends, "GetFriendsAsync", "userId", Security::None); static Reflection::BoundYieldFuncDesc(int)> func_getCharacterAppearance(&Players::getCharacterAppearance, "GetCharacterAppearanceAsync", "userId", Security::None); static Reflection::BoundFuncDesc(int)> func_getPlayerByUserId(&Players::getPlayerInstanceByID, "GetPlayerByUserId", "userId", Security::None); Reflection::RemoteEventDesc Players::event_requestCloudEditKick( &Players::requestCloudEditKick, "RequestCloudEditKick", "playerId", Security::Roblox, Reflection::RemoteEventCommon::REPLICATE_ONLY, Reflection::RemoteEventCommon::CLIENT_SERVER); Reflection::RemoteEventDesc Players::event_requestCloudEditShutdown( &Players::requestCloudEditShutdown, "RequestCloudEditShutdown", Security::Roblox, Reflection::RemoteEventCommon::REPLICATE_ONLY, Reflection::RemoteEventCommon::CLIENT_SERVER); REFLECTION_END(); std::set Players::goldenHashes; boost::mutex Players::goldenHashesMutex; bool Players::canKickBecauseRunningInRealGameServer = false; MemHashConfigs Players::goldMemHashes; boost::mutex Players::goldMemHashesMutex; namespace { shared_ptr buildBasicChatBitStream( const Instance* sender, const Instance* receiver, unsigned char chatType) { // serialize our data, changing the message to the filtered message shared_ptr data(new RakNet::BitStream()); *data << chatType; if (sender) { FASTLOGS(DFLog::WebChatFiltering, "senderGUID: %s", sender->getGuid().readableString()); RBX::Guid::Data guid; sender->getGuid().extract(guid); *data << guid.scope; *data << guid.index; } if (receiver) { FASTLOGS(DFLog::WebChatFiltering, "senderGUID: %s", receiver->getGuid().readableString()); RBX::Guid::Data guid; receiver->getGuid().extract(guid); *data << guid.scope; *data << guid.index; } return data; } } bool Players::isNetworkClient(Instance* instance) { return Instance::fastDynamicCast(instance) != NULL; } Players::Players() :rakPeer(NULL) , maxPlayers(12) , sysStatsUrl("") , players(Instances()) // Initialize to a non-NULL value , chatOption(CLASSIC_CHAT) , characterAutoSpawn(true) , testPlayerNameId(0) , testPlayerUserId(0) { this->setName("Players"); } template bool valueTableGet(shared_ptr valueTable, const std::string& key, T* outValue) { Reflection::ValueTable::const_iterator it = valueTable->find(key); if (it == valueTable->end() || !it->second.isType()) { return false; } *outValue = it->second.cast(); return true; } void Players::getUserIdFromName(std::string userName, boost::function resumeFunction, boost::function errorFunction) { if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find(this)) { apiService->getAsync(format(FString::GetUserIdUrl.c_str(), userName.c_str()), true, RBX::PRIORITY_DEFAULT, boost::bind(&Players::onReceivedRawGetUserIdSuccess, weak_from(DataModel::get(this)), _1, resumeFunction, errorFunction), boost::bind(&Players::onReceivedRawGetUserIdError, weak_from(DataModel::get(this)), _1, errorFunction)); } } void Players::onReceivedRawGetUserIdSuccess(weak_ptr weakDataModel, std::string response, boost::function resumeFunction, boost::function errorFunction) { if (shared_ptr sharedDataModel = weakDataModel.lock()) { shared_ptr table; std::string errorMessage; if (LuaWebService::parseWebJSONResponseHelper(&response, NULL, table, errorMessage)) { int id; if (!valueTableGet(table, "Id", &id)) { errorFunction("Players:GetUserIdFromName() failed because the user does not exist"); } else { resumeFunction(id); } } else { errorFunction(format("Players:GetUserIdFromName() failed because %s", errorMessage.c_str())); } } else { errorFunction("Players:GetUserIdFromName() failed because of an unknown error."); } } void Players::onReceivedRawGetUserIdError(weak_ptr weakDataModel, std::string error, boost::function errorFunction) { if (shared_ptr sharedDataModel = weakDataModel.lock()) { if (!error.empty()) { errorFunction(format("Players:GetUserIdFromName() failed because %s", error.c_str())); } else { errorFunction("Players:GetUserIdFromName() failed because of an unknown error."); } } else { errorFunction("Players:GetUserIdFromName() failed because of an unknown error."); } } void Players::getNameFromUserId(int userId, boost::function resumeFunction, boost::function errorFunction) { if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find(this)) { apiService->getAsync(format(FString::GetUserNameUrl.c_str(), userId), true, RBX::PRIORITY_DEFAULT, boost::bind(&Players::onReceivedRawGetUserNameSuccess, weak_from(DataModel::get(this)), _1, resumeFunction, errorFunction), boost::bind(&Players::onReceivedRawGetUserNameError, weak_from(DataModel::get(this)), _1, errorFunction)); } } void Players::getFriends(int userId, boost::function)> resumeFunction, boost::function errorFunction) { std::string fetchUrl; ContentProvider* cp = DataModel::get(this)->find(); if (cp) { fetchUrl = format(FString::GetFriendsUrl.c_str(), cp->getApiBaseUrl().c_str(), userId); shared_ptr pagination = Creatable::create(weak_from(DataModel::get(this)), fetchUrl); pagination->fetchNextChunk(boost::bind(resumeFunction, pagination), errorFunction); } else { errorFunction("Content Provider not found"); } } void Players::setAppearanceParent(shared_ptr model, weak_ptr instance) { shared_ptr i(instance.lock()); if (!i) return; if (Instance::fastDynamicCast(i.get()) || Instance::fastDynamicCast(i.get()) || Instance::fastDynamicCast(i.get())) { i->setParent(model.get()); } else if (dynamic_cast(i.get())) { if (!i->fastDynamicCast()) { i->setParent(model.get()); } } } void Players::doLoadAppearance(AsyncHttpQueue::RequestResult result, shared_ptr instances, std::string contentDescription, shared_ptr model , shared_ptr amountToLoad, boost::function)> resumeFunction, boost::function errorFunction) { if (amountToLoad.get()) { try { std::for_each(instances->begin(), instances->end(), boost::bind(&Players::setAppearanceParent, model, _1)); *amountToLoad.get() = *amountToLoad.get() - 1; if (*amountToLoad.get() == 0) { amountToLoad.reset(); resumeFunction(model); } } catch (RBX::base_exception& e) { amountToLoad.reset(); errorFunction(format("Players:GetCharacterAppearanceAsync() Error loading character appearance %s: %s", contentDescription.c_str(), e.what())); } } } void Players::doMakeAccoutrementRequests(std::string response, weak_ptr dataModel, shared_ptr model , boost::function)> resumeFunction, boost::function errorFunction) { std::vector strings; boost::split(strings, response, boost::is_any_of(";")); shared_ptr d(dataModel.lock()); shared_ptr amountToLoad = shared_ptr(new int); *amountToLoad.get() = (int)strings.size(); if (d) { std::string placeIdStr; placeIdStr = format(kPlaceIdUrlParamFormat, d->getPlaceID()); for (size_t i = 0; i < strings.size(); i++) { std::string contentIdString = strings[i]; contentIdString += placeIdStr; ServiceProvider::create(d.get())->loadContent( ContentId(contentIdString), ContentProvider::PRIORITY_DEFAULT, boost::bind(&Players::doLoadAppearance, _1, _2, contentIdString, model, amountToLoad, resumeFunction, errorFunction), AsyncHttpQueue::AsyncWrite ); } } } void Players::makeAccoutrementRequests(std::string* response, std::exception* err, weak_ptr dataModel, shared_ptr model, boost::function)> resumeFunction, boost::function errorFunction) { if (!response) { if (err) { errorFunction(format("Players:GetCharacterAppearanceAsync() Request exception: %s", err->what())); } else { errorFunction(format("Players:GetCharacterAppearanceAsync() Request exception")); } return; } shared_ptr d(dataModel.lock()); if (d) { try { d->submitTask(boost::bind(&Players::doMakeAccoutrementRequests, *response, dataModel, model, resumeFunction, errorFunction), DataModelJob::Write); } catch (const RBX::base_exception&) { errorFunction("Players:GetCharacterAppearanceAsync() unexpected error"); } } } void Players::getCharacterAppearance(int userId, boost::function)> resumeFunction, boost::function errorFunction) { if (!DFFlag::GetCharacterAppearanceEnabled) { errorFunction("Players:GetCharacterAppearanceAsync() is not yet enabled!"); return; } if (userId <= 0) { errorFunction("Players:GetCharacterAppearanceAsync() got negative userId"); return; } std::string characterAppearance = format("%sasset/CharacterFetch.ashx?userId=%d", ServiceProvider::create(this)->getBaseUrl().c_str(), userId); shared_ptr model = ModelInstance::createInstance(); weak_ptr dm(shared_from(Instance::fastDynamicCast(this->getRootAncestor()))); RBX::Http(characterAppearance).get(boost::bind(&makeAccoutrementRequests, _1, _2, boost::weak_ptr(dm), model, resumeFunction, errorFunction)); } void Players::onReceivedRawGetUserNameSuccess(weak_ptr weakDataModel, std::string response, boost::function resumeFunction, boost::function errorFunction) { if (shared_ptr sharedDataModel = weakDataModel.lock()) { shared_ptr table; std::string errorMessage; if (LuaWebService::parseWebJSONResponseHelper(&response, NULL, table, errorMessage)) { std::string userName; if (!valueTableGet(table, "Username", &userName)) { errorFunction("Players:GetNameFromUserId() failed because the user does not exist"); } else { resumeFunction(userName); } } else { errorFunction(format("Players:GetNameFromUserId() failed because %s", errorMessage.c_str())); } } else { errorFunction("Players:GetNameFromUserId() failed because of an unknown error."); } } void Players::onReceivedRawGetUserNameError(weak_ptr weakDataModel, std::string error, boost::function errorFunction) { if (shared_ptr sharedDataModel = weakDataModel.lock()) { if (!error.empty()) { errorFunction(format("Players:GetNameFromUserId() failed because %s", error.c_str())); } else { errorFunction("Players:GetNameFromUserId() failed because of an unknown error."); } } else { errorFunction("Players:GetNameFromUserId() failed because of an unknown error."); } } bool Players::clientIsPresent(const RBX::Instance* context, bool testInDatamodel) { return Client::clientIsPresent(context, testInDatamodel); } bool Players::serverIsPresent(const RBX::Instance* context, bool testInDatamodel) { return Server::serverIsPresent(context, testInDatamodel); } bool Players::getDistributedPhysicsEnabled() { return NetworkSettings::singleton().distributedPhysicsEnabled; } bool Players::isCloudEdit(const RBX::Instance* context) { if (Client* c = ServiceProvider::find(context)) { return c->isCloudEdit(); } else if (Server* s = ServiceProvider::find(context)) { return s->isCloudEdit(); } else { return false; } } bool Players::frontendProcessing(const RBX::Instance* context, bool testInDatamodel) { const ServiceProvider* serviceProvider = ServiceProvider::findServiceProvider(context); RBXASSERT(!testInDatamodel || serviceProvider != NULL); return serviceProvider && !serverIsPresent(context, testInDatamodel); } bool Players::backendProcessing(const RBX::Instance* context, bool testInDatamodel) { const ServiceProvider* serviceProvider = ServiceProvider::findServiceProvider(context); RBXASSERT(!testInDatamodel || serviceProvider != NULL); return serviceProvider && !clientIsPresent(context, testInDatamodel); } int Players::getPlayerCount(const RBX::Instance* context) { const ServiceProvider* serviceProvider = ServiceProvider::findServiceProvider(context); if (serviceProvider == NULL) return 0; Players* players = serviceProvider->find(); return players ? players->getNumPlayers() : 0; } RBX::SystemAddress Players::findLocalSimulatorAddress(const RBX::Instance* context) { if (!getDistributedPhysicsEnabled()) { return NetworkOwner::Unassigned(); } if (serverIsPresent(context, false)) { return NetworkOwner::Server(); } return Client::findLocalSimulatorAddress(context); } int Players::getMaxPlayers() const { return maxPlayers; } int Players::getPreferredPlayers() const { return preferredPlayers; } void Players::setMaxPlayers(int value) { if (value < 0) value = 1; if (value != maxPlayers) { maxPlayers = value; raisePropertyChanged(propPlayerMaxCount); raisePropertyChanged(propPlayerMaxCountInternal); } } void Players::setPreferredPlayers(int value) { if (value < 0) value = 1; if (value != preferredPlayers) { preferredPlayers = value; raisePropertyChanged(propPlayerPreferredCount); raisePropertyChanged(propPlayerPreferredCountInternal); } } /* id == 0 Local Player yes no Already Exists YES ignore reset(id) NO create(id) create(id) */ // only for reflection Instance* Players::getLocalPlayerDangerous() const { return localPlayer.get(); } shared_ptr Players::createLocalPlayer(int userId, bool teleportedIn) { if (localPlayer) throw std::runtime_error("Local player already exists"); { RBX::Security::Impersonator impersonate(RBX::Security::Replicator_); localPlayer = Creatable::create(); } if (RBX::DataModel* dataModel = (RBX::DataModel*)this->getParent()) dataModel->getGuiBuilder().removeSafeChatMenu(); localPlayer->setTeleportedIn(teleportedIn); { RBX::Security::Impersonator impersonate(RBX::Security::Replicator_); localPlayer->setUserId(userId); } RBX::RbxDbgInfo::s_instance.PlayerID = userId; Player::prop_OsPlatform.setValue(localPlayer.get(), DebugSettings::singleton().osPlatform()); localPlayer->setParent(this); raiseChanged(propLocalPlayer); shared_ptr starterGear = Creatable::create(); starterGear.get()->setParent(localPlayer.get()); if (localPlayer->calculatesSpawnLocationEarly()) { if (backendProcessing(this) && frontendProcessing(this)) { // play solo mode localPlayer->doFirstSpawnLocationCalculation(ServiceProvider::findServiceProvider(this), ""); } } //Visit GUI Fixes if (DFFlag::LoadGuisWithoutChar && Network::Players::frontendProcessing(this) && Network::Players::backendProcessing(this)) RBX::DataModel::get(this)->setIsGameLoaded(true); return localPlayer; } void Players::resetLocalPlayer() { if (localPlayer) { localPlayer->unlockParent(); localPlayer->destroy(); localPlayer.reset(); } } bool Players::superSafeOn() const { return (localPlayer) ? Player::prop_SuperSafeChat.getValue(localPlayer.get()) : false; } Players::~Players(void) { setConnection(NULL); } static void writeMessage(const AbuseReport::Message& msg, XmlElement* messages) { XmlElement* element = messages->addChild(RBX::Name::declare("message")); element->setValue(msg.text); element->addAttribute(RBX::Name::declare("userID"), msg.userID); element->addAttribute(RBX::Name::declare("guid"), msg.guid); } ChatMessage::ChatMessage(const char* message, ChatType chatType, shared_ptr source) :message(message) , chatType(chatType) , source(source) { Guid::generateStandardGUID(guid); } ChatMessage::ChatMessage(const ChatMessage& other, const std::string& newMessage) :message(newMessage) , guid(other.guid) , chatType(other.chatType) , source(other.source) , destination(other.destination) {} ChatMessage::ChatMessage(const char* message, ChatType chatType, shared_ptr source, shared_ptr destination) :message(message) , chatType(chatType) , source(source) , destination(destination) { Guid::generateStandardGUID(guid); } bool ChatMessage::isVisibleToPlayer(shared_ptr player) const { return isVisibleToPlayer(player, source, destination, chatType); } bool ChatMessage::isVisibleToPlayer(shared_ptr const player, shared_ptr const source, shared_ptr const destination, const ChatMessage::ChatType chatType) { switch (chatType) { case CHAT_TYPE_ALL: case CHAT_TYPE_GAME: return true; case CHAT_TYPE_TEAM: return (player && source && !source->getNeutral() && !player->getNeutral() && source->getTeamColor() == player->getTeamColor()); case CHAT_TYPE_WHISPER: return (player && (source == player || destination == player)); default: return false; } } std::string ChatMessage::getReportAbuseMessage() const { switch (chatType) { case CHAT_TYPE_ALL: return message; case CHAT_TYPE_GAME: return "[[game]]" + message; case CHAT_TYPE_TEAM: return "[[team]]" + message; case CHAT_TYPE_WHISPER: { std::ostringstream str; str << "[[to "; if (destination) { str << destination->getName(); } else { str << "???"; } str << "]]" << message; return str.str(); } default: return message; } } void AbuseReport::addMessage(shared_ptr reportingPlayer, const ChatMessage& cm) { if (messages.size() >= (size_t)GameSettings::singleton().reportAbuseChatHistory) return; if (!reportingPlayer || cm.isVisibleToPlayer(reportingPlayer)) { int userID = cm.source ? cm.source->getUserID() : 0; Message m = { userID, cm.getReportAbuseMessage(), cm.guid }; messages.push_back(m); } } AbuseReporter::AbuseReporter(std::string abuseUrl) :_data(new data()) { requestProcessor.reset(new worker_thread(boost::bind(&AbuseReporter::processRequests, _data, abuseUrl), "rbx_abusereporter")); } void AbuseReporter::add(AbuseReport& r, shared_ptr reportingPlayer, const std::list& chatHistory) { // Copy all the messages from the chat history std::for_each( chatHistory.begin(), chatHistory.end(), boost::bind(&AbuseReport::addMessage, boost::ref(r), reportingPlayer, _1) ); // Push a new request into the queue { boost::mutex::scoped_lock lock(_data->requestSync); _data->queue.push(r); } requestProcessor->wake(); } worker_thread::work_result AbuseReporter::processRequests(shared_ptr _data, std::string abuseUrl) { try { AbuseReport r; { boost::mutex::scoped_lock lock(_data->requestSync); if (_data->queue.empty()) return worker_thread::done; r = _data->queue.front(); } std::stringstream body; { TextXmlWriter writer(body); XmlElement root(RBX::Name::declare("report")); root.addAttribute(RBX::Name::declare("userID"), r.submitterID); root.addAttribute(RBX::Name::declare("placeID"), r.placeID); root.addAttribute(RBX::Name::declare("gameJobID"), r.gameJobID); root.addChild(new XmlElement(RBX::Name::declare("comment"), r.comment)); XmlElement* messages = root.addChild(new XmlElement(RBX::Name::declare("messages"))); std::for_each(r.messages.begin(), r.messages.end(), boost::bind(&writeMessage, _1, messages)); writer.serialize(&root); body.flush(); } std::string response; Http(abuseUrl).post(body, Http::kContentTypeDefaultUnspecified, false, response); StandardOut::singleton()->printf(MESSAGE_SENSITIVE, "Posted abuse report to %s", abuseUrl.c_str()); StandardOut::singleton()->print(MESSAGE_INFO, "Posted abuse report"); } catch (RBX::base_exception& e) { StandardOut::singleton()->printf(MESSAGE_SENSITIVE, "Unable to post abuse report to %s. %s", abuseUrl.c_str(), e.what()); StandardOut::singleton()->printf(MESSAGE_WARNING, "Unable to post abuse report. %s", e.what()); } { boost::mutex::scoped_lock lock(_data->requestSync); _data->queue.pop(); } return worker_thread::more; } bool Players::canReportAbuse() const { return (abuseReporter || clientIsPresent(this)); } void Players::reportAbuse(Player* player, const std::string& comment) { if (abuseReporter) { if (player) { StandardOut::singleton()->printf(MESSAGE_INFO, "Submitting abuse report on %s", player ? player->getName().c_str() : "[[[anonymous]]]"); } else { StandardOut::singleton()->printf(MESSAGE_INFO, "Submitting abuse report on game"); } AbuseReport r; r.submitterID = localPlayer ? localPlayer->getUserID() : 0; r.allegedAbuserID = player ? player->getUserID() : 0; r.comment = comment; if (RBX::DataModel* dataModel = RBX::DataModel::get(this)) { r.placeID = dataModel->getPlaceID(); r.gameJobID = dataModel->getJobId(); } abuseReporter->add(r, localPlayer, chatHistory); } else { // Send the abuse report to somebody else for processing StandardOut::singleton()->print(MESSAGE_INFO, "Sending abuse report to game server via Raknet"); if (!rakPeer) throw std::runtime_error("Can't report abuse: Not in a networked game"); shared_ptr bitStream(new RakNet::BitStream()); *bitStream << (unsigned char)ID_REPORT_ABUSE; // Write the alleged abuser (or null) *bitStream << (player ? player->getUserID() : (int)0); // Write the sender's comment *bitStream << comment; // Send ID_REPORT_ABUSE (this packet is received by the Server) rakPeer->Send(bitStream, HIGH_PRIORITY, RELIABLE, CHAT_CHANNEL, UNASSIGNED_SYSTEM_ADDRESS, true); } } void Players::reportAbuseLua(shared_ptr instance, std::string reason, std::string comment) { shared_ptr player; if (instance) { player = Instance::fastSharedDynamicCast(instance); if (!player) throw RBX::runtime_error("player must be a Player object or null"); } if (!localPlayer) throw RBX::runtime_error("You can only report-abuse from a client machine"); if (localPlayer->getUserID() > 0) { std::ostringstream strstream; strstream << reason << ";" << comment; reportAbuse(player ? player.get() : NULL, strstream.str()); } } void Players::gamechat(const std::string& message) { if (localPlayer) throw std::runtime_error("Only servers can produce game chat"); if (!rakPeer) return; shared_ptr bitStream(new RakNet::BitStream()); *bitStream << (unsigned char)ID_CHAT_GAME; *bitStream << message; rakPeer->Send(bitStream, CHAT_PRIORITY, CHAT_RELIABILITY, CHAT_CHANNEL, UNASSIGNED_SYSTEM_ADDRESS, true); } void Players::checkChat(const std::string& message) { if (!localPlayer) throw std::runtime_error("No local Player to chat from"); if (superSafeOn()) { if (message.find("/sc ") != 0) throw std::runtime_error("SuperSafe chat is on"); } } static RBX::Guid::Data readPlayerIdentifier(RakNet::BitStream& inBitStream) { RBX::Guid::Data id; inBitStream >> id.scope; inBitStream >> id.index; return id; } static void writePlayerIdentifier(RakNet::BitStream& bitStream, const boost::intrusive_ptr::Registry>& guidRegistery, shared_ptr player) { // Write the sender guidRegistery->registerGuid(player.get()); RBX::Guid::Data id; player->getGuid().extract(id); // This is very inefficient without a dictionary, but we can't // use the dictionary here - these packets go out-of-order bitStream << id.scope; bitStream << id.index; } void Players::whisperChat(std::string message, shared_ptr instance) { checkChat(message); shared_ptr player = Instance::fastSharedDynamicCast(instance); if (!player || player->getParent() != this) throw std::runtime_error("Player object is not a player to chat to"); shared_ptr bitStream(new RakNet::BitStream()); *bitStream << (unsigned char)ID_CHAT_PLAYER; // Write the sender writePlayerIdentifier(*bitStream, getGuidRegistry(), localPlayer); // Write the receiver writePlayerIdentifier(*bitStream, getGuidRegistry(), player); // Write the text *bitStream << message; // Send ID_CHAT_PLAYER rakPeer->Send(bitStream, CHAT_PRIORITY, CHAT_RELIABILITY, CHAT_CHANNEL, UNASSIGNED_SYSTEM_ADDRESS, true); ChatMessage event(message.c_str(), ChatMessage::CHAT_TYPE_WHISPER, localPlayer, player); raiseChatMessageSignal(event); } void Players::teamChat(std::string message) { checkChat(message); shared_ptr bitStream(new RakNet::BitStream()); *bitStream << (unsigned char)ID_CHAT_TEAM; // Write the sender writePlayerIdentifier(*bitStream, getGuidRegistry(), localPlayer); // Write the text *bitStream << message; // Send ID_CHAT_TEAM rakPeer->Send(bitStream, CHAT_PRIORITY, CHAT_RELIABILITY, CHAT_CHANNEL, UNASSIGNED_SYSTEM_ADDRESS, true); ChatMessage event(message.c_str(), ChatMessage::CHAT_TYPE_TEAM, localPlayer); raiseChatMessageSignal(event); } void Players::chat(std::string message) { checkChat(message); if (rakPeer) { shared_ptr bitStream(new RakNet::BitStream()); *bitStream << (unsigned char)ID_CHAT_ALL; // Write the sender writePlayerIdentifier(*bitStream, getGuidRegistry(), localPlayer); // Write the text *bitStream << message; // Send ID_CHAT_ALL rakPeer->Send(bitStream, CHAT_PRIORITY, CHAT_RELIABILITY, CHAT_CHANNEL, UNASSIGNED_SYSTEM_ADDRESS, true); } ChatMessage event(message.c_str(), ChatMessage::CHAT_TYPE_ALL, localPlayer); raiseChatMessageSignal(event); } // returns true if we were able to parse an intelligible command static bool ParseChatCommand(const ChatMessage& msg, bool isSelf, std::string* parsed_msg) { if (msg.message.size() <= 0 || msg.message[0] != '/') return false; std::vector v; boost::char_separator sep(" -_/,"); boost::tokenizer > tokens(msg.message, sep); for (boost::tokenizer > ::iterator tok_iter = tokens.begin(); tok_iter != tokens.end(); ++tok_iter) { v.push_back(*tok_iter); } if (v.size() == 0) return false; // Now have tokenized version of command // lowercase the actual command safeToLower(v[0]); // SafeChat Speak if (v[0] == "sc") { if (parsed_msg) *parsed_msg = SafeChat::singleton().getMessage(v); return true; } return false; } void Players::raisePlayerChattedSignal(const ChatMessage& message) { if (message.chatType == ChatMessage::CHAT_TYPE_ALL) { if (message.source) message.source->chattedSignal(message.message, message.destination); } if (localPlayer) { switch (message.chatType) { case ChatMessage::CHAT_TYPE_ALL: playerChattedSignal(PLAYER_CHAT_TYPE_ALL, message.source, message.message, shared_ptr()); break; case ChatMessage::CHAT_TYPE_TEAM: playerChattedSignal(PLAYER_CHAT_TYPE_TEAM, message.source, message.message, shared_ptr()); break; case ChatMessage::CHAT_TYPE_WHISPER: playerChattedSignal(PLAYER_CHAT_TYPE_WHISPER, message.source, message.message, message.destination); break; case ChatMessage::CHAT_TYPE_GAME: gameAnnounceSignal(message.message); break; } } } void Players::raiseChatMessageSignal(const ChatMessage& message) { bool fromOthers = (localPlayer == NULL) || (message.source != localPlayer); std::string msg; if (ParseChatCommand(message, !fromOthers, &msg)) { ChatMessage parsedMessage(message, msg); chatMessageSignal(parsedMessage); raisePlayerChattedSignal(parsedMessage); } else { // Check if we are using super safe chat - if so, forget about it if (localPlayer && localPlayer->getSuperSafeChat()) return; chatMessageSignal(message); raisePlayerChattedSignal(message); } } void Players::addChatMessage(const ChatMessage& message) { chatHistory.push_front(message); while (chatHistory.size() > (size_t)GameSettings::singleton().chatHistory) chatHistory.pop_back(); raiseChatMessageSignal(message); } const boost::intrusive_ptr::Registry>& Players::getGuidRegistry() { if (!guidRegistry) guidRegistry = ServiceProvider::create(this)->registry; return guidRegistry; } Players::ReceiveResult Players::OnReceiveChat(Player* sourceValidation, RakNet::RakPeerInterface* peer, RakNet::Packet* packet, unsigned char chatType) { RakNet::BitStream inBitstream(packet->data, packet->length, false); inBitstream.IgnoreBits(8); // Ignore the packet id RBX::Guid::Data sourceGuid; RBX::Guid::Data destinationGuid; shared_ptr sourceInstance; shared_ptr destinationInstance; if (chatType != ID_CHAT_GAME) { sourceGuid = readPlayerIdentifier(inBitstream); sourceInstance = getGuidRegistry()->getByGuid(sourceGuid); if (sourceValidation) { if (sourceInstance.get() != sourceValidation) { // bad data. Hacker? return Players::PLAYERS_STOP_PROCESSING_AND_DEALLOCATE; } } } if (chatType == ID_CHAT_PLAYER) { destinationGuid = readPlayerIdentifier(inBitstream); destinationInstance = getGuidRegistry()->getByGuid(destinationGuid); if (DFFlag::FilterInvalidWhisper && !destinationInstance.get()) { // bad data. Hacker? return Players::PLAYERS_STOP_PROCESSING_AND_DEALLOCATE; } } // Read message std::string message; inBitstream >> message; if (!sourceInstance) { if (chatType == ID_CHAT_GAME) { ChatMessage event(message.c_str(), ChatMessage::CHAT_TYPE_GAME, shared_from(NULL)); addChatMessage(event); } else { StandardOut::singleton()->printf(MESSAGE_SENSITIVE, "Chat message from unknown Player %s %s", RakNetAddressToString(packet->systemAddress).c_str(), message.c_str()); } } else if (sourceInstance->getParent() == this) // confirm the player hasn't been bumped { shared_ptr sourcePlayer = shared_polymorphic_downcast(sourceInstance); const bool kIsBackendProcessing = backendProcessing(this); // Check on the client (game client, not game server) to see if we should display the chat based on whether we're in the right team // NOTE: Everyone RECEIVES all chat! switch (chatType) { case ID_CHAT_ALL: { ChatMessage event(message.c_str(), ChatMessage::CHAT_TYPE_ALL, sourcePlayer); if (kIsBackendProcessing || event.isVisibleToPlayer(localPlayer)) addChatMessage(event); break; } case ID_CHAT_TEAM: { ChatMessage event(message.c_str(), ChatMessage::CHAT_TYPE_TEAM, sourcePlayer); if (kIsBackendProcessing || event.isVisibleToPlayer(localPlayer)) addChatMessage(event); break; } case ID_CHAT_PLAYER: { shared_ptr destinationPlayer; if (DFFlag::FilterInvalidWhisper) { destinationPlayer = shared_dynamic_cast(destinationInstance); if (!destinationPlayer.get()) { // bad data. Hacker? return Players::PLAYERS_STOP_PROCESSING_AND_DEALLOCATE; } } else { destinationPlayer = shared_polymorphic_downcast(destinationInstance); } ChatMessage event(message.c_str(), ChatMessage::CHAT_TYPE_WHISPER, sourcePlayer, destinationPlayer); if (kIsBackendProcessing || event.isVisibleToPlayer(localPlayer)) addChatMessage(event); break; } } // Note: This only happens on the game server // Check the message through the content filter for abusive content if (kIsBackendProcessing) { if (0 == message.length()) return Players::PLAYERS_STOP_PROCESSING_AND_DEALLOCATE; // This is our switch to allow us to turn off content filtering and check it against the local Diogenes (player's client) // TODO: The local diogenes code is ugly, we send messages to everyone and then check to see if it's not in diogenes before displaying. // I'd like to change this to the game server to avoid player's hacking their diogenes file and seeing bad text, but // the player who actually sends the message needs to be displayed it regardless (so they dont know we're filtering), and this // is done when he receives the message. So I would have to have him display it on his screen manually instead of on receive. // But then I'd have to check to see if he's received his own message to make sure he doesn't see his msgs twice... - Tim if (chatFilterUrl == "") { // Forward message to other peers peer->Send((const char*)packet->data, packet->length, CHAT_PRIORITY, CHAT_RELIABILITY, CHAT_CHANNEL, packet->systemAddress, true); return Players::PLAYERS_STOP_PROCESSING_AND_DEALLOCATE; } ChatFilter::FilteredChatMessageCallback callback = boost::bind( &Players::sendFilteredChatMessageSignalHelper, shared_from(this), packet->systemAddress, buildBasicChatBitStream(sourceInstance.get(), destinationInstance.get(), chatType), sourceInstance, shared_ptr(), _1); ChatFilter* chatFilter = ServiceProvider::create(this); chatFilter->filterMessage(sourcePlayer, destinationInstance, message, callback); return Players::PLAYERS_STOP_PROCESSING_AND_DEALLOCATE; } } return Players::PLAYERS_STOP_PROCESSING_AND_DEALLOCATE; } void Players::sendFilteredChatMessageSignalHelper(const RakNet::SystemAddress& systemAddress, const shared_ptr& baseData, const shared_ptr sourceInstance, shared_ptr chatEvent, const ChatFilter::Result& result) { DataModel::LegacyLock l(DataModel::get(this), DataModelJob::Write); sendFilteredChatMessageSignal(systemAddress, baseData, sourceInstance, result.whitelistFilteredMessage, result.blacklistFilteredMessage); } Players::ReceiveResult Players::OnReceiveReportAbuse(Player* player, RakNet::RakPeerInterface* peer, RakNet::Packet* packet) { // Read all the info RakNet::BitStream inBitstream(packet->data, packet->length, false); inBitstream.IgnoreBits(8); // Ignore the packet id AbuseReport r; r.submitterID = player ? player->getUserID() : 0; inBitstream >> r.allegedAbuserID; inBitstream >> r.comment; if (RBX::DataModel* dataModel = RBX::DataModel::get(this)) { r.placeID = dataModel->getPlaceID(); r.gameJobID = dataModel->getJobId(); } { std::ostringstream str; str << "AbuserID:" << r.allegedAbuserID << ";" << r.comment; r.comment = str.str(); } shared_ptr reportingPlayer = shared_from(player); if (abuseReporter) abuseReporter->add(r, reportingPlayer, chatHistory); abuseReportedReceived(r); return Players::PLAYERS_STOP_PROCESSING_AND_DEALLOCATE; // We handled the message. Nobody else needs to } void Players::setAbuseReportUrl(std::string value) { if (value.empty()) abuseReporter.reset(); else abuseReporter.reset(new AbuseReporter(value)); } void Players::setChatFilterUrl(std::string value) { chatFilterUrl = value; } std::string Players::getLoadDataUrl(int userId) const { if (loadDataUrl.empty()) throw std::runtime_error("No LoadData url set"); return RBX::format(loadDataUrl.c_str(), userId); } void Players::setLoadDataUrl(std::string value) { loadDataUrl = value; } std::string Players::getSaveDataUrl(int userId) const { if (saveDataUrl.empty()) throw std::runtime_error("No SaveData url set"); return RBX::format(saveDataUrl.c_str(), userId); } void Players::setSaveDataUrl(std::string value) { saveDataUrl = value; } std::string Players::getSaveLeaderboardDataUrl(int userId) const { if (saveLeaderboardDataUrl.empty()) throw std::runtime_error("No SaveLeaderboardData url set"); return RBX::format(saveLeaderboardDataUrl.c_str(), userId); } void Players::setSaveLeaderboardDataUrl(std::string value) { saveLeaderboardDataUrl = value; } void Players::addLeaderboardKey(std::string key) { if (!hasLeaderboardKey(key)) leaderboardKeys.insert(key); } bool Players::hasLeaderboardKey(const std::string& key) const { return leaderboardKeys.find(key) != leaderboardKeys.end(); } boost::unordered_set::const_iterator Players::beginLeaderboardKey() const { return leaderboardKeys.begin(); } boost::unordered_set::const_iterator Players::endLeaderboardKey() const { return leaderboardKeys.end(); } void Players::setSysHash(std::string hash) { //Check for new Mac hash embedded into the goldenHash size_t pos = hash.find(":"); if (pos != std::string::npos) { //The mac client has a different hash goldenHash = hash.substr(0, pos); goldenHash2 = hash.substr(pos + 1); } else { goldenHash2 = hash; goldenHash = hash; } FASTLOGS(FLog::GoldenHashes, "Hashses set through script, Windows hash: %s", goldenHash); FASTLOGS(FLog::GoldenHashes, "Mac hash: %s", goldenHash2); } void Players::setSysStatsUrl(std::string value) { sysStatsUrl = value; } void Players::setGoldenHashes(const std::string& windowsHash, const std::string& macHash, const std::string& windowsPlayerBetaHash) { if (windowsHash.length() > 0) goldenHash = windowsHash; if (macHash.length() > 0) goldenHash2 = macHash; if (windowsPlayerBetaHash.length() > 0) goldenHash3 = windowsPlayerBetaHash; if (goldenHash.size() > 0 || goldenHash2.size() > 0 || goldenHash3.size() > 0) canKickBecauseRunningInRealGameServer = true; FASTLOG1(FLog::GoldenHashes, "Golden hashes set, should kick: %u", canKickBecauseRunningInRealGameServer); FASTLOGS(FLog::GoldenHashes, "Windows hash: %s", goldenHash); FASTLOGS(FLog::GoldenHashes, "Mac hash: %s", goldenHash2); FASTLOGS(FLog::GoldenHashes, "WP Beta: %s ", goldenHash3); } void Players::setGoldenHashes2(const std::set& hashes) { boost::mutex::scoped_lock lock(goldenHashesMutex); goldenHashes = hashes; if (goldenHashes.size() > 0) canKickBecauseRunningInRealGameServer = true; FASTLOG1(FLog::GoldenHashes, "Golden hashes set, should kick: %u", canKickBecauseRunningInRealGameServer); for (std::set::iterator i = goldenHashes.begin(); i != goldenHashes.end(); i++) { FASTLOGS(FLog::GoldenHashes, "hash: %s", *i); } } void Players::setGoldMemHashes(const MemHashConfigs& hashes) { boost::mutex::scoped_lock lock(goldMemHashesMutex); goldMemHashes = hashes; } void Players::setBuildUserPermissionsUrl(std::string value) { buildUserPermissionsUrl = value; } bool Players::hasBuildUserPermissionsUrl() const { return !buildUserPermissionsUrl.empty(); } std::string Players::getBuildUserPermissionsUrl(int playerId) const { return format(buildUserPermissionsUrl.c_str(), playerId); } void Players::friendEventFired(int userId, int otherUserId, FriendService::FriendEventType friendEvent) { //Frontend and backend shared_ptr playerA = getPlayerByID(userId); shared_ptr playerB = getPlayerByID(otherUserId); if (playerA && playerB) { friendRequestEvent(playerA, playerB, friendEvent); } } void Players::friendStatusChanged(int userId, int otherUserId, FriendService::FriendStatus friendStatus) { shared_ptr playerA = getPlayerByID(userId); shared_ptr playerB = getPlayerByID(otherUserId); if (playerA && playerB) { playerA->onFriendStatusChanged(playerB, friendStatus); } } void Players::friendServiceRequest(bool makeFriends, weak_ptr sourcePlayer, int otherUserId) { shared_ptr playerA = sourcePlayer.lock(); shared_ptr playerB = getPlayerByID(otherUserId); if (!playerA || !playerB) return; if (RBX::FriendService* fs = ServiceProvider::find(this)) { if (!makeFriends) { fs->rejectFriendRequestOrBreakFriendship(playerA->getUserID(), playerB->getUserID()); } else { fs->issueFriendRequestOrMakeFriendship(playerA->getUserID(), playerB->getUserID()); } } } void Players::setCharacterAutoSpawnProperty(bool value) { if (value != characterAutoSpawn) { characterAutoSpawn = value; raisePropertyChanged(propCharacterAutoSpawn); } } bool Players::getShouldAutoSpawnCharacter() const { return !isCloudEdit(this) && characterAutoSpawn; } void Players::loadLocalPlayerGuis() { //Fixes issues with characterAutoSpawn not giving guis. //If characterAutoSpawn is false, and we're Filtering or Visit mode bool isVisit = (Network::Players::frontendProcessing(this) && Network::Players::backendProcessing(this)); bool isFiltering = ServiceProvider::find(this)->getNetworkFilteringEnabled(); if ((isVisit || isFiltering) && !characterAutoSpawn) { localPlayer->rebuildGui(); } } void Players::gotBlockUserSuccess(std::string response, bool blockUser, int blockerUserId, int blockeeUserId, boost::function resumeFunction, boost::function errorFunction) { std::string methodName = (blockUser ? "Players:BlockUser" : "Players:UnblockUser"); if (!response.empty()) { shared_ptr jsonResult(rbx::make_shared()); bool parseResult = WebParser::parseJSONTable(response, jsonResult); if (!parseResult) { std::string errorString = methodName + " could not parse JSON response from web."; if (!errorFunction.empty()) { errorFunction(errorString); } else { event_serverFinishedBlockUser.fireAndReplicateEvent(this, blockerUserId, blockeeUserId, errorString); } return; } Reflection::Variant successVar = jsonResult->at("success"); bool success = successVar.get(); if (!resumeFunction.empty()) { if (success) { resumeFunction(""); } else { errorFunction(methodName + " response indicating block request failed"); } return; } else { if (success) { event_serverFinishedBlockUser.fireAndReplicateEvent(this, blockerUserId, blockeeUserId, ""); } else { event_serverFinishedBlockUser.fireAndReplicateEvent(this, blockerUserId, blockeeUserId, methodName + " response indicating block request failed"); } } } else { std::string errorString = methodName + " got empty response from web."; if (!errorFunction.empty()) { errorFunction(errorString); } else { event_serverFinishedBlockUser.fireAndReplicateEvent(this, blockerUserId, blockeeUserId, errorString); } } } void Players::gotBlockUserError(std::string error, bool blockUser, int blockerUserId, int blockeeUserId, boost::function errorFunction) { const std::string methodName = (blockUser ? "Players:BlockUser" : "Players:UnblockUser"); const std::string errorString = methodName + " failed because " + error; if (!errorFunction.empty()) { errorFunction(errorString); } else { event_serverFinishedBlockUser.fireAndReplicateEvent(this, blockerUserId, blockeeUserId, errorString); } } void Players::clientReceiveBlockUserFinished(int blockerUserId, int blockeeUserId, std::string errorString) { std::pair blockerPair(blockerUserId, blockeeUserId); if (clientBlockUserMap.find(blockerPair) != clientBlockUserMap.end()) { if (errorString.empty()) { clientBlockUserMap[blockerPair].first(errorString); } else { clientBlockUserMap[blockerPair].second(errorString); } clientBlockUserMap.erase(blockerPair); return; } std::pair reverseBlockerPair(blockeeUserId, blockerUserId); if (clientBlockUserMap.find(reverseBlockerPair) != clientBlockUserMap.end()) { if (errorString.empty()) { clientBlockUserMap[blockerPair].first(errorString); } else { clientBlockUserMap[blockerPair].second(errorString); } clientBlockUserMap.erase(reverseBlockerPair); return; } } void Players::serverMakeBlockUserRequest(bool blockUser, int blockerUserId, int blockeeUserId, boost::function resumeFunction, boost::function errorFunction) { if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find(this)) { std::stringstream params; params << "blockerId=" << blockerUserId << "&blockeeId=" << blockeeUserId; std::string apiPath = blockUser ? "userblock/blockuser" : "userblock/unblockuser"; apiService->postAsync(apiPath, params.str(), true, RBX::PRIORITY_DEFAULT, RBX::HttpService::APPLICATION_URLENCODED, boost::bind(&Players::gotBlockUserSuccess, this, _1, blockUser, blockerUserId, blockeeUserId, resumeFunction, errorFunction), boost::bind(&Players::gotBlockUserError, this, _1, blockUser, blockerUserId, blockeeUserId, errorFunction)); } else { errorFunction("Players:BlockUser could not find ApiService."); return; } } void Players::internalBlockUser(int blockerUserId, int blockeeUserId, bool isBlocking, boost::function resumeFunction, boost::function errorFunction) { if (blockerUserId <= 0 || blockeeUserId <= 0) { errorFunction("Players:BlockUser or Player:UnblockUser failed because the blocker and/or blockee userId is <= 0"); return; } if (clientIsPresent(this)) { std::pair blockerPair(blockerUserId, blockeeUserId); std::pair reverseBlockerPair(blockeeUserId, blockerUserId); if (clientBlockUserMap.find(blockerPair) != clientBlockUserMap.end() || clientBlockUserMap.find(reverseBlockerPair) != clientBlockUserMap.end()) { errorFunction(RBX::format("Currently processing block request for userIds %i and %i, please wait until these requests return a value before submitting a new request.", blockerUserId, blockeeUserId)); return; } clientBlockUserMap[blockerPair] = std::pair< boost::function, boost::function >(resumeFunction, errorFunction); event_blockUserFromClient.fireAndReplicateEvent(this, blockerUserId, blockeeUserId, isBlocking); } else { serverMakeBlockUserRequest(isBlocking, blockerUserId, blockeeUserId, resumeFunction, errorFunction); } } void Players::blockUser(int blockerUserId, int blockeeUserId, boost::function resumeFunction, boost::function errorFunction) { internalBlockUser(blockerUserId, blockeeUserId, true, resumeFunction, errorFunction); } void Players::unblockUser(int blockerUserId, int blockeeUserId, boost::function resumeFunction, boost::function errorFunction) { internalBlockUser(blockerUserId, blockeeUserId, false, resumeFunction, errorFunction); } void Players::onServiceProvider(ServiceProvider* oldProvider, ServiceProvider* newProvider) { if (oldProvider) { blockUserClientSignalConnection.disconnect(); blockUserFinishedFromServerConnection.disconnect(); if (DFFlag::LoadGuisWithoutChar) loadLocalPlayerGuisConnection.disconnect(); } Super::onServiceProvider(oldProvider, newProvider); if (newProvider) { if (backendProcessing(newProvider)) { blockUserClientSignalConnection = blockUserRequestFromClientSignal.connect(boost::bind(&Players::serverMakeBlockUserRequest, this, _3, _1, _2, boost::function(), boost::function())); } if (frontendProcessing(newProvider)) { blockUserFinishedFromServerConnection = blockUserFinishedFromServerSignal.connect(boost::bind(&Players::clientReceiveBlockUserFinished, this, _1, _2, _3)); if (DFFlag::LoadGuisWithoutChar) { if (ServiceProvider::create(newProvider)) loadLocalPlayerGuisConnection = RBX::DataModel::get(newProvider)->gameLoadedSignal.connect(boost::bind(&Players::loadLocalPlayerGuis, this)); } } } } void Players::setConnection(ConcurrentRakPeer* rakPeer) { this->rakPeer = rakPeer; } bool Players::askAddChild(const Instance* instance) const { return Instance::fastDynamicCast(instance) != NULL; } ModelInstance* Players::findLocalCharacter(RBX::Instance* context) { if (Player* player = findLocalPlayer(context)) { return player->getCharacter(); } else { return NULL; } } const ModelInstance* Players::findConstLocalCharacter(const RBX::Instance* context) { if (const Player* player = findConstLocalPlayer(context)) { return player->getConstCharacter(); } else { return NULL; } } Player* Players::findLocalPlayer(Instance* context) { if (Players* players = ServiceProvider::find(context)) { return players->getLocalPlayer(); } else { return NULL; } } const Player* Players::findConstLocalPlayer(const Instance* context) { if (const Players* players = ServiceProvider::find(context)) { return players->getConstLocalPlayer(); } else { return NULL; } } shared_ptr Players::findAncestorPlayer(const RBX::Instance* descendent) { if (Players* players = ServiceProvider::find(descendent)) { //First check is the descendent is from one of our Players //Must use dynamic_cast since non-Players can be added to the Players object Player* player = Instance::fastDynamicCast(players->findFirstAncestorOf(descendent).get()); if (player) return shared_from(player); //Then check if this is an ancestor of one of our Players "Character" object (the ModelInstance in the Workspace) shared_ptr playersVector = players->getPlayers(); for (Instances::const_iterator iter = playersVector->begin(); iter != playersVector->end(); ++iter) { if ((player = boost::polymorphic_downcast(iter->get()))) { if (player->getCharacter()->isAncestorOf(descendent)) { return shared_from(player); } } } } return shared_ptr(); } shared_ptr Players::findPlayerWithAddress(const RBX::SystemAddress& playerAddress, const RBX::Instance* context) { if (Players* players = ServiceProvider::find(context)) { //First check is the descendent is from one of our Players //Must use dynamic_cast since non-Players can be added to the Players object shared_ptr playersVector = players->getPlayers(); for (Instances::const_iterator iter = playersVector->begin(); iter != playersVector->end(); ++iter) { if (shared_ptr player = Instance::fastSharedDynamicCast(*iter)) { if (player->getRemoteAddressAsRbxAddress() == playerAddress) { return player; } } } } return shared_ptr(); } shared_ptr Players::getPlayerByID(int userID) { Instances::const_iterator iter = players->begin(); Instances::const_iterator end = players->end(); for (; iter != end; ++iter) { shared_ptr player = shared_polymorphic_downcast(*iter); if (player->getUserID() == userID) return player; } return shared_ptr(); } shared_ptr Players::getPlayerInstanceByID(int userID) { return getPlayerByID(userID); } shared_ptr Players::playerFromCharacter(shared_ptr character) { if (!character) return shared_ptr(); shared_ptr myPlayers = getPlayers(); Instances::const_iterator iter = myPlayers->begin(); Instances::const_iterator end = myPlayers->end(); for (; iter != end; ++iter) { Player* player = boost::polymorphic_downcast(iter->get()); if (player->getCharacter() == character.get()) return *iter; } return shared_ptr(); } Player* Players::getPlayerFromCharacter(RBX::Instance* character) { if (Players* players = ServiceProvider::find(character)) { shared_ptr player = players->playerFromCharacter(shared_from(character)); return boost::polymorphic_downcast(player.get()); } return NULL; } void Players::onDescendantRemoving(const shared_ptr& instance) { if (Players::backendProcessing(this, false)) if (Player* player = Instance::fastDynamicCast(instance.get())) player->lockParent(); // prevent the player from coming back in (this is an exploit protection) Super::onDescendantRemoving(instance); } void Players::onChildRemoving(Instance* child) { if (Player* player = Instance::fastDynamicCast(child)) { if (FriendService* friendService = ServiceProvider::find(this)) { friendService->playerRemoving(player->getUserID()); } { boost::shared_ptr& c = players.write(); c->erase(std::find(c->begin(), c->end(), shared_from(child))); } if (DFFlag::FirePlayerAddedAndPlayerRemovingOnClient || Players::backendProcessing(this, false)) { playerRemovingSignal(shared_from(player)); playerRemovingLateSignal(shared_from(player)); } Http::playerCount = getNumPlayers(); } } static void callServerStop(DataModel* dm) { if (Server* server = dm->find()) { server->stop(); } } void Players::processRemoteEvent(const Reflection::EventDescriptor& descriptor, const Reflection::EventArguments& args, const RBX::SystemAddress& source) { if (DFFlag::CloudEditSupportPlayersKickAndShutdown && descriptor == event_requestCloudEditKick && isCloudEdit(this)) { if (args.size() == 1 && args[0].isNumber()) { int playerId = args[0].cast(); disconnectPlayer(playerId, Replicator::DisconnectReason::DisconnectReason_CloudEditKick); } } else if (DFFlag::CloudEditSupportPlayersKickAndShutdown && descriptor == event_requestCloudEditShutdown && isCloudEdit(this)) { if (DataModel* dm = DataModel::get(this)) { dm->submitTask(&callServerStop, DataModelJob::Write); } } else { Super::processRemoteEvent(descriptor, args, source); } } void Players::reportScriptSecurityError(int userId, std::string hash, std::string error, std::string stack) { } void Players::killPlayer(int userId) { Player* player = Instance::fastDynamicCast(getPlayerByID(userId).get()); if (player) { // TODO: Null check??? player->getCharacter()->findFirstChildOfType()->setHealth(0.0f); } } static void whoCaresResponse(std::string* result, std::exception* exception) { } void Players::disconnectPlayer(Instance& instance, int userId, int reason) { for (size_t i = 0; i < instance.numChildren(); ++i) { if (Replicator* proxy = Instance::fastDynamicCast(instance.getChild(i))) if (Player* player = proxy->findTargetPlayer()) if (player->getUserID() == userId) { FASTLOG(FLog::Network, "Players service requests player disconnect"); if (frontendProcessing(player)) proxy->sendDisconnectionSignal("server", false); proxy->requestDisconnect(Replicator::DisconnectReason(reason)); } } } void Players::disconnectPlayer(int userId, int reason) { if (Server* server = ServiceProvider::find(this)) { if (DataModel* dm = DataModel::get(this)) { if (reason == Replicator::DisconnectReason_LuaKick) { int placeID = dm->getPlaceID(); RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "ServerLuaKick", boost::lexical_cast(placeID).c_str()); } } disconnectPlayer(*server, userId, reason); } } void Players::disconnectPlayerLocal(int userId, int reason) { if (Client* client = ServiceProvider::find(this)) { if (DataModel* dm = DataModel::get(this)) { if (reason == Replicator::DisconnectReason_LuaKick) { int placeID = dm->getPlaceID(); RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "LocalLuaKick", boost::lexical_cast(placeID).c_str()); } } // Remove the comment at the down if you already prepared your sysstats. // disconnectPlayer(*client, userId, reason); } } bool Players::hashMatches(const std::string& hash) { if (FFlag::DebugLocalRccServerConnection) { return true; } if (DFFlag::RCCSupportCloudEdit && isCloudEdit(this)) { return true; } FASTLOGS(FLog::GoldenHashes, "Checking hash: %s", hash); boost::mutex::scoped_lock lock(goldenHashesMutex); if (goldenHashes.find(hash) != goldenHashes.end()) return true; // TODO: we might be able to remove this now if (hash.empty()) return false; std::string ios = "i"; ios += "o"; ios += "s"; ios = ios + "," + ios; // slight obfuscation if (hash == ios) return true; // what we expect from ios clients if (hash != goldenHash && hash != goldenHash2 && hash != goldenHash3) { FASTLOG(FLog::GoldenHashes, "Hash is no good"); //Their hash is no good, they failed the test return false; } return true; } unsigned int Players::checkGoldMemHashes(const std::vector& hashes) { boost::mutex::scoped_lock mtx(RBX::Network::Players::goldMemHashesMutex); unsigned int bestResult = 0; unsigned int bestErrors = 0xFFFFFFFF; for (MemHashConfigs::const_iterator configIt = goldMemHashes.begin(); configIt < goldMemHashes.end(); ++configIt) { // skip empty configs to avoid "always pass" if (configIt->empty()) { continue; } // try to find the config with the fewest errors unsigned int numErrors = 0; unsigned int result = 0; for (MemHashVector::const_iterator vecIt = configIt->begin(); vecIt < configIt->end(); ++vecIt) { // report error if out-of-bounds or different if (hashes.size() <= vecIt->checkIdx || hashes[vecIt->checkIdx] != vecIt->value) { result |= vecIt->failMask; ++numErrors; } } if (numErrors < bestErrors) { bestResult = result; bestErrors = numErrors; } // stop when something matches if (numErrors == 0) { break; } } return bestResult; } bool Players::getUseCoreScriptHealthBar() { return true; } // used to report hackers void Players::onRemoteSysStats(int userId, const std::string& stat, const std::string& message, bool desireKick) { StandardOut::singleton()->printf(MESSAGE_INFO, "Call made to SysStats! UserID: %d, Stat: %s, Message: %s", userId, stat.c_str(), message.c_str()); // logging to see if more are triggered bool willKick = desireKick && canKickBecauseRunningInRealGameServer; // make call to handler if (cheatingPlayers[userId].find(stat + message) != cheatingPlayers[userId].end()) { //Shut. It. Down. StandardOut::singleton()->printf(MESSAGE_INFO, "Players::onRemoteSysStats disconnect not in the clist"); disconnectPlayer(userId, Replicator::DisconnectReason_OnRemoteSysStats); return; } else { FASTLOGS(FLog::US14116, "onRemoteSysStats: %s", RBX::format("%d | %s | %s", userId, stat.c_str(), message.c_str())); cheatingPlayers[userId].insert(stat + message); } if (!serverIsPresent(this)) { // only report from server return; } static const std::string kStatsCategory("RemoteSysStats"); bool isStudioServer = true; int placeId = 0; if (DataModel* dm = DataModel::get(this)) { placeId = dm->getPlaceID(); isStudioServer = placeId <= 0; } if (isStudioServer) { FASTLOG3(FLog::US14116, "Sending display stats: %d | %s | %s", userId, stat.c_str(), message.c_str()); } if (sysStatsUrl.empty()){ StandardOut::singleton()->printf(MESSAGE_INFO, "SysStats URL Empty."); return; } //They are using CheatEngine or Fidler or have a non-signed EXE //Make the request and send it out std::stringstream requestUrl; requestUrl << sysStatsUrl << "&UserID=" << userId << "&Resolution=" << stat << "&Message=" << message; requestUrl << char(0); Http http(requestUrl.str()); std::string response; try { http.get(boost::bind(&whoCaresResponse, _1, _2)); } catch (std::bad_alloc&) { throw; } catch (RBX::base_exception&) { StandardOut::singleton()->printf(MESSAGE_INFO, "Players::onRemoteSysStats disconnect send failed"); // Remove the comment at the down if you already prepared your sysstats. disconnectPlayer(userId, Replicator::DisconnectReason_OnRemoteSysStats); return; } StandardOut::singleton()->printf(MESSAGE_INFO, "Players::onRemoteSysStats disconnect"); //Shut. It. Down. // Remove the comment at the down if you already prepared your sysstats. disconnectPlayer(userId, Replicator::DisconnectReason_OnRemoteSysStats); } void Players::onChildAdded(Instance* child) { if (Player* player = Instance::fastDynamicCast(child)) { int userId = player->getUserID(); if (userId > 0) { shared_ptr otherPlayer = getPlayerByID(userId); if (otherPlayer) { // TODO: Wrap this into our security framework. This looks like just a hack disconnectPlayer(userId, Replicator::DisconnectReason_OnRemoteSysStats); StandardOut::singleton()->printf(MESSAGE_INFO, "Kicked duplicated user."); //throw std::runtime_error(format("Player %d attempted to join twice. Booting both players", player->getUserID())); } } { boost::shared_ptr& c = players.write(); c->push_back(shared_from(child)); } if (FriendService* friendService = ServiceProvider::find(this)) { friendService->playerAdded(userId); } if (Players::backendProcessing(this)) // Server does it in client/server - bail on client mode. will still execute in solo mode { if (userId == 0 && player->getName() == "Player") { // we are in a test mode, give every player a unique name and userId // userIds are negative for an extra security measure for uploading to website, etc. RBX::Security::Impersonator impersonate(RBX::Security::Replicator_); player->setName(RBX::format("Player%d", ++testPlayerNameId)); player->setUserId(--testPlayerUserId); } Workspace* workspace = ServiceProvider::find(this); if (!DFFlag::LoadGuisWithoutChar || !workspace->getNetworkFilteringEnabled() || (Network::Players::frontendProcessing(this) && Network::Players::backendProcessing(this))) { // this allows guis to be created if you load a character manually later player->createPlayerGui(); } player->scriptSecurityErrorSignal.connect(boost::bind(&Players::reportScriptSecurityError, this, userId, _1, _2, _3)); player->killPlayerSignal.connect(boost::bind(&Players::killPlayer, this, userId)); player->statsSignal.connect(boost::bind(&Players::onRemoteSysStats, this, userId, "1920x1200", _1, true)); player->remoteFriendServiceSignal.connect(boost::bind(&Players::friendServiceRequest, this, _1, weak_from(player), _2)); if (!isCloudEdit(this) && !player->calculatesSpawnLocationEarly()) { // Team Service if (Teams* ts = ServiceProvider::find(this)) ts->assignNewPlayerToTeam(player); } if (!DFFlag::FirePlayerAddedAndPlayerRemovingOnClient) { playerAddedEarlySignal(shared_from(player)); playerAddedSignal(shared_from(player)); } } if (DFFlag::FirePlayerAddedAndPlayerRemovingOnClient) { playerAddedEarlySignal(shared_from(player)); playerAddedSignal(shared_from(player)); } Http::playerCount = getNumPlayers(); } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class AppendOtherCharacters { public: const Player* ownerPlayer; Region2& region; AppendOtherCharacters(const Player* _main, Region2& _region) : ownerPlayer(_main), region(_region) {} void operator()(shared_ptr instance) { if (Player* p = Instance::fastDynamicCast(instance.get())) { if (p != ownerPlayer) { CoordinateFrame pos; if (p->hasCharacterHead(pos)) { region.appendOther(Region2::WeightedPoint(pos.translation.xz(), p->getSimulationRadius())); } } } } }; void Players::buildClientRegion(Region2& clientRegion) { clientRegion.clearEmpty(); CoordinateFrame pos; if (localPlayer && (localPlayer->hasCharacterHead(pos))) { clientRegion.setOwner(Region2::WeightedPoint(pos.translation.xz(), localPlayer->getSimulationRadius())); AppendOtherCharacters appendOtherCharacters(localPlayer.get(), clientRegion); this->visitChildren(appendOtherCharacters); } } void Players::renderDPhysicsRegions(Adorn* adorn) { shared_ptr myPlayers = getPlayers(); Instances::const_iterator iter = myPlayers->begin(); Instances::const_iterator end = myPlayers->end(); for (; iter != end; ++iter) { Player* player = boost::polymorphic_downcast(iter->get()); player->renderDPhysicsRegion(adorn); } } void Players::setChatOption(ChatOption value) { chatOption = value; } void Players::setNonSuperSafeChatForAllPlayersEnabled(bool enabled) { nonSuperSafeChatForAllPlayersEnabled = enabled; } bool Players::getNonSuperSafeChatForAllPlayersEnabled() const { return nonSuperSafeChatForAllPlayersEnabled; } namespace RBX { namespace Reflection { template<> EnumDesc::EnumDesc() :EnumDescriptor("ChatStyle") { addPair(Players::CLASSIC_CHAT, "Classic"); addPair(Players::BUBBLE_CHAT, "Bubble"); addPair(Players::CLASSIC_AND_BUBBLE_CHAT, "ClassicAndBubble"); } template<> Players::ChatOption& Variant::convert(void) { return genericConvert(); } template<> EnumDesc::EnumDesc() :EnumDescriptor("PlayerChatType") { addPair(Players::PLAYER_CHAT_TYPE_ALL, "All"); addPair(Players::PLAYER_CHAT_TYPE_TEAM, "Team"); addPair(Players::PLAYER_CHAT_TYPE_WHISPER, "Whisper"); } template<> Players::PlayerChatType& Variant::convert(void) { return genericConvert(); } }//namespace Reflection template<> bool StringConverter::convertToValue(const std::string& text, Players::ChatOption& value) { return Reflection::EnumDesc::singleton().convertToValue(text.c_str(), value); } template<> bool StringConverter::convertToValue(const std::string& text, Players::PlayerChatType& value) { return Reflection::EnumDesc::singleton().convertToValue(text.c_str(), value); } }//namespace RBX