Ansariel 2023-06-28 14:13:01 +02:00
commit b0e9ce8dc4
13 changed files with 262 additions and 75 deletions

View File

@ -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

View File

@ -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
// <FS:Beq> Add High/Low water mark support
,const F32 highwater_mark_percent
,const F32 lowwater_mark_percent
// </FS:Beq>
) :
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; // <FS:Beq/> try to make simple cache less naive.
if (boost::filesystem::is_directory(cache_path, ec) && !ec.failed())
{
// <FS:Ansariel> Optimize asset simple disk cache
@ -137,6 +144,7 @@ void LLDiskCache::purge()
{
continue;
}
file_size_total += file_size; // <FS:Beq/> 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()
}
}
// <FS:Beq> 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.
// </FS:Beq>
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; // <FS:Beq/> sort oldest to newest, to we can remove the oldest files first.
});
LL_INFOS() << "Purging cache to a maximum of " << mMaxSizeBytes << " bytes" << LL_ENDL;
// <FS:Beq> 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;
// </FS:Beq>
// <FS:Beq> Extra accounting to track the retention of static assets
//std::vector<bool> file_removed;
std::vector<S32> file_removed;
int keep{0};
int del{0};
int skip{0};
enum class purge_action { delete_file=0, keep_file, skip_file };
std::map<std::string,purge_action> file_removed;
auto keep{file_info.size()};
auto del{0};
auto skip{0};
// </FS:Beq>
if (mEnableCacheDebugInfo)
{
file_removed.reserve(file_info.size());
}
uintmax_t file_size_total = 0;
// <FS:Beq> 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;
// // <FS> 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=" <<uuid_as_string<< LL_ENDL;
// if (std::find(mSkipList.begin(), mSkipList.end(), uuid_as_string) != mSkipList.end())
// {
// // this is one of our protected items so no purging
// should_remove = false;
// action = 2;
// updateFileAccessTime(entry.second.second); // force these to the front of the list next time so that purge size works
// }
// }
// // </FS>
// if (mEnableCacheDebugInfo)
// {
// // <FS> 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;
// <FS> 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=" <<uuid_as_string<< LL_ENDL;
if (std::find(mSkipList.begin(), mSkipList.end(), uuid_as_string) != mSkipList.end())
{
// this is one of our protected items so no purging
should_remove = false;
action = 2;
updateFileAccessTime(entry.second.second); // force these to the front of the list next time so that purge size works
}
break;
}
// </FS>
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=" <<uuid_as_string<< LL_ENDL;
if (std::find(mSkipList.begin(), mSkipList.end(), uuid_as_string) != mSkipList.end())
{
// this is one of our protected items so no purging
should_remove = false;
action = purge_action::skip_file;
updateFileAccessTime(entry.second.second); // force these to the front of the list next time so that purge size works
skip++;
}
else{
del++;
}
keep--;
if (mEnableCacheDebugInfo)
{
// <FS> Static asset stuff
//file_removed.push_back(should_remove);
file_removed.push_back(action);
// <FS> Static asset stuff
//file_removed.push_back(should_remove);
file_removed.emplace(entry.second.second, action);
}
// </FS>
if (should_remove)
{
boost::filesystem::remove(entry.second.second, ec);
@ -200,37 +262,52 @@ void LLDiskCache::purge()
}
}
}
// <FS:Beq> update the debug logging to be more useful
auto end_time = std::chrono::high_resolution_clock::now();
auto execute_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
// </FS:Beq>
if (mEnableCacheDebugInfo)
{
auto end_time = std::chrono::high_resolution_clock::now();
auto execute_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
// <FS:Beq> update the debug logging to be more useful
// auto end_time = std::chrono::high_resolution_clock::now();
// auto execute_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
// </FS:Beq>
// 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; // <FS:Beq/> 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];
// <FS> Static asset stuff
deleted_so_far += entry.second.first; // <FS:Beq/> 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";
}
// </FS>
// 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 << ")"; // <FS:Beq/> 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; // <FS:Beq/> Extra accounting to track the retention of static assets
// <FS:Beq> make the summary stats more easily enabled.
}
// <FS:Beq> 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;
// </FS:Beq>
LL_INFOS("LLDiskCache") << "Deleted: " << del << " Skipped: " << skip << " Kept: " << keep << LL_ENDL; // <FS:Beq/> Extra accounting to track the retention of static assets
LL_INFOS("LLDiskCache") << "Total of " << deleted_size_total << " bytes removed." << LL_ENDL; // <FS:Beq/> Extra accounting to track the retention of static assets
// } <FS:Beq/> 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)
// <FS:Beq> 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<seconds>(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;
}
// </FS:Beq>
uintmax_t total_file_size = 0;
/**
@ -577,7 +686,10 @@ uintmax_t LLDiskCache::dirFileSize(const std::string& dir)
}
}
return total_file_size;
// <FS:Beq> Lets not scan every single time if we can avoid it eh?
// return total_file_size;
return updateCacheSize(total_file_size);
// </FS:Beq>
}
LLPurgeDiskCacheThread::LLPurgeDiskCacheThread() :

View File

@ -63,6 +63,9 @@
#define _LLDISKCACHE
#include "llsingleton.h"
#include <chrono>
using namespace std::chrono;
class LLDiskCache :
public LLParamSingleton<LLDiskCache>
@ -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,
// <FS:Beq> 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
// </FS:Beq>
);
virtual ~LLDiskCache() = default;
@ -157,6 +171,10 @@ class LLDiskCache :
// <FS:Ansariel> Better asset cache size control
void setMaxSizeBytes(uintmax_t size) { mMaxSizeBytes = size; }
// <FS:Beq> High/Low water control
void setHighWaterPercentage(F32 HiPct) { mHighPercent = llclamp(HiPct, mLowPercent, 100.0); };
void setLowWaterPercentage(F32 LowPct) { mLowPercent = llclamp(LowPct, 0.0, mHighPercent); };
// </FS:Beq>
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); // <FS:Beq/> enable time based caching of dirfilesize except when force is true.
uintmax_t dirFileSize(const std::string& dir, bool force=false); // <FS:Beq/> 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<system_clock> 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;
// <FS:Beq> High/Low water control
F32 mHighPercent { 95.0 };
F32 mLowPercent { 70.0 };
// </FS:Beq>
/**
* The folder that holds the cached files. The consumer of this

View File

@ -2952,6 +2952,28 @@
<key>Value</key>
<integer>2048</integer>
</map>
<key>FSDiskCacheHighWaterPercent</key>
<map>
<key>Comment</key>
<string>Trigger point above which we should start to clear out older cache entries</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<integer>95.0</integer>
</map>
<key>FSDiskCacheLowWaterPercent</key>
<map>
<key>Comment</key>
<string>Level to drain cache to once it goes over the high water limit.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
<integer>70.0</integer>
</map>
<key>CacheLocation</key>
<map>
<key>Comment</key>
@ -23003,7 +23025,7 @@ Change of this parameter will affect the layout of buttons in notification toast
<key>Comment</key>
<string>Shows the muted text in nearby chat transcript if enabled.</string>
<key>Persist</key>
<integer>1</integer>
<integer>0</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>

View File

@ -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<void(const std::vector<LLSD>& 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<Updater> mRadarListUpdater;
@ -125,7 +125,7 @@ private:
bool lastIgnore;
};
typedef boost::unordered_map<LLUUID, RadarFields, FSUUIDHash> radarfields_map_t;
typedef std::unordered_map<LLUUID, RadarFields, FSUUIDHash> radarfields_map_t;
radarfields_map_t mLastRadarSweep;
entry_map_t mEntryList;

View File

@ -5036,7 +5036,10 @@ bool LLAppViewer::initCache()
// </FS:Ansariel>
const std::string cache_dir = gDirUtilp->getExpandedFilename(LL_PATH_CACHE, cache_dir_name);
LLDiskCache::initParamSingleton(cache_dir, disk_cache_size, enable_cache_debug_info);
// <FS:Beq> 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"));
// </FS:Beq>
if (!read_only)
{

View File

@ -1194,7 +1194,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; // <FS:Beq/> consistent tagging to help searching log files
deleteCacheDirectory( dir );
}
@ -1212,7 +1212,7 @@ void LLAppViewerWin32::startCachePurge()
if( !hThread )
{
LL_WARNS("CachePurge") << "CreateThread failed: " << GetLastError() << LL_ENDL;
LL_WARNS("LLDiskCache") << "CreateThread failed: " << GetLastError() << LL_ENDL; // <FS:Beq/> consistent tagging to help searching log files
}
else
SetThreadPriority( hThread, THREAD_MODE_BACKGROUND_BEGIN );

View File

@ -1087,6 +1087,20 @@ void handleDiskCacheSizeChanged(const LLSD& newValue)
}
// </FS:Ansariel>
// <FS:Beq> 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);
}
// </FS:Beq>
void handleTargetFPSChanged(const LLSD& newValue)
{
const auto targetFPS = gSavedSettings.getU32("TargetFPS");
@ -1464,7 +1478,10 @@ void settings_setup_listeners()
// <FS:Ansariel> Better asset cache size control
setting_setup_signal_listener(gSavedSettings, "FSDiskCacheSize", handleDiskCacheSizeChanged);
// <FS:Beq> Better asset cache purge control
setting_setup_signal_listener(gSavedSettings, "FSDiskCacheHighWaterPercent", handleDiskCacheHighWaterPctChanged);
setting_setup_signal_listener(gSavedSettings, "FSDiskCacheHighWaterPercent", handleDiskCacheLowWaterPctChanged);
// </FS:Beq>
// <FS:Zi> Handle IME text input getting enabled or disabled
#if LL_SDL2

View File

@ -38,7 +38,7 @@
<check_box label="Inverser l'axe vertical" name="invert_mouse"/>
</panel>
<panel label="Mouvement" name="tab-movement">
<check_box label="Les flèches directionnelles déplacent toujours mon avatar" name="arrow_keys_move_avatar_check"/>
<check_box label="Les flèches directionnelles déplacent toujours mon avatar" name="arrow_keys_move_avatar_check" tool_tip="S'applique uniquement au champ de saisie du Chat local situé au bas du visualiseur, et non à l'onglet Chat local dans Conversations."/>
<check_box label="Appuyer sur les touches alphabétiques de déplacement (WASD) déplace mon avatar" name="LetterKeysAffectsMovementNotFocusChatBar"/>
<check_box label="Double-pression sur une touche directionnelle pour courir" name="tap_tap_hold_to_run"/>
<check_box label="Décoller / Atterrir en maintenant les touches Saut / Accroupi" name="automatic_fly"/>

View File

@ -4,9 +4,10 @@
<layout_stack name="inventory_layout_stack">
<layout_panel name="inbox_layout_panel">
<panel name="marketplace_inbox">
<string name="InboxLabelWithArg">Objets reçus ([NUM])</string>
<string name="InboxLabelNoArg">Objets reçus</string>
<button label="Objets reçus" name="inbox_btn"/>
<string name="InboxLabelWithArg">Achats reçus ([NUM])</string>
<string name="InboxLabelNoArg">Achats reçus</string>
<button label="Achats reçus" name="inbox_btn"/>
<button name="reload_received_items_btn" tool_tip="Recharger la liste des achats reçus."/>
<text name="inbox_fresh_new_count">[NUM] nouveaux</text>
<panel name="inbox_inventory_placeholder_panel" tool_tip="Glissez les objets dans votre inventaire pour les utiliser"><text name="inbox_inventory_placeholder">Les achats du Marketplace seront livrés ici.</text></panel>
</panel>

View File

@ -11,6 +11,7 @@
Odebrane przedmioty
</string>
<button label="Odebrane przedmioty" name="inbox_btn"/>
<button name="reload_received_items_btn" tool_tip="Załaduj ponownie listę odebranych przedmiotów." />
<text name="inbox_fresh_new_count">
[NUM] nowe/y
</text>

View File

@ -43,7 +43,7 @@
<check_box label="Инвертировать" name="invert_mouse"/>
</panel>
<panel label="Движение" name="tab-movement">
<check_box label="Клавиши со стрелками всегда перемещают меня" name="arrow_keys_move_avatar_check"/>
<check_box label="Клавиши со стрелками всегда перемещают меня" name="arrow_keys_move_avatar_check" tool_tip="Применяется только к полю ввода 'Общий чат' в нижней части окна просмотра, а не к вкладке 'Общий чат' в диалогах."/>
<check_box label="Буквенные клавиши влияют на движение не запуская локальный чат (т.e. WASD)" name="LetterKeysAffectsMovementNotFocusChatBar"/>
<check_box label="Двойное нажатие для бега" name="tap_tap_hold_to_run"/>
<check_box label="Летать / Приземлиться (верх (прыжок) / вниз (присесть))" name="automatic_fly"/>

View File

@ -11,6 +11,7 @@
Полученные вещи
</string>
<button label="Полученные вещи" name="inbox_btn"/>
<button name="reload_received_items_btn" tool_tip="Перезагрузить список полученных предметов."/>
<text name="inbox_fresh_new_count">
Новых: [NUM]
</text>