From 01dda978b354592e4b7eefcb2290ea766301072c Mon Sep 17 00:00:00 2001 From: Ansariel Date: Tue, 27 Jun 2023 17:52:08 +0200 Subject: [PATCH 01/12] Let's replace this boost::unordered_map with the std counterpart, too --- indra/newview/fsradar.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/indra/newview/fsradar.h b/indra/newview/fsradar.h index ca32582bd0..8fbb20e5ed 100644 --- a/indra/newview/fsradar.h +++ b/indra/newview/fsradar.h @@ -34,10 +34,10 @@ class LLAvatarName; -const U32 FSRADAR_MAX_AVATARS_PER_ALERT = 6; // maximum number of UUIDs we can cram into a single channel radar alert message -const U32 FSRADAR_COARSE_OFFSET_INTERVAL = 7; // seconds after which we query the bridge for a coarse location adjustment -const U32 FSRADAR_MAX_OFFSET_REQUESTS = 60; // 2048 / UUID size, leaving overhead space -const U32 FSRADAR_CHAT_MIN_SPACING = 6; // minimum delay between radar chat messages +constexpr U32 FSRADAR_MAX_AVATARS_PER_ALERT{ 6 }; // maximum number of UUIDs we can cram into a single channel radar alert message +constexpr U32 FSRADAR_COARSE_OFFSET_INTERVAL{ 7 }; // seconds after which we query the bridge for a coarse location adjustment +constexpr U32 FSRADAR_MAX_OFFSET_REQUESTS{ 60 }; // 2048 / UUID size, leaving overhead space +constexpr U32 FSRADAR_CHAT_MIN_SPACING{ 6 }; // minimum delay between radar chat messages typedef enum e_radar_name_format { @@ -100,7 +100,7 @@ public: mCallback(); } - callback_t mCallback; + callback_t mCallback; }; typedef boost::signals2::signal& entries, const LLSD& stats)> radar_update_callback_t; @@ -110,11 +110,11 @@ public: } private: - void updateRadarList(); - void updateTracking(); - void checkTracking(); - void radarAlertMsg(const LLUUID& agent_id, const LLAvatarName& av_name, std::string_view postMsg); - void updateAgeAlertCheck(); + void updateRadarList(); + void updateTracking(); + void checkTracking(); + void radarAlertMsg(const LLUUID& agent_id, const LLAvatarName& av_name, std::string_view postMsg); + void updateAgeAlertCheck(); std::unique_ptr mRadarListUpdater; @@ -125,7 +125,7 @@ private: bool lastIgnore; }; - typedef boost::unordered_map radarfields_map_t; + typedef std::unordered_map radarfields_map_t; radarfields_map_t mLastRadarSweep; entry_map_t mEntryList; From 783d59fcd558da7b0eb5a0b4f267ec3ffedbfb30 Mon Sep 17 00:00:00 2001 From: Beq Date: Tue, 27 Jun 2023 17:26:33 +0100 Subject: [PATCH 02/12] Tweak to ensure best match is picked up. --- .github/workflows/build_viewer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_viewer.yml b/.github/workflows/build_viewer.yml index 1a9df8ca3e..8adc9171c1 100644 --- a/.github/workflows/build_viewer.yml +++ b/.github/workflows/build_viewer.yml @@ -168,7 +168,7 @@ jobs: fi function find_most_recent_bundle() { local pattern="$1-.*$2[-_]+.*" - local most_recent_file=$(ls -t "${{ github.workspace }}" | grep "$pattern" | head -1) + local most_recent_file=$(ls -t "${{ github.workspace }}" | egrep "$pattern" | head -1) if [ -z "$most_recent_file" ]; then echo "" else From de702e112abed9037dcb19e112399302eedd4f80 Mon Sep 17 00:00:00 2001 From: Beq Date: Tue, 27 Jun 2023 19:12:58 +0100 Subject: [PATCH 03/12] Slightly less naive cache The simple cache ihas a number of issues * the "max cache size" is not the max cache size * the purge typically only purges DOWN to the max cache size * once you reach max you are basically purging constantly. This change addresses that a bit by adding highwater threshold at 95% leaving a bit of headroom for the incoming cache updates while we purge, and a low water threshold, that we purge down to (70%) . This means people can now set their cache to a ramdisk of they really want and it won't (normally) overflow. it reduces the cache thrashing, this is not a speedup on the main thread really, but just a less naive process. This does not address the fact that every time we check the cache it has to enumerate every single file (beyond stupid) even though most have not changed. This is TBD. --- indra/llfilesystem/lldiskcache.cpp | 214 ++++++++++++++++++------ indra/llfilesystem/lldiskcache.h | 34 +++- indra/newview/app_settings/settings.xml | 22 +++ indra/newview/llappviewer.cpp | 5 +- indra/newview/llappviewerwin32.cpp | 4 +- indra/newview/llviewercontrol.cpp | 19 ++- 6 files changed, 241 insertions(+), 57 deletions(-) diff --git a/indra/llfilesystem/lldiskcache.cpp b/indra/llfilesystem/lldiskcache.cpp index f2ecd9f5b0..01ed49d0d9 100644 --- a/indra/llfilesystem/lldiskcache.cpp +++ b/indra/llfilesystem/lldiskcache.cpp @@ -44,7 +44,12 @@ static const char* subdirs = "0123456789abcdef"; LLDiskCache::LLDiskCache(const std::string cache_dir, const uintmax_t max_size_bytes, - const bool enable_cache_debug_info) : + const bool enable_cache_debug_info +// Add High/Low water mark support + ,const F32 highwater_mark_percent + ,const F32 lowwater_mark_percent +// + ) : mCacheDir(cache_dir), mMaxSizeBytes(max_size_bytes), mEnableCacheDebugInfo(enable_cache_debug_info) @@ -113,6 +118,8 @@ void LLDiskCache::purge() #else std::string cache_path(mCacheDir); #endif + uintmax_t file_size_total = 0; // try to make simple cache less naive. + if (boost::filesystem::is_directory(cache_path, ec) && !ec.failed()) { // Optimize asset simple disk cache @@ -137,6 +144,7 @@ void LLDiskCache::purge() { continue; } + file_size_total += file_size; // try to make simple cache less naive. file_info.push_back(file_info_t(file_time, { file_size, file_path })); } @@ -145,52 +153,106 @@ void LLDiskCache::purge() } } + // add high water/low water thresholds to reduce the churn in the cache. + LL_DEBUGS("LLDiskCache") << "Cache is " << (int)(((F32)file_size_total)/mMaxSizeBytes*100.0) << "% full" << LL_ENDL; + if( file_size_total < mMaxSizeBytes * (mHighPercent/100) ) + { + // Nothing to do here + LL_DEBUGS("LLDiskCache") << "Not exceded high water - do nothing" << LL_ENDL; + return; + } + // If we reach here we are above the trigger level so we must purge until we've removed enough to take us down to the low water mark. + // std::sort(file_info.begin(), file_info.end(), [](file_info_t& x, file_info_t& y) { - return x.first > y.first; + return x.first < y.first; // sort oldest to newest, to we can remove the oldest files first. }); - LL_INFOS() << "Purging cache to a maximum of " << mMaxSizeBytes << " bytes" << LL_ENDL; + + // add high water/low water thresholds to reduce the churn in the cache. + auto target_size = (uintmax_t)(mMaxSizeBytes * (mLowPercent/100)); + LL_INFOS() << "Purging cache to a maximum of " << target_size << " bytes" << LL_ENDL; + // // Extra accounting to track the retention of static assets //std::vector file_removed; - std::vector file_removed; - int keep{0}; - int del{0}; - int skip{0}; + enum class purge_action { delete_file=0, keep_file, skip_file }; + std::map file_removed; + auto keep{file_info.size()}; + auto del{0}; + auto skip{0}; // - if (mEnableCacheDebugInfo) - { - file_removed.reserve(file_info.size()); - } - uintmax_t file_size_total = 0; + // revised purge logic to track amount removed not retained to shortern loop + // uintmax_t file_size_total = 0; + // if (mEnableCacheDebugInfo) + // { + // file_removed.reserve(file_info.size()); + // } + // uintmax_t file_size_total = 0; + // for (file_info_t& entry : file_info) + // { + // file_size_total += entry.second.first; + + // bool should_remove = file_size_total > mMaxSizeBytes; + // // Make sure static assets are not eliminated + // S32 action{ should_remove ? 0 : 1 }; + // if (should_remove) + // { + // auto uuid_as_string = gDirUtilp->getBaseFileName(entry.second.second, true); + // uuid_as_string = uuid_as_string.substr(mCacheFilenamePrefix.size() + 1, 36);// skip "sl_cache_" and trailing "_N" + // // LL_INFOS() << "checking UUID=" < + // if (mEnableCacheDebugInfo) + // { + // // Static asset stuff + // //file_removed.push_back(should_remove); + // file_removed.push_back(action); + // } + uintmax_t deleted_size_total = 0; for (file_info_t& entry : file_info) { - file_size_total += entry.second.first; + // first check if we still need to delete more files + bool should_remove = (file_size_total - deleted_size_total) > target_size; - bool should_remove = file_size_total > mMaxSizeBytes; // Make sure static assets are not eliminated - S32 action{ should_remove ? 0 : 1 }; - if (should_remove) + auto action{ should_remove ? purge_action::delete_file : purge_action::keep_file }; + if (!should_remove) { - auto uuid_as_string = gDirUtilp->getBaseFileName(entry.second.second, true); - uuid_as_string = uuid_as_string.substr(mCacheFilenamePrefix.size() + 1, 36);// skip "sl_cache_" and trailing "_N" - // LL_INFOS() << "checking UUID=" < + + auto this_file_size = entry.second.first; + deleted_size_total += this_file_size; + auto uuid_as_string = gDirUtilp->getBaseFileName(entry.second.second, true); + uuid_as_string = uuid_as_string.substr(mCacheFilenamePrefix.size() + 1, 36);// skip "sl_cache_" and trailing "_N" + // LL_INFOS() << "checking UUID=" < Static asset stuff - //file_removed.push_back(should_remove); - file_removed.push_back(action); + // Static asset stuff + //file_removed.push_back(should_remove); + file_removed.emplace(entry.second.second, action); } + // if (should_remove) { boost::filesystem::remove(entry.second.second, ec); @@ -200,37 +262,52 @@ void LLDiskCache::purge() } } } - +// update the debug logging to be more useful + auto end_time = std::chrono::high_resolution_clock::now(); + auto execute_time = std::chrono::duration_cast(end_time - start_time).count(); +// if (mEnableCacheDebugInfo) { - auto end_time = std::chrono::high_resolution_clock::now(); - auto execute_time = std::chrono::duration_cast(end_time - start_time).count(); + // update the debug logging to be more useful + // auto end_time = std::chrono::high_resolution_clock::now(); + // auto execute_time = std::chrono::duration_cast(end_time - start_time).count(); + // // Log afterward so it doesn't affect the time measurement // Logging thousands of file results can take hundreds of milliseconds + auto deleted_so_far = 0; // update the debug logging to be more useful for (size_t i = 0; i < file_info.size(); ++i) { const file_info_t& entry = file_info[i]; // Static asset stuff + deleted_so_far += entry.second.first; // update the debug logging to be more useful //const bool removed = file_removed[i]; //const std::string action = removed ? "DELETE:" : "KEEP:"; std::string action{}; - switch (file_removed[i]) - { - default: - case 0: - action = "KEEP"; - keep++; - break; - case 1: + + // Check if the file exists in the map + auto& filename{ entry.second.second }; + if (file_removed.find(filename) != file_removed.end()) { + // File found in the map, retrieve the corresponding enum value + switch (file_removed[filename]) { + case purge_action::delete_file: action = "DELETE"; del++; - break; - case 2: + break; + case purge_action::skip_file: action = "STATIC"; skip++; - break; + break; + default: + // Handle any unexpected enum value + action = "UNKNOWN"; + break; } + } + else + { + action = "KEEP"; + } // // have to do this because of LL_INFO/LL_END weirdness @@ -240,14 +317,22 @@ void LLDiskCache::purge() line << entry.first << " "; line << entry.second.first << " "; line << entry.second.second; - line << " (" << file_size_total << "/" << mMaxSizeBytes << ")"; + line << " (" << file_size_total - deleted_so_far << "/" << mMaxSizeBytes << ")"; // update the debug logging to be more useful LL_INFOS() << line.str() << LL_ENDL; } - - LL_INFOS() << "Total dir size after purge is " << dirFileSize(mCacheDir) << LL_ENDL; - LL_INFOS() << "Cache purge took " << execute_time << " ms to execute for " << file_info.size() << " files" << LL_ENDL; - LL_INFOS() << "Deleted: " << del << " Skipped: " << skip << " Kept: " << keep << LL_ENDL; // Extra accounting to track the retention of static assets +// make the summary stats more easily enabled. } + // update the debug logging to be more useful + // LL_INFOS() << "Total dir size after purge is " << dirFileSize(mCacheDir) << LL_ENDL; + // LL_INFOS() << "Cache purge took " << execute_time << " ms to execute for " << file_info.size() << " files" << LL_ENDL; + + auto newCacheSize = updateCacheSize(file_size_total - deleted_size_total); + LL_INFOS("LLDiskCache") << "Total dir size after purge is " << newCacheSize << LL_ENDL; + LL_INFOS("LLDiskCache") << "Cache purge took " << execute_time << " ms to execute for " << file_info.size() << " files" << LL_ENDL; +// + LL_INFOS("LLDiskCache") << "Deleted: " << del << " Skipped: " << skip << " Kept: " << keep << LL_ENDL; // Extra accounting to track the retention of static assets + LL_INFOS("LLDiskCache") << "Total of " << deleted_size_total << " bytes removed." << LL_ENDL; // Extra accounting to track the retention of static assets + // } this bracket was moved up a few lines. } const std::string LLDiskCache::assetTypeToString(LLAssetType::EType at) @@ -534,8 +619,32 @@ void LLDiskCache::removeOldVFSFiles() } } -uintmax_t LLDiskCache::dirFileSize(const std::string& dir) +// Lets not scan every single time if we can avoid it eh? +// uintmax_t LLDiskCache::dirFileSize(const std::string& dir) +// { +uintmax_t LLDiskCache::updateCacheSize(const uintmax_t newsize) { + mStoredCacheSize = newsize; + mLastScanTime = system_clock::now(); + return mStoredCacheSize; +} + +uintmax_t LLDiskCache::dirFileSize(const std::string& dir, bool force ) +{ + using namespace std::chrono; + const seconds cache_duration{ 120 };// A rather arbitrary number. it takes 5 seconds+ on a fast drive to scan 80K+ items. purge runs every minute and will update. so 120 should mean we never need a superfluous cache scan. + + const auto current_time = system_clock::now(); + + const auto time_difference = duration_cast(current_time - mLastScanTime); + + // Check if the cached result can be used + if( !force && time_difference < cache_duration ) + { + LL_DEBUGS("LLDiskCache") << "Using cached result: " << mStoredCacheSize << LL_ENDL; + return mStoredCacheSize; + } +// uintmax_t total_file_size = 0; /** @@ -577,7 +686,10 @@ uintmax_t LLDiskCache::dirFileSize(const std::string& dir) } } - return total_file_size; +// Lets not scan every single time if we can avoid it eh? + // return total_file_size; + return updateCacheSize(total_file_size); +// } LLPurgeDiskCacheThread::LLPurgeDiskCacheThread() : diff --git a/indra/llfilesystem/lldiskcache.h b/indra/llfilesystem/lldiskcache.h index 4b51b66e1f..fc1be25b9f 100644 --- a/indra/llfilesystem/lldiskcache.h +++ b/indra/llfilesystem/lldiskcache.h @@ -63,6 +63,9 @@ #define _LLDISKCACHE #include "llsingleton.h" +#include +using namespace std::chrono; + class LLDiskCache : public LLParamSingleton @@ -92,7 +95,18 @@ class LLDiskCache : * if there are bugs, we can ask uses to enable this * setting and send us their logs */ - const bool enable_cache_debug_info); + const bool enable_cache_debug_info, + // Add high/low threshold controls for cache purging + /** + * A floating point percentage of the max_size_bytes above which the cache purge will trigger. + */ + const F32 highwater_mark_percent, + /** + * A floating point percentage of the max_size_bytes which the cache purge will aim to reach once triggered. + */ + const F32 lowwater_mark_percent + // + ); virtual ~LLDiskCache() = default; @@ -157,6 +171,10 @@ class LLDiskCache : // Better asset cache size control void setMaxSizeBytes(uintmax_t size) { mMaxSizeBytes = size; } + // High/Low water control + void setHighWaterPercentage(F32 HiPct) { mHighPercent = llclamp(HiPct, mLowPercent, 100.0); }; + void setLowWaterPercentage(F32 LowPct) { mLowPercent = llclamp(LowPct, 0.0, mHighPercent); }; + // private: /** @@ -164,7 +182,8 @@ class LLDiskCache : * directory. Primarily used here to determine the directory size * before and after the cache purge */ - uintmax_t dirFileSize(const std::string& dir); + uintmax_t updateCacheSize(const uintmax_t newsize); // enable time based caching of dirfilesize except when force is true. + uintmax_t dirFileSize(const std::string& dir, bool force=false); // enable time based caching of dirfilesize except when force is true. /** * Utility function to convert an LLAssetType enum into a @@ -172,6 +191,13 @@ class LLDiskCache : */ const std::string assetTypeToString(LLAssetType::EType at); + /** + * cache the directory size cos it takes forever to calculate it + * + */ + uintmax_t mStoredCacheSize{ 0 }; + time_point mLastScanTime{ }; + private: /** * The maximum size of the cache in bytes. After purge is called, the @@ -179,6 +205,10 @@ class LLDiskCache : * less than this value */ uintmax_t mMaxSizeBytes; + // High/Low water control + F32 mHighPercent { 95.0 }; + F32 mLowPercent { 70.0 }; + // /** * The folder that holds the cached files. The consumer of this diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 4dcf7dd3c3..9f9ac1049d 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -2952,6 +2952,28 @@ Value 2048 + FSDiskCacheHighWaterPercent + + Comment + Trigger point above which we should start to clear out older cache entries + Persist + 1 + Type + F32 + Value + 95.0 + + FSDiskCacheLowWaterPercent + + Comment + Level to drain cache to once it goes over the high water limit. + Persist + 1 + Type + F32 + Value + 70.0 + CacheLocation Comment diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 486ead0f37..5fb5a21137 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -5195,7 +5195,10 @@ bool LLAppViewer::initCache() // const std::string cache_dir = gDirUtilp->getExpandedFilename(LL_PATH_CACHE, cache_dir_name); - LLDiskCache::initParamSingleton(cache_dir, disk_cache_size, enable_cache_debug_info); + // Improve cache purge triggering + // LLDiskCache::initParamSingleton(cache_dir, disk_cache_size, enable_cache_debug_info); + LLDiskCache::initParamSingleton(cache_dir, disk_cache_size, enable_cache_debug_info, gSavedSettings.getF32("FSDiskCacheHighWaterPercent"), gSavedSettings.getF32("FSDiskCacheLowWaterPercent")); + // if (!read_only) { diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index f3b43244c4..26c0a04254 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -1196,7 +1196,7 @@ DWORD WINAPI purgeThread( LPVOID lpParameter ) for( auto dir : vctDirs ) { - LL_INFOS("CachePurge") << "Removing an old cache" << LL_ENDL; + LL_INFOS("LLDiskCache") << "Removing an old cache" << LL_ENDL; // consistent tagging to help searching log files deleteCacheDirectory( dir ); } @@ -1214,7 +1214,7 @@ void LLAppViewerWin32::startCachePurge() if( !hThread ) { - LL_WARNS("CachePurge") << "CreateThread failed: " << GetLastError() << LL_ENDL; + LL_WARNS("LLDiskCache") << "CreateThread failed: " << GetLastError() << LL_ENDL; // consistent tagging to help searching log files } else SetThreadPriority( hThread, THREAD_MODE_BACKGROUND_BEGIN ); diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 85203f2503..a22a5734a4 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -1087,6 +1087,20 @@ void handleDiskCacheSizeChanged(const LLSD& newValue) } // +// Better asset cache purge control +void handleDiskCacheHighWaterPctChanged(const LLSD& newValue) +{ + const auto new_high = newValue.asReal(); + LLDiskCache::getInstance()->setHighWaterPercentage(new_high); +} + +void handleDiskCacheLowWaterPctChanged(const LLSD& newValue) +{ + const auto new_low = newValue.asReal(); + LLDiskCache::getInstance()->setHighWaterPercentage(new_low); +} +// + void handleTargetFPSChanged(const LLSD& newValue) { const auto targetFPS = gSavedSettings.getU32("TargetFPS"); @@ -1464,7 +1478,10 @@ void settings_setup_listeners() // Better asset cache size control setting_setup_signal_listener(gSavedSettings, "FSDiskCacheSize", handleDiskCacheSizeChanged); - + // Better asset cache purge control + setting_setup_signal_listener(gSavedSettings, "FSDiskCacheHighWaterPercent", handleDiskCacheHighWaterPctChanged); + setting_setup_signal_listener(gSavedSettings, "FSDiskCacheHighWaterPercent", handleDiskCacheLowWaterPctChanged); + // // Handle IME text input getting enabled or disabled #if LL_SDL2 From 4352952d5f9d4dc81b57adcec4b85c246d77b202 Mon Sep 17 00:00:00 2001 From: Beq Date: Wed, 28 Jun 2023 01:53:31 +0100 Subject: [PATCH 04/12] FIRE-33106 muted objects are not greyed out in the history when shown When the show muted text in chat history is enabled the chat from blocked objcets has no differentiation. Adding yet another colour into the mix makes no sense, if we make them both grey then we cannot easily differentiate users/objects so for now I propose simply not persisting this so that when a user accidentally enabels this is goes away again with a relog. --- indra/newview/app_settings/settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 9f9ac1049d..ab1ac86fb6 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -23003,7 +23003,7 @@ Change of this parameter will affect the layout of buttons in notification toast Comment Shows the muted text in nearby chat transcript if enabled. Persist - 1 + 0 Type Boolean Value From 0f14775cc88d34870899f5cc109e11e813aeb517 Mon Sep 17 00:00:00 2001 From: PanteraPolnocy Date: Wed, 28 Jun 2023 11:12:19 +0200 Subject: [PATCH 05/12] FIRE-33042, FIRE-33041: French (by Laurent Bechir), Russian (by Romka Swallowtail) and Polish translation updates --- .../skins/default/xui/fr/panel_preferences_move.xml | 2 +- indra/newview/skins/default/xui/fr/sidepanel_inventory.xml | 7 ++++--- indra/newview/skins/default/xui/pl/sidepanel_inventory.xml | 1 + .../skins/default/xui/ru/panel_preferences_move.xml | 2 +- indra/newview/skins/default/xui/ru/sidepanel_inventory.xml | 1 + 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/indra/newview/skins/default/xui/fr/panel_preferences_move.xml b/indra/newview/skins/default/xui/fr/panel_preferences_move.xml index 9f9ea3eae8..6acc2dbe74 100644 --- a/indra/newview/skins/default/xui/fr/panel_preferences_move.xml +++ b/indra/newview/skins/default/xui/fr/panel_preferences_move.xml @@ -38,7 +38,7 @@ - + diff --git a/indra/newview/skins/default/xui/fr/sidepanel_inventory.xml b/indra/newview/skins/default/xui/fr/sidepanel_inventory.xml index 5a60e9ca30..6afcf572bf 100644 --- a/indra/newview/skins/default/xui/fr/sidepanel_inventory.xml +++ b/indra/newview/skins/default/xui/fr/sidepanel_inventory.xml @@ -4,9 +4,10 @@ - Objets reçus ([NUM]) - Objets reçus -