/** * @file fsradar.cpp * @brief Firestorm radar implementation * * $LicenseInfo:firstyear=2013&license=viewerlgpl$ * Copyright (c) 2013 Ansariel Hiller @ Second Life * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA * http://www.firestormviewer.org * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "fsradar.h" // libs #include "llavatarnamecache.h" #include "llanimationstates.h" #include "llcommonutils.h" #include "llnotificationsutil.h" #include "lleventtimer.h" // newview #include "fsassetblacklist.h" #include "fscommon.h" #include "fskeywords.h" #include "fslslbridge.h" #include "fslslbridgerequest.h" #include "lggcontactsets.h" #include "lfsimfeaturehandler.h" #include "llagent.h" #include "llavataractions.h" #include "llmutelist.h" #include "llnotificationmanager.h" #include "lltracker.h" #include "lltrans.h" #include "llviewercontrol.h" // for gSavedSettings #include "llviewermenu.h" // for gMenuHolder #include "llviewerparcelmgr.h" #include "llvoavatar.h" #include "llvoiceclient.h" #include "llworld.h" #include "llspeakers.h" #include "llviewerregion.h" #include "rlvactions.h" #include "rlvhandler.h" constexpr F32 FS_RADAR_LIST_UPDATE_INTERVAL = 1.f; /** * Periodically updates the nearby people list while the Nearby tab is active. * * The period is defined by FS_RADAR_LIST_UPDATE_INTERVAL constant. */ class FSRadarListUpdater : public FSRadar::Updater, public LLEventTimer { LOG_CLASS(FSRadarListUpdater); public: FSRadarListUpdater(callback_t cb) : LLEventTimer(FS_RADAR_LIST_UPDATE_INTERVAL), FSRadar::Updater(cb) { update(); mEventTimer.start(); } /*virtual*/ bool tick() { update(); return false; } }; //============================================================================= FSRadar::FSRadar() : mRadarAlertRequest(false), mRadarFrameCount(0), mRadarLastBulkOffsetRequestTime(0), mRadarLastRequestTime(0.f), mShowUsernamesCallbackConnection(), mNameFormatCallbackConnection(), mAgeAlertCallbackConnection(), mRegionCapabilitiesReceivedCallbackConnection(), mRegionChangedCallbackConnection() { // Use the callback from LLAvatarNameCache here or we might update the names too early! LLAvatarNameCache::getInstance()->addUseDisplayNamesCallback(boost::bind(&FSRadar::updateNames, this)); mShowUsernamesCallbackConnection = gSavedSettings.getControl("NameTagShowUsernames")->getSignal()->connect(boost::bind(&FSRadar::updateNames, this)); mNameFormatCallbackConnection = gSavedSettings.getControl("RadarNameFormat")->getSignal()->connect(boost::bind(&FSRadar::updateNames, this)); mAgeAlertCallbackConnection = gSavedSettings.getControl("RadarAvatarAgeAlertValue")->getSignal()->connect(boost::bind(&FSRadar::updateAgeAlertCheck, this)); mRegionChangedCallbackConnection = gAgent.addRegionChangedCallback([this]() { onRegionChanged(); }); } FSRadar::~FSRadar() { gAgent.removeRegionChangedCallback(mRegionChangedCallbackConnection); if (mRegionCapabilitiesReceivedCallbackConnection.connected()) { mRegionCapabilitiesReceivedCallbackConnection.disconnect(); } if (mShowUsernamesCallbackConnection.connected()) { mShowUsernamesCallbackConnection.disconnect(); } if (mNameFormatCallbackConnection.connected()) { mNameFormatCallbackConnection.disconnect(); } if (mAgeAlertCallbackConnection.connected()) { mAgeAlertCallbackConnection.disconnect(); } } void FSRadar::initSingleton() { mRadarListUpdater = std::make_unique(std::bind(&FSRadar::updateRadarList, this)); } void FSRadar::radarAlertMsg(const LLUUID& agent_id, const LLAvatarName& av_name, std::string_view postMsg) { // Milkshake-style radar alerts static LLCachedControl sFSMilkshakeRadarToasts(gSavedSettings, "FSMilkshakeRadarToasts", false); if (sFSMilkshakeRadarToasts) { LLSD payload = agent_id; LLSD args; args["NAME"] = FSRadarEntry::getRadarName(av_name); args["MESSAGE"] = static_cast(postMsg); LLNotificationsUtil::add("RadarAlert", args, payload.with("respond_on_mousedown", true), boost::bind(&LLAvatarActions::zoomIn, agent_id)); } else { // LLChat chat; chat.mText = postMsg; chat.mSourceType = CHAT_SOURCE_SYSTEM; chat.mFromName = FSRadarEntry::getRadarName(av_name); chat.mFromID = agent_id; chat.mChatType = CHAT_TYPE_RADAR; // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports LLSD args; LLNotificationsUI::LLNotificationManager::instance().onChat(chat, args); if (FSKeywords::getInstance()->chatContainsKeyword(chat, true)) { FSKeywords::notify(chat); } } // } void FSRadar::updateRadarList() { //Configuration LLWorld* world = LLWorld::getInstance(); LLMuteList* mutelist = LLMuteList::getInstance(); FSAssetBlacklist* blacklist = FSAssetBlacklist::getInstance(); LGGContactSets* contactsets = LGGContactSets::getInstance(); LLLocalSpeakerMgr* speakermgr = LLLocalSpeakerMgr::getInstance(); LLVoiceClient* voice_client = LLVoiceClient::getInstance(); LLViewerParcelMgr& parcelmgr = LLViewerParcelMgr::instance(); LLUIColorTable& colortable = LLUIColorTable::instance(); LLAvatarTracker& avatartracker = LLAvatarTracker::instance(); FSLSLBridge& bridge = FSLSLBridge::instance(); LFSimFeatureHandler& simfeaturehandler = LFSimFeatureHandler::instance(); const F32 chat_range_say = simfeaturehandler.sayRange(); const F32 chat_range_shout = simfeaturehandler.shoutRange(); static const std::string str_chat_entering = LLTrans::getString("entering_chat_range"); static const std::string str_chat_leaving = LLTrans::getString("leaving_chat_range"); static const std::string str_draw_distance_entering = LLTrans::getString("entering_draw_distance"); static const std::string str_draw_distance_leaving = LLTrans::getString("leaving_draw_distance"); static const std::string str_region_entering = LLTrans::getString("entering_region"); static const std::string str_region_entering_distance = LLTrans::getString("entering_region_distance"); static const std::string str_region_leaving = LLTrans::getString("leaving_region"); static const std::string str_avatar_age_alert = LLTrans::getString("avatar_age_alert"); static const std::string str_avatar_age_hidden = LLTrans::getString("avatar_age_not_available"); static LLCachedControl sRadarReportChatRangeEnter(gSavedSettings, "RadarReportChatRangeEnter"); static LLCachedControl sRadarReportChatRangeLeave(gSavedSettings, "RadarReportChatRangeLeave"); static LLCachedControl sRadarReportDrawRangeEnter(gSavedSettings, "RadarReportDrawRangeEnter"); static LLCachedControl sRadarReportDrawRangeLeave(gSavedSettings, "RadarReportDrawRangeLeave"); static LLCachedControl sRadarReportSimRangeEnter(gSavedSettings, "RadarReportSimRangeEnter"); static LLCachedControl sRadarReportSimRangeLeave(gSavedSettings, "RadarReportSimRangeLeave"); static LLCachedControl sRadarEnterChannelAlert(gSavedSettings, "RadarEnterChannelAlert"); static LLCachedControl sRadarLeaveChannelAlert(gSavedSettings, "RadarLeaveChannelAlert"); static LLCachedControl sRadarAvatarAgeAlert(gSavedSettings, "RadarAvatarAgeAlert"); static LLCachedControl sNearMeRange(gSavedSettings, "NearMeRange"); static LLCachedControl sLimitRange(gSavedSettings, "LimitRadarByRange"); static LLCachedControl sRenderFarClip(gSavedSettings, "RenderFarClip"); static LLCachedControl sFSLegacyRadarFriendColoring(gSavedSettings, "FSLegacyRadarFriendColoring"); static LLCachedControl sFSRadarColorNamesByDistance(gSavedSettings, "FSRadarColorNamesByDistance", false); static LLCachedControl sFSRadarShowMutedAndDerendered(gSavedSettings, "FSRadarShowMutedAndDerendered"); static LLCachedControl sFSRadarEnhanceByBridge(gSavedSettings, "FSRadarEnhanceByBridge"); bool sUseLSLBridge = bridge.canUseBridge(); F32 drawRadius(sRenderFarClip); const LLVector3d& posSelf = gAgent.getPositionGlobal(); LLUUID regionSelf; if (LLViewerRegion* own_reg = gAgent.getRegion(); own_reg) { regionSelf = own_reg->getRegionID(); } bool alertScripts = mRadarAlertRequest; // save the current value, so it doesn't get changed out from under us by another thread time_t now = time(nullptr); //STEP 0: Clear model data mRadarEnterAlerts.clear(); mRadarLeaveAlerts.clear(); mRadarOffsetRequests.clear(); mRadarEntriesData.clear(); mAvatarStats.clear(); //STEP 1: Update our basic data model: detect Avatars & Positions in our defined range std::vector positions; uuid_vec_t avatar_ids; if (RlvActions::canShowNearbyAgents()) { if (sLimitRange) { world->getAvatars(&avatar_ids, &positions, gAgent.getPositionGlobal(), sNearMeRange); } else { world->getAvatars(&avatar_ids, &positions); } } // Determine lists of new added and removed avatars uuid_vec_t current_vec, added_vec, removed_vec; current_vec.reserve(mEntryList.size()); for (const auto& [av_id, entry] : mEntryList) { current_vec.emplace_back(av_id); } LLCommonUtils::computeDifference(avatar_ids, current_vec, added_vec, removed_vec); // Remove old avatars from our list for (const auto& avid : removed_vec) { if (entry_map_t::iterator found = mEntryList.find(avid); found != mEntryList.end()) { mEntryList.erase(found); } } // Add new avatars for (const auto& avid : added_vec) { mEntryList.emplace(avid, std::make_shared(avid)); } speakermgr->update(true); //STEP 2: Transform detected model list data into more flexible multimap data structure; //TS: Count avatars in chat range and in the same region U32 inChatRange{ 0 }; U32 inSameRegion{ 0 }; std::vector::const_iterator pos_it = positions.begin(), pos_end = positions.end(); uuid_vec_t::const_iterator item_it = avatar_ids.begin(), item_end = avatar_ids.end(); for (;pos_it != pos_end && item_it != item_end; ++pos_it, ++item_it) { // //2a. For each detected av, gather up all data we would want to display or use to drive alerts // LLUUID avId = static_cast(*item_it); LLVector3d avPos = static_cast(*pos_it); if (avId == gAgentID) { continue; } // Skip modelling this avatar if its basic data is either inaccessible, or it's a dummy placeholder auto ent = getEntry(avId); if (!ent) // don't update this radar listing if data is inaccessible { continue; } // Try to get the avatar's viewer object - we will need it anyway later LLVOAvatar* avVo = static_cast(gObjectList.findObject(avId)); static LLUICachedControl sFSShowDummyAVsinRadar("FSShowDummyAVsinRadar"); if (!sFSShowDummyAVsinRadar && avVo && avVo->mIsDummy) { continue; } bool is_muted = mutelist->isMuted(avId); bool is_blacklisted = blacklist->isBlacklisted(avId, LLAssetType::AT_PERSON); bool should_be_ignored = is_muted || is_blacklisted; ent->mIgnore = should_be_ignored; if (!sFSRadarShowMutedAndDerendered && should_be_ignored) { continue; } LLUUID avRegion; if (LLViewerRegion* reg = world->getRegionFromPosGlobal(avPos); reg) { avRegion = reg->getRegionID(); } bool isInSameRegion = (avRegion == regionSelf); bool isOnSameParcel = parcelmgr.inAgentParcel(avPos); S32 seentime = (S32)difftime(now, ent->mFirstSeen); S32 hours = (S32)(seentime / 3600); S32 mins = (S32)((seentime - hours * 3600) / 60); S32 secs = (S32)((seentime - hours * 3600 - mins * 60)); std::string avSeenStr = llformat("%d:%02d:%02d", hours, mins, secs); S32 avStatusFlags = ent->mStatus; ERadarPaymentInfoFlag avFlag = FSRADAR_PAYMENT_INFO_NONE; if (avStatusFlags & AVATAR_TRANSACTED) { avFlag = FSRADAR_PAYMENT_INFO_USED; } else if (avStatusFlags & AVATAR_IDENTIFIED) { avFlag = FSRADAR_PAYMENT_INFO_FILLED; } S32 avAge = ent->mAge; std::string avName = ent->mName; U32 lastZOffsetTime = ent->mLastZOffsetTime; F32 avZOffset = ent->mZOffset; if (avPos[VZ] == AVATAR_UNKNOWN_Z_OFFSET) // if our official z position is AVATAR_UNKNOWN_Z_OFFSET, we need a correction. { // set correction if we have it if (avZOffset > 0.1f) { avPos[VZ] = avZOffset; } //schedule offset requests, if needed if (sUseLSLBridge && sFSRadarEnhanceByBridge && (now > (mRadarLastBulkOffsetRequestTime + FSRADAR_COARSE_OFFSET_INTERVAL)) && (now > lastZOffsetTime + FSRADAR_COARSE_OFFSET_INTERVAL)) { mRadarOffsetRequests.push_back(avId); ent->mLastZOffsetTime = now; } } F32 avRange = (avPos[VZ] != AVATAR_UNKNOWN_Z_OFFSET ? dist_vec(avPos, posSelf) : AVATAR_UNKNOWN_RANGE); ent->mRange = avRange; ent->mGlobalPos = avPos; ent->mRegion = avRegion; // Double-check range here since limiting range on calling LLWorld::getAvatars does // not work if other avatar is beyond draw distance and above 1020m height. Need to // use LSL bridge result to filter those out. if (sLimitRange && avRange > sNearMeRange) { continue; } // //2b. Process newly detected avatars // radarfields_map_t::iterator last_sweep_found_it = mLastRadarSweep.find(avId); if (last_sweep_found_it == mLastRadarSweep.end()) { // chat alerts if (sRadarReportChatRangeEnter && (avRange <= chat_range_say) && avRange > AVATAR_UNKNOWN_RANGE) { LLStringUtil::format_map_t args; args["DISTANCE"] = llformat("%3.2f", avRange); std::string message = format_string(str_chat_entering, args); make_ui_sound("UISndRadarChatEnter"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, message)); } if (sRadarReportDrawRangeEnter && (avRange <= drawRadius) && avRange > AVATAR_UNKNOWN_RANGE) { LLStringUtil::format_map_t args; args["DISTANCE"] = llformat("%3.2f", avRange); std::string message = format_string(str_draw_distance_entering, args); make_ui_sound("UISndRadarDrawEnter"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, message)); } if (sRadarReportSimRangeEnter && isInSameRegion) { make_ui_sound("UISndRadarSimEnter"); // FIRE-6069: Radar alerts sounds if (avRange != AVATAR_UNKNOWN_RANGE) // Don't report an inaccurate range in localchat, if the true range is not known. { LLStringUtil::format_map_t args; args["DISTANCE"] = llformat("%3.2f", avRange); std::string message = format_string(str_region_entering_distance, args); LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, message)); } else { LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, str_region_entering)); } } if (sRadarEnterChannelAlert || (alertScripts)) { // Autodetect Phoenix chat UUID compatibility. // If Leave channel alerts are not set, restrict reports to same-sim only. if (!sRadarLeaveChannelAlert) { if (isInSameRegion) { mRadarEnterAlerts.push_back(avId); } } else { mRadarEnterAlerts.push_back(avId); } } } // // 2c. Process previously detected avatars // else { RadarFields rf = last_sweep_found_it->second; if (sRadarReportChatRangeEnter || sRadarReportChatRangeLeave) { if (sRadarReportChatRangeEnter && (avRange <= chat_range_say && avRange > AVATAR_UNKNOWN_RANGE) && (rf.lastDistance > chat_range_say || rf.lastDistance == AVATAR_UNKNOWN_RANGE)) { LLStringUtil::format_map_t args; args["DISTANCE"] = llformat("%3.2f", avRange); std::string message = format_string(str_chat_entering, args); make_ui_sound("UISndRadarChatEnter"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, message)); } else if (sRadarReportChatRangeLeave && (avRange > chat_range_say || avRange == AVATAR_UNKNOWN_RANGE) && (rf.lastDistance <= chat_range_say && rf.lastDistance > AVATAR_UNKNOWN_RANGE)) { make_ui_sound("UISndRadarChatLeave"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, str_chat_leaving)); } } if (sRadarReportDrawRangeEnter || sRadarReportDrawRangeLeave) { if (sRadarReportDrawRangeEnter && (avRange <= drawRadius && avRange > AVATAR_UNKNOWN_RANGE) && (rf.lastDistance > drawRadius || rf.lastDistance == AVATAR_UNKNOWN_RANGE)) { LLStringUtil::format_map_t args; args["DISTANCE"] = llformat("%3.2f", avRange); std::string message = format_string(str_draw_distance_entering, args); make_ui_sound("UISndRadarDrawEnter"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, message)); } else if (sRadarReportDrawRangeLeave && (avRange > drawRadius || avRange == AVATAR_UNKNOWN_RANGE) && (rf.lastDistance <= drawRadius && rf.lastDistance > AVATAR_UNKNOWN_RANGE)) { make_ui_sound("UISndRadarDrawLeave"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, str_draw_distance_leaving)); } } if (sRadarReportSimRangeEnter || sRadarReportSimRangeLeave) { if (sRadarReportSimRangeEnter && isInSameRegion && avRegion != rf.lastRegion && rf.lastRegion.notNull()) { make_ui_sound("UISndRadarSimEnter"); // FIRE-6069: Radar alerts sounds if (avRange != AVATAR_UNKNOWN_RANGE) // Don't report an inaccurate range in localchat, if the true range is not known. { LLStringUtil::format_map_t args; args["DISTANCE"] = llformat("%3.2f", avRange); std::string message = format_string(str_region_entering_distance, args); LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, message)); } else { LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, str_region_entering)); } } else if (sRadarReportSimRangeLeave && rf.lastRegion == regionSelf && !isInSameRegion && avRegion.notNull()) { make_ui_sound("UISndRadarSimLeave"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, str_region_leaving)); } } //If we were manually asked to update an external source for all existing avatars, add them to the queue. if (alertScripts) { mRadarEnterAlerts.push_back(avId); } } // //2d. Prepare data for presentation view for this avatar // if (isInSameRegion) { inSameRegion++; } LLSD entry; LLSD entry_options; entry["id"] = avId; entry["name"] = avName; entry["in_region"] = isInSameRegion; entry["on_parcel"] = isOnSameParcel; entry["flags"] = avFlag; entry["seen"] = avSeenStr; entry["range"] = (avRange > AVATAR_UNKNOWN_RANGE ? llformat("%3.2f", avRange) : llformat(">%3.2f", drawRadius)); entry["typing"] = (avVo && avVo->isTyping()); entry["sitting"] = (avVo && (avVo->getParent() || avVo->isMotionActive(ANIM_AGENT_SIT_GROUND) || avVo->isMotionActive(ANIM_AGENT_SIT_GROUND_CONSTRAINED))); if (!gRlvHandler.hasBehaviour(RLV_BHVR_SHOWNAMES)) { entry["notes"] = ent->getNotes(); if (avAge > -1) entry["age"] = llformat("%d", avAge); else if (avAge == -2) entry["age"] = str_avatar_age_hidden; else entry["age"] = ""; if (ent->hasAlertAge()) { entry_options["age_color"] = colortable.getColor("AvatarListItemAgeAlert", LLColor4::red).get().getValue(); if (sRadarAvatarAgeAlert && !ent->hasAgeAlertPerformed()) { make_ui_sound("UISndRadarAgeAlert"); LLStringUtil::format_map_t args; args["AGE"] = llformat("%d", avAge); std::string message = format_string(str_avatar_age_alert, args); LLAvatarNameCache::get(avId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, message)); } ent->mAgeAlertPerformed = true; } } else { entry["notes"] = LLStringUtil::null; entry["age"] = "---"; } //AO: Set any range colors / styles LLUIColor range_color; if (avRange > AVATAR_UNKNOWN_RANGE) { if (avRange <= chat_range_say) { range_color = colortable.getColor("AvatarListItemChatRange", LLColor4::red); inChatRange++; } else if (avRange <= chat_range_shout) { range_color = colortable.getColor("AvatarListItemShoutRange", LLColor4::white); } else { range_color = colortable.getColor("AvatarListItemBeyondShoutRange", LLColor4::white); } } else { range_color = colortable.getColor("AvatarListItemBeyondShoutRange", LLColor4::white); } entry_options["range_color"] = range_color.get().getValue(); // Check if avatar is in draw distance and a VOAvatar instance actually exists if (avRange <= drawRadius && avRange > AVATAR_UNKNOWN_RANGE && avVo) { entry_options["range_style"] = LLFontGL::BOLD; } else { entry_options["range_style"] = LLFontGL::NORMAL; } // Set friends colors / styles LLFontGL::StyleFlags nameCellStyle = LLFontGL::NORMAL; const LLRelationship* relation = avatartracker.getBuddyInfo(avId); if (relation && !sFSLegacyRadarFriendColoring && !gRlvHandler.hasBehaviour(RLV_BHVR_SHOWNAMES)) { nameCellStyle = (LLFontGL::StyleFlags)(nameCellStyle | LLFontGL::BOLD); } if (is_muted) { nameCellStyle = (LLFontGL::StyleFlags)(nameCellStyle | LLFontGL::ITALIC); } entry_options["name_style"] = nameCellStyle; LLColor4 name_color = colortable.getColor("AvatarListItemIconDefaultColor", LLColor4::white).get(); name_color = contactsets->colorize(avId, (sFSRadarColorNamesByDistance ? range_color.get() : name_color), ContactSetType::RADAR); contactsets->hasFriendColorThatShouldShow(avId, ContactSetType::RADAR, name_color); entry_options["name_color"] = name_color.getValue(); // Voice power level indicator if (voice_client->voiceEnabled() && voice_client->isVoiceWorking()) { if (LLSpeaker* speaker = speakermgr->findSpeaker(avId); speaker && speaker->isInVoiceChannel()) { EVoicePowerLevel power_level = voice_client->getPowerLevel(avId); switch (power_level) { case VPL_PTT_Off: entry["voice_level_icon"] = "Radar_VoicePTT_Off"; break; case VPL_PTT_On: entry["voice_level_icon"] = "Radar_VoicePTT_On"; break; case VPL_Level1: entry["voice_level_icon"] = "Radar_VoicePTT_Lvl1"; break; case VPL_Level2: entry["voice_level_icon"] = "Radar_VoicePTT_Lvl2"; break; case VPL_Level3: entry["voice_level_icon"] = "Radar_VoicePTT_Lvl3"; break; default: break; } } } // Save data for our listeners LLSD entry_data; entry_data["entry"] = entry; entry_data["options"] = entry_options; mRadarEntriesData.push_back(entry_data); } // End STEP 2, all model/presentation row processing complete. // //STEP 3, process any bulk actions that require the whole model to be known first // // //3a. dispatch requests for ZOffset updates, working around minimap's inaccurate height // if (mRadarOffsetRequests.size() > 0) { static const std::string prefix = "getZOffsets|"; std::string msg {}; U32 updatesPerRequest = 0; while (mRadarOffsetRequests.size() > 0) { LLUUID avId = mRadarOffsetRequests.back(); mRadarOffsetRequests.pop_back(); msg = llformat("%s%s,", msg.c_str(), avId.asString().c_str()); if (++updatesPerRequest > FSRADAR_MAX_OFFSET_REQUESTS) { msg = msg.substr(0, msg.size() - 1); bridge.viewerToLSL(prefix + msg, FSLSLBridgeRequestRadarPos_Success); //LL_INFOS() << " OFFSET REQUEST SEGMENT"<< prefix << msg << LL_ENDL; msg.clear(); updatesPerRequest = 0; } } if (updatesPerRequest > 0) { msg = msg.substr(0, msg.size() - 1); bridge.viewerToLSL(prefix + msg, FSLSLBridgeRequestRadarPos_Success); //LL_INFOS() << " OFFSET REQUEST FINAL " << prefix << msg << LL_ENDL; } // clear out the dispatch queue mRadarOffsetRequests.clear(); mRadarLastBulkOffsetRequestTime = now; } // //3b: process alerts for avatars that where here last frame, but gone this frame (ie, they left) // as well as dispatch all earlier detected alerts for crossing range thresholds. // if (RlvActions::canShowNearbyAgents()) { for (const auto& [prevId, rf] : mLastRadarSweep) { if ((sFSRadarShowMutedAndDerendered || !rf.lastIgnore) && mEntryList.find(prevId) == mEntryList.end()) { if (sRadarReportChatRangeLeave && (rf.lastDistance <= chat_range_say) && rf.lastDistance > AVATAR_UNKNOWN_RANGE) { make_ui_sound("UISndRadarChatLeave"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(prevId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, str_chat_leaving)); } if (sRadarReportDrawRangeLeave && (rf.lastDistance <= drawRadius) && rf.lastDistance > AVATAR_UNKNOWN_RANGE) { make_ui_sound("UISndRadarDrawLeave"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(prevId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, str_draw_distance_leaving)); } if (sRadarReportSimRangeLeave && (rf.lastRegion == regionSelf || rf.lastRegion.isNull())) { make_ui_sound("UISndRadarSimLeave"); // FIRE-6069: Radar alerts sounds LLAvatarNameCache::get(prevId, boost::bind(&FSRadar::radarAlertMsg, this, _1, _2, str_region_leaving)); } if (sRadarLeaveChannelAlert) { mRadarLeaveAlerts.push_back(prevId); } } } } static LLCachedControl sRadarAlertChannel(gSavedSettings, "RadarAlertChannel"); if (U32 num_entering = (U32)mRadarEnterAlerts.size(); num_entering > 0) { mRadarFrameCount++; U32 num_this_pass = llmin(FSRADAR_MAX_AVATARS_PER_ALERT, num_entering); std::string msg = llformat("%d,%d", mRadarFrameCount, num_this_pass); U32 loop = 0; while (loop < num_entering) { for (U32 i = 0; i < num_this_pass; i++) { msg = llformat("%s,%s", msg.c_str(), mRadarEnterAlerts[loop + i].asString().c_str()); } LLMessageSystem* msgs = gMessageSystem; msgs->newMessage("ScriptDialogReply"); msgs->nextBlock("AgentData"); msgs->addUUID("AgentID", gAgentID); msgs->addUUID("SessionID", gAgentSessionID); msgs->nextBlock("Data"); msgs->addUUID("ObjectID", gAgentID); msgs->addS32("ChatChannel", sRadarAlertChannel()); msgs->addS32("ButtonIndex", 1); msgs->addString("ButtonLabel", msg.c_str()); gAgent.sendReliableMessage(); loop += num_this_pass; num_this_pass = llmin(FSRADAR_MAX_AVATARS_PER_ALERT, num_entering - loop); msg = llformat("%d,%d", mRadarFrameCount, num_this_pass); } } if (U32 num_leaving = (U32)mRadarLeaveAlerts.size(); num_leaving > 0) { mRadarFrameCount++; U32 num_this_pass = llmin(FSRADAR_MAX_AVATARS_PER_ALERT, num_leaving); std::string msg = llformat("%d,-%d", mRadarFrameCount, llmin(FSRADAR_MAX_AVATARS_PER_ALERT, num_leaving)); U32 loop = 0; while (loop < num_leaving) { for (U32 i = 0; i < num_this_pass; i++) { msg = llformat("%s,%s", msg.c_str(), mRadarLeaveAlerts[loop + i].asString().c_str()); } LLMessageSystem* msgs = gMessageSystem; msgs->newMessage("ScriptDialogReply"); msgs->nextBlock("AgentData"); msgs->addUUID("AgentID", gAgentID); msgs->addUUID("SessionID", gAgentSessionID); msgs->nextBlock("Data"); msgs->addUUID("ObjectID", gAgentID); msgs->addS32("ChatChannel", sRadarAlertChannel()); msgs->addS32("ButtonIndex", 1); msgs->addString("ButtonLabel", msg.c_str()); gAgent.sendReliableMessage(); loop += num_this_pass; num_this_pass = llmin(FSRADAR_MAX_AVATARS_PER_ALERT, num_leaving - loop); msg = llformat("%d,-%d", mRadarFrameCount, num_this_pass); } } // reset any active alert requests if (alertScripts) { mRadarAlertRequest = false; } // //STEP 4: Cache our current model data, so we can compare it with the next fresh group of model data for fast change detection. // mLastRadarSweep.clear(); for (const auto& [avid, entry] : mEntryList) { RadarFields rf; rf.lastDistance = entry->mRange; rf.lastIgnore = entry->mIgnore; rf.lastRegion = LLUUID::null; if (entry->mGlobalPos != LLVector3d(0.0, 0.0, 0.0)) { LLViewerRegion* lastRegion = world->getRegionFromPosGlobal(entry->mGlobalPos); if (lastRegion) { rf.lastRegion = lastRegion->getRegionID(); } } mLastRadarSweep[entry->mID] = rf; } // //STEP 5: Final data updates and notification of subscribers // if (RlvActions::canShowNearbyAgents()) { mAvatarStats["total"] = llformat("%d", mLastRadarSweep.size() - 1); mAvatarStats["region"] = llformat("%d", inSameRegion); mAvatarStats["chatrange"] = llformat("%d", inChatRange); } else { mAvatarStats["total"] = "-"; mAvatarStats["region"] = "-"; mAvatarStats["chatrange"] = "-"; } checkTracking(); // Inform our subscribers about updates if (!mUpdateSignal.empty()) { mUpdateSignal(mRadarEntriesData, mAvatarStats); } } void FSRadar::requestRadarChannelAlertSync() { if (F32 timeNow = gFrameTimeSeconds; (timeNow - FSRADAR_CHAT_MIN_SPACING) > mRadarLastRequestTime) { mRadarLastRequestTime = timeNow; mRadarAlertRequest = true; } } std::shared_ptr FSRadar::getEntry(const LLUUID& avatar_id) { if (entry_map_t::iterator found = mEntryList.find(avatar_id); found != mEntryList.end()) { return found->second; } return nullptr; } void FSRadar::teleportToAvatar(const LLUUID& targetAv) // Teleports user to last scanned location of nearby avatar // Note: currently teleportViaLocation is disrupted by enforced landing points set on a parcel. { if (auto entry = getEntry(targetAv); entry) { LLVector3d avpos = entry->mGlobalPos; if (avpos.mdV[VZ] == AVATAR_UNKNOWN_Z_OFFSET) { LLNotificationsUtil::add("TeleportToAvatarNotPossible"); } else { // FIRE-20862: Teleport the configured offset toward the center of the region from the // avatar's reported position LLViewerRegion* avreg = LLWorld::getInstance()->getRegionFromPosGlobal(avpos); if (avreg) { LLVector3d region_center = avreg->getCenterGlobal(); LLVector3d offset = avpos - region_center; LLVector3d destination; F32 lateral_distance = gSavedSettings.getF32("FSTeleportToOffsetLateral"); F32 vertical_distance = gSavedSettings.getF32("FSTeleportToOffsetVertical"); if (offset.normalize() != 0.f) // there's an actual offset { if (lateral_distance > 0.0f) { offset *= lateral_distance; destination = avpos - offset; } else { destination = avpos; } } else // the target is exactly at the center, so the offset is 0 { destination = region_center + LLVector3d(0.f, lateral_distance, 0.f); } destination.mdV[VZ] = avpos.mdV[VZ] + vertical_distance; gAgent.teleportViaLocation(destination); } } } else { LLNotificationsUtil::add("TeleportToAvatarNotPossible"); } } //static void FSRadar::onRadarNameFmtClicked(const LLSD& userdata) { const std::string chosen_item = userdata.asString(); if (chosen_item == "DN") { gSavedSettings.setU32("RadarNameFormat", FSRADAR_NAMEFORMAT_DISPLAYNAME); } else if (chosen_item == "UN") { gSavedSettings.setU32("RadarNameFormat", FSRADAR_NAMEFORMAT_USERNAME); } else if (chosen_item == "DNUN") { gSavedSettings.setU32("RadarNameFormat", FSRADAR_NAMEFORMAT_DISPLAYNAME_USERNAME); } else if (chosen_item == "UNDN") { gSavedSettings.setU32("RadarNameFormat", FSRADAR_NAMEFORMAT_USERNAME_DISPLAYNAME); } } //static bool FSRadar::radarNameFmtCheck(const LLSD& userdata) { const std::string menu_item = userdata.asString(); U32 name_format = gSavedSettings.getU32("RadarNameFormat"); switch (name_format) { case FSRADAR_NAMEFORMAT_DISPLAYNAME: return (menu_item == "DN"); case FSRADAR_NAMEFORMAT_USERNAME: return (menu_item == "UN"); case FSRADAR_NAMEFORMAT_DISPLAYNAME_USERNAME: return (menu_item == "DNUN"); case FSRADAR_NAMEFORMAT_USERNAME_DISPLAYNAME: return (menu_item == "UNDN"); default: break; } return false; } // Milkshake-style radar alerts //static void FSRadar::onRadarReportToClicked(const LLSD& userdata) { const std::string chosen_item = userdata.asString(); if (chosen_item == "radar_toasts") { gSavedSettings.setBOOL("FSMilkshakeRadarToasts", true); } else if (chosen_item == "radar_nearby_chat") { gSavedSettings.setBOOL("FSMilkshakeRadarToasts", false); } } //static bool FSRadar::radarReportToCheck(const LLSD& userdata) { const std::string menu_item = userdata.asString(); if (gSavedSettings.getBOOL("FSMilkshakeRadarToasts")) { return (menu_item == "radar_toasts"); } else { return (menu_item == "radar_nearby_chat"); } } // void FSRadar::startTracking(const LLUUID& avatar_id) { if (getEntry(avatar_id)) { mTrackedAvatarId = avatar_id; updateTracking(); } else { LLNotificationsUtil::add("TrackAvatarNotPossible"); } } void FSRadar::checkTracking() { if (LLTracker::getTrackingStatus() == LLTracker::TRACKING_LOCATION && LLTracker::getTrackedLocationType() == LLTracker::LOCATION_AVATAR) { updateTracking(); } } void FSRadar::updateTracking() { if (auto entry = getEntry(mTrackedAvatarId); entry) { if (LLTracker::getTrackedPositionGlobal() != entry->mGlobalPos) { LLTracker::trackLocation(entry->mGlobalPos, entry->mName, "", LLTracker::LOCATION_AVATAR); } } else { LLTracker::stopTracking(false); } } void FSRadar::zoomAvatar(const LLUUID& avatar_id, std::string_view name) { if (LLAvatarActions::canZoomIn(avatar_id)) { LLAvatarActions::zoomIn(avatar_id); } else { LLStringUtil::format_map_t args; args["AVATARNAME"] = static_cast(name); report_to_nearby_chat(LLTrans::getString("camera_no_focus", args)); } } void FSRadar::updateNames() { for (auto& [av_id, entry] : mEntryList) { entry->updateName(); } } void FSRadar::updateName(const LLUUID& avatar_id) { if (auto entry = getEntry(avatar_id); entry) { entry->updateName(); } } void FSRadar::updateAgeAlertCheck() { for (auto& [av_id, entry] : mEntryList) { entry->checkAge(); } } void FSRadar::updateNotes(const LLUUID& avatar_id, std::string_view notes) { if (auto entry = getEntry(avatar_id)) { entry->setNotes(notes); } } void FSRadar::onRegionChanged() { if (mRegionCapabilitiesReceivedCallbackConnection.connected()) { mRegionCapabilitiesReceivedCallbackConnection.disconnect(); } if (auto region = gAgent.getRegion()) { if (region->capabilitiesReceived()) { for (auto& [id, entry] : mEntryList) entry->requestProperties(); } else { mRegionCapabilitiesReceivedCallbackConnection = region->setCapabilitiesReceivedCallback( [this](const LLUUID&, LLViewerRegion*) { mRegionCapabilitiesReceivedCallbackConnection.disconnect(); for (auto& [id, entry] : mEntryList) entry->requestProperties(); }); } } }