mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-01-13 09:17:18 +00:00
fix(Core/AuctionHouse): Fix AH searches with high number of auctions (#13467)
Fix AH searches with high number of auctions
This commit is contained in:
@@ -113,7 +113,6 @@ bool LoadRealmInfo(Acore::Asio::IoContext& ioContext);
|
||||
AsyncAcceptor* StartRaSocketAcceptor(Acore::Asio::IoContext& ioContext);
|
||||
void ShutdownCLIThread(std::thread* cliThread);
|
||||
void AuctionListingRunnable();
|
||||
void ShutdownAuctionListingThread(std::thread* thread);
|
||||
void WorldUpdateLoop();
|
||||
variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile, [[maybe_unused]] std::string& cfg_service);
|
||||
|
||||
@@ -398,9 +397,9 @@ int main(int argc, char** argv)
|
||||
cliThread.reset(new std::thread(CliThread), &ShutdownCLIThread);
|
||||
}
|
||||
|
||||
// Launch CliRunnable thread
|
||||
std::shared_ptr<std::thread> auctionLisingThread;
|
||||
auctionLisingThread.reset(new std::thread(AuctionListingRunnable),
|
||||
// Launch auction listing thread
|
||||
std::shared_ptr<std::thread> auctionListingThread;
|
||||
auctionListingThread.reset(new std::thread(AuctionListingRunnable),
|
||||
[](std::thread* thr)
|
||||
{
|
||||
thr->join();
|
||||
@@ -717,42 +716,41 @@ void AuctionListingRunnable()
|
||||
|
||||
while (!World::IsStopped())
|
||||
{
|
||||
if (AsyncAuctionListingMgr::IsAuctionListingAllowed())
|
||||
Milliseconds diff = AsyncAuctionListingMgr::GetDiff();
|
||||
AsyncAuctionListingMgr::ResetDiff();
|
||||
|
||||
if (!AsyncAuctionListingMgr::GetTempList().empty() || !AsyncAuctionListingMgr::GetList().empty())
|
||||
{
|
||||
uint32 diff = AsyncAuctionListingMgr::GetDiff();
|
||||
AsyncAuctionListingMgr::ResetDiff();
|
||||
|
||||
if (AsyncAuctionListingMgr::GetTempList().size() || AsyncAuctionListingMgr::GetList().size())
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(AsyncAuctionListingMgr::GetLock());
|
||||
std::lock_guard<std::mutex> guard(AsyncAuctionListingMgr::GetTempLock());
|
||||
|
||||
for (auto const& delayEvent: AsyncAuctionListingMgr::GetTempList())
|
||||
AsyncAuctionListingMgr::GetList().emplace_back(delayEvent);
|
||||
|
||||
AsyncAuctionListingMgr::GetTempList().clear();
|
||||
}
|
||||
|
||||
for (auto& itr: AsyncAuctionListingMgr::GetList())
|
||||
{
|
||||
if (itr._pickupTimer <= diff)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(AsyncAuctionListingMgr::GetTempLock());
|
||||
|
||||
for (auto const& delayEvent : AsyncAuctionListingMgr::GetTempList())
|
||||
AsyncAuctionListingMgr::GetList().emplace_back(delayEvent);
|
||||
|
||||
AsyncAuctionListingMgr::GetTempList().clear();
|
||||
itr._pickupTimer = Milliseconds::zero();
|
||||
}
|
||||
|
||||
for (auto& itr : AsyncAuctionListingMgr::GetList())
|
||||
else
|
||||
{
|
||||
if (itr._msTimer <= diff)
|
||||
itr._msTimer = 0;
|
||||
else
|
||||
itr._msTimer -= diff;
|
||||
itr._pickupTimer -= diff;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::list<AuctionListItemsDelayEvent>::iterator itr = AsyncAuctionListingMgr::GetList().begin(); itr != AsyncAuctionListingMgr::GetList().end(); ++itr)
|
||||
{
|
||||
if ((*itr)._msTimer != 0)
|
||||
continue;
|
||||
for (auto itr = AsyncAuctionListingMgr::GetList().begin(); itr != AsyncAuctionListingMgr::GetList().end(); ++itr)
|
||||
{
|
||||
if ((*itr)._pickupTimer != Milliseconds::zero())
|
||||
continue;
|
||||
|
||||
if ((*itr).Execute())
|
||||
AsyncAuctionListingMgr::GetList().erase(itr);
|
||||
if ((*itr).Execute())
|
||||
AsyncAuctionListingMgr::GetList().erase(itr);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(1ms);
|
||||
@@ -761,15 +759,6 @@ void AuctionListingRunnable()
|
||||
LOG_INFO("server", "Auction House Listing thread exiting without problems.");
|
||||
}
|
||||
|
||||
void ShutdownAuctionListingThread(std::thread* thread)
|
||||
{
|
||||
if (thread)
|
||||
{
|
||||
thread->join();
|
||||
delete thread;
|
||||
}
|
||||
}
|
||||
|
||||
variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile, [[maybe_unused]] std::string& configService)
|
||||
{
|
||||
options_description all("Allowed options");
|
||||
|
||||
@@ -3843,6 +3843,13 @@ ChangeFaction.MaxMoney = 0
|
||||
|
||||
Pet.RankMod.Health = 1
|
||||
|
||||
#
|
||||
# AuctionHouse.SearchTimeout
|
||||
# Description: Time (in milliseconds) after which an auction house search is discarded.
|
||||
# Default: 1000 - (1 second)
|
||||
|
||||
AuctionHouse.SearchTimeout = 1000
|
||||
|
||||
#
|
||||
###################################################################################################
|
||||
|
||||
|
||||
@@ -727,7 +727,7 @@ void AuctionHouseObject::BuildListOwnerItems(WorldPacket& data, Player* player,
|
||||
bool AuctionHouseObject::BuildListAuctionItems(WorldPacket& data, Player* player,
|
||||
std::wstring const& wsearchedname, uint32 listfrom, uint8 levelmin, uint8 levelmax, uint8 usable,
|
||||
uint32 inventoryType, uint32 itemClass, uint32 itemSubClass, uint32 quality,
|
||||
uint32& count, uint32& totalcount, uint8 /*getAll*/, AuctionSortOrderVector const& sortOrder)
|
||||
uint32& count, uint32& totalcount, uint8 /*getAll*/, AuctionSortOrderVector const& sortOrder, Milliseconds searchTimeout)
|
||||
{
|
||||
uint32 itrcounter = 0;
|
||||
|
||||
@@ -754,14 +754,11 @@ bool AuctionHouseObject::BuildListAuctionItems(WorldPacket& data, Player* player
|
||||
|
||||
for (AuctionEntryMap::const_iterator itr = _auctionsMap.begin(); itr != _auctionsMap.end(); ++itr)
|
||||
{
|
||||
if (!AsyncAuctionListingMgr::IsAuctionListingAllowed()) // pussywizard: World::Update is waiting for us...
|
||||
if ((itrcounter++) % 100 == 0) // check condition every 100 iterations
|
||||
{
|
||||
if ((itrcounter++) % 100 == 0) // check condition every 100 iterations
|
||||
if (GetMSTimeDiff(GameTime::GetGameTimeMS(), GetTimeMS()) >= searchTimeout) // pussywizard: stop immediately if diff is high or waiting too long
|
||||
{
|
||||
if (sWorldUpdateTime.GetAverageUpdateTime() >= 30 || GetMSTimeDiff(GameTime::GetGameTimeMS(), GetTimeMS()) >= 10ms) // pussywizard: stop immediately if diff is high or waiting too long
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ public:
|
||||
bool BuildListAuctionItems(WorldPacket& data, Player* player,
|
||||
std::wstring const& searchedname, uint32 listfrom, uint8 levelmin, uint8 levelmax, uint8 usable,
|
||||
uint32 inventoryType, uint32 itemClass, uint32 itemSubClass, uint32 quality,
|
||||
uint32& count, uint32& totalcount, uint8 getAll, AuctionSortOrderVector const& sortOrder);
|
||||
uint32& count, uint32& totalcount, uint8 getAll, AuctionSortOrderVector const& sortOrder, Milliseconds searchTimeout);
|
||||
|
||||
private:
|
||||
AuctionEntryMap _auctionsMap;
|
||||
@@ -184,7 +184,6 @@ public:
|
||||
|
||||
AuctionHouseObject* GetAuctionsMap(uint32 factionTemplateId);
|
||||
AuctionHouseObject* GetAuctionsMapByHouseId(uint8 auctionHouseId);
|
||||
AuctionHouseObject* GetBidsMap(uint32 factionTemplateId);
|
||||
|
||||
Item* GetAItem(ObjectGuid itemGuid)
|
||||
{
|
||||
|
||||
@@ -668,23 +668,24 @@ void WorldSession::HandleAuctionListOwnerItems(WorldPacket& recvData)
|
||||
recvData >> listfrom; // not used in fact (this list does not have page control in client)
|
||||
|
||||
// pussywizard:
|
||||
const uint32 delay = 4500;
|
||||
const uint32 now = GameTime::GetGameTimeMS().count();
|
||||
const Milliseconds now = GameTime::GetGameTimeMS();
|
||||
if (_lastAuctionListOwnerItemsMSTime > now) // list is pending
|
||||
return;
|
||||
uint32 diff = getMSTimeDiff(_lastAuctionListOwnerItemsMSTime, now);
|
||||
|
||||
const Milliseconds delay = Milliseconds(4500);
|
||||
Milliseconds diff = GetMSTimeDiff(_lastAuctionListOwnerItemsMSTime, now);
|
||||
if (diff > delay)
|
||||
diff = delay;
|
||||
|
||||
_lastAuctionListOwnerItemsMSTime = now + delay; // set longest possible here, actual exectuing will change this to getMSTime of that moment
|
||||
_player->m_Events.AddEvent(new AuctionListOwnerItemsDelayEvent(guid, _player->GetGUID(), true), _player->m_Events.CalculateTime(delay - diff));
|
||||
_lastAuctionListOwnerItemsMSTime = now + delay; // set longest possible here, actual executing will change this to getMSTime of that moment
|
||||
_player->m_Events.AddEvent(new AuctionListOwnerItemsDelayEvent(guid, _player->GetGUID()), _player->m_Events.CalculateTime(delay.count() - diff.count()));
|
||||
}
|
||||
|
||||
void WorldSession::HandleAuctionListOwnerItemsEvent(ObjectGuid creatureGuid)
|
||||
{
|
||||
LOG_DEBUG("network", "WORLD: Received CMSG_AUCTION_LIST_OWNER_ITEMS");
|
||||
|
||||
_lastAuctionListOwnerItemsMSTime = GameTime::GetGameTimeMS().count(); // pussywizard
|
||||
_lastAuctionListOwnerItemsMSTime = GameTime::GetGameTimeMS(); // pussywizard
|
||||
|
||||
Creature* creature = GetPlayer()->GetNPCIfCanInteractWith(creatureGuid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
if (!creature)
|
||||
@@ -757,17 +758,17 @@ void WorldSession::HandleAuctionListItems(WorldPacket& recvData)
|
||||
}
|
||||
|
||||
// pussywizard:
|
||||
const uint32 delay = 2000;
|
||||
const uint32 now = GameTime::GetGameTimeMS().count();
|
||||
uint32 diff = getMSTimeDiff(_lastAuctionListItemsMSTime, now);
|
||||
const Milliseconds delay = 2s;
|
||||
const Milliseconds now = GameTime::GetGameTimeMS();
|
||||
Milliseconds diff = GetMSTimeDiff(_lastAuctionListItemsMSTime, now);
|
||||
if (diff > delay)
|
||||
{
|
||||
diff = delay;
|
||||
}
|
||||
_lastAuctionListItemsMSTime = now + delay - diff;
|
||||
std::lock_guard<std::mutex> guard(AsyncAuctionListingMgr::GetTempLock());
|
||||
AsyncAuctionListingMgr::GetTempList().push_back(AuctionListItemsDelayEvent(delay - diff, _player->GetGUID(), guid, searchedname, listfrom, levelmin, levelmax, usable, auctionSlotID,
|
||||
auctionMainCategory, auctionSubCategory, quality, getAll, sortOrder));
|
||||
AsyncAuctionListingMgr::GetTempList().emplace_back(delay - diff, _player->GetGUID(), guid, searchedname, listfrom, levelmin, levelmax, usable, auctionSlotID,
|
||||
auctionMainCategory, auctionSubCategory, quality, getAll, sortOrder);
|
||||
}
|
||||
|
||||
void WorldSession::HandleAuctionListPendingSales(WorldPacket& recvData)
|
||||
|
||||
@@ -22,11 +22,9 @@
|
||||
#include "Player.h"
|
||||
#include "SpellAuraEffects.h"
|
||||
|
||||
uint32 AsyncAuctionListingMgr::auctionListingDiff = 0;
|
||||
bool AsyncAuctionListingMgr::auctionListingAllowed = false;
|
||||
Milliseconds AsyncAuctionListingMgr::auctionListingDiff = Milliseconds::zero();
|
||||
std::list<AuctionListItemsDelayEvent> AsyncAuctionListingMgr::auctionListingList;
|
||||
std::list<AuctionListItemsDelayEvent> AsyncAuctionListingMgr::auctionListingListTemp;
|
||||
std::mutex AsyncAuctionListingMgr::auctionListingLock;
|
||||
std::mutex AsyncAuctionListingMgr::auctionListingTempLock;
|
||||
|
||||
bool AuctionListOwnerItemsDelayEvent::Execute(uint64 /*e_time*/, uint32 /*p_time*/)
|
||||
@@ -60,18 +58,19 @@ bool AuctionListItemsDelayEvent::Execute()
|
||||
|
||||
wstrToLower(wsearchedname);
|
||||
|
||||
uint32 searchTimeout = sWorld->getIntConfig(CONFIG_AUCTION_HOUSE_SEARCH_TIMEOUT);
|
||||
bool result = auctionHouse->BuildListAuctionItems(data, plr,
|
||||
wsearchedname, _listfrom, _levelmin, _levelmax, _usable,
|
||||
_auctionSlotID, _auctionMainCategory, _auctionSubCategory, _quality,
|
||||
count, totalcount, _getAll, _sortOrder);
|
||||
count, totalcount, _getAll, _sortOrder, Milliseconds(searchTimeout));
|
||||
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
data.put<uint32>(0, count);
|
||||
data << (uint32) totalcount;
|
||||
data << (uint32) 300; // clientside search cooldown [ms] (gray search button)
|
||||
plr->GetSession()->SendPacket(&data);
|
||||
if (result)
|
||||
{
|
||||
data.put<uint32>(0, count);
|
||||
data << (uint32) totalcount;
|
||||
data << (uint32) 300; // clientside search cooldown [ms] (gray search button)
|
||||
plr->GetSession()->SendPacket(&data);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,30 +25,28 @@
|
||||
class AuctionListOwnerItemsDelayEvent : public BasicEvent
|
||||
{
|
||||
public:
|
||||
AuctionListOwnerItemsDelayEvent(ObjectGuid _creatureGuid, ObjectGuid guid, bool o) : creatureGuid(_creatureGuid), playerguid(guid), owner(o) {}
|
||||
AuctionListOwnerItemsDelayEvent(ObjectGuid _creatureGuid, ObjectGuid guid) : creatureGuid(_creatureGuid), playerguid(guid) {}
|
||||
~AuctionListOwnerItemsDelayEvent() override {}
|
||||
|
||||
bool Execute(uint64 e_time, uint32 p_time) override;
|
||||
void Abort(uint64 /*e_time*/) override {}
|
||||
bool getOwner() { return owner; }
|
||||
|
||||
private:
|
||||
ObjectGuid creatureGuid;
|
||||
ObjectGuid playerguid;
|
||||
bool owner;
|
||||
};
|
||||
|
||||
class AuctionListItemsDelayEvent
|
||||
{
|
||||
public:
|
||||
AuctionListItemsDelayEvent(uint32 msTimer, ObjectGuid playerguid, ObjectGuid creatureguid, std::string searchedname, uint32 listfrom, uint8 levelmin, uint8 levelmax,
|
||||
AuctionListItemsDelayEvent(Milliseconds pickupTimer, ObjectGuid playerguid, ObjectGuid creatureguid, std::string searchedname, uint32 listfrom, uint8 levelmin, uint8 levelmax,
|
||||
uint8 usable, uint32 auctionSlotID, uint32 auctionMainCategory, uint32 auctionSubCategory, uint32 quality, uint8 getAll, AuctionSortOrderVector sortOrder) :
|
||||
_msTimer(msTimer), _playerguid(playerguid), _creatureguid(creatureguid), _searchedname(searchedname), _listfrom(listfrom), _levelmin(levelmin), _levelmax(levelmax),_usable(usable),
|
||||
_pickupTimer(pickupTimer), _playerguid(playerguid), _creatureguid(creatureguid), _searchedname(searchedname), _listfrom(listfrom), _levelmin(levelmin), _levelmax(levelmax),_usable(usable),
|
||||
_auctionSlotID(auctionSlotID), _auctionMainCategory(auctionMainCategory), _auctionSubCategory(auctionSubCategory), _quality(quality), _getAll(getAll), _sortOrder(sortOrder) { }
|
||||
|
||||
bool Execute();
|
||||
|
||||
uint32 _msTimer;
|
||||
Milliseconds _pickupTimer;
|
||||
ObjectGuid _playerguid;
|
||||
ObjectGuid _creatureguid;
|
||||
std::string _searchedname;
|
||||
@@ -67,23 +65,17 @@ public:
|
||||
class AsyncAuctionListingMgr
|
||||
{
|
||||
public:
|
||||
static void Update(uint32 diff) { auctionListingDiff += diff; }
|
||||
static uint32 GetDiff() { return auctionListingDiff; }
|
||||
static void ResetDiff() { auctionListingDiff = 0; }
|
||||
static bool IsAuctionListingAllowed() { return auctionListingAllowed; }
|
||||
static void SetAuctionListingAllowed(bool a) { auctionListingAllowed = a; }
|
||||
|
||||
static void Update(Milliseconds diff) { auctionListingDiff += diff; }
|
||||
static Milliseconds GetDiff() { return auctionListingDiff; }
|
||||
static void ResetDiff() { auctionListingDiff = Milliseconds::zero(); }
|
||||
static std::list<AuctionListItemsDelayEvent>& GetList() { return auctionListingList; }
|
||||
static std::list<AuctionListItemsDelayEvent>& GetTempList() { return auctionListingListTemp; }
|
||||
static std::mutex& GetLock() { return auctionListingLock; }
|
||||
static std::mutex& GetTempLock() { return auctionListingTempLock; }
|
||||
|
||||
private:
|
||||
static uint32 auctionListingDiff;
|
||||
static bool auctionListingAllowed;
|
||||
static Milliseconds auctionListingDiff;
|
||||
static std::list<AuctionListItemsDelayEvent> auctionListingList;
|
||||
static std::list<AuctionListItemsDelayEvent> auctionListingListTemp;
|
||||
static std::mutex auctionListingLock;
|
||||
static std::mutex auctionListingTempLock;
|
||||
};
|
||||
|
||||
|
||||
@@ -1058,8 +1058,8 @@ public: // opcodes handlers
|
||||
void HandleEnterPlayerVehicle(WorldPacket& data);
|
||||
void HandleUpdateProjectilePosition(WorldPacket& recvPacket);
|
||||
|
||||
uint32 _lastAuctionListItemsMSTime;
|
||||
uint32 _lastAuctionListOwnerItemsMSTime;
|
||||
Milliseconds _lastAuctionListItemsMSTime;
|
||||
Milliseconds _lastAuctionListOwnerItemsMSTime;
|
||||
|
||||
void HandleTeleportTimeout(bool updateInSessions);
|
||||
bool HandleSocketClosed();
|
||||
|
||||
@@ -413,6 +413,7 @@ enum WorldIntConfigs
|
||||
CONFIG_LFG_KICK_PREVENTION_TIMER,
|
||||
CONFIG_CHANGE_FACTION_MAX_MONEY,
|
||||
CONFIG_WATER_BREATH_TIMER,
|
||||
CONFIG_AUCTION_HOUSE_SEARCH_TIMEOUT,
|
||||
INT_CONFIG_VALUE_COUNT
|
||||
};
|
||||
|
||||
|
||||
@@ -1285,6 +1285,8 @@ void World::LoadConfigSettings(bool reload)
|
||||
|
||||
_bool_configs[CONFIG_ALLOWS_RANK_MOD_FOR_PET_HEALTH] = sConfigMgr->GetOption<bool>("Pet.RankMod.Health", true);
|
||||
|
||||
_int_configs[CONFIG_AUCTION_HOUSE_SEARCH_TIMEOUT] = sConfigMgr->GetOption<uint32>("AuctionHouse.SearchTimeout", 1000);
|
||||
|
||||
///- Read the "Data" directory from the config file
|
||||
std::string dataPath = sConfigMgr->GetOption<std::string>("DataDir", "./");
|
||||
if (dataPath.empty() || (dataPath.at(dataPath.length() - 1) != '/' && dataPath.at(dataPath.length() - 1) != '\\'))
|
||||
@@ -2349,41 +2351,27 @@ void World::Update(uint32 diff)
|
||||
ResetGuildCap();
|
||||
}
|
||||
|
||||
// pussywizard:
|
||||
// acquire mutex now, this is kind of waiting for listing thread to finish it's work (since it can't process next packet)
|
||||
// so we don't have to do it in every packet that modifies auctions
|
||||
AsyncAuctionListingMgr::SetAuctionListingAllowed(false);
|
||||
// pussywizard: handle auctions when the timer has passed
|
||||
if (_timers[WUPDATE_AUCTIONS].Passed())
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(AsyncAuctionListingMgr::GetLock());
|
||||
METRIC_TIMER("world_update_time", METRIC_TAG("type", "Update expired auctions"));
|
||||
|
||||
// pussywizard: handle auctions when the timer has passed
|
||||
if (_timers[WUPDATE_AUCTIONS].Passed())
|
||||
{
|
||||
METRIC_TIMER("world_update_time", METRIC_TAG("type", "Update expired auctions"));
|
||||
_timers[WUPDATE_AUCTIONS].Reset();
|
||||
|
||||
_timers[WUPDATE_AUCTIONS].Reset();
|
||||
|
||||
// pussywizard: handle expired auctions, auctions expired when realm was offline are also handled here (not during loading when many required things aren't loaded yet)
|
||||
sAuctionMgr->Update();
|
||||
}
|
||||
|
||||
AsyncAuctionListingMgr::Update(diff);
|
||||
|
||||
if (currentGameTime > _mail_expire_check_timer)
|
||||
{
|
||||
sObjectMgr->ReturnOrDeleteOldMails(true);
|
||||
_mail_expire_check_timer = currentGameTime + 6h;
|
||||
}
|
||||
|
||||
{
|
||||
/// <li> Handle session updates when the timer has passed
|
||||
METRIC_TIMER("world_update_time", METRIC_TAG("type", "Update sessions"));
|
||||
UpdateSessions(diff);
|
||||
}
|
||||
// pussywizard: handle expired auctions, auctions expired when realm was offline are also handled here (not during loading when many required things aren't loaded yet)
|
||||
sAuctionMgr->Update();
|
||||
}
|
||||
|
||||
// end of section with mutex
|
||||
AsyncAuctionListingMgr::SetAuctionListingAllowed(true);
|
||||
AsyncAuctionListingMgr::Update(Milliseconds(diff));
|
||||
|
||||
if (currentGameTime > _mail_expire_check_timer)
|
||||
{
|
||||
sObjectMgr->ReturnOrDeleteOldMails(true);
|
||||
_mail_expire_check_timer = currentGameTime + 6h;
|
||||
}
|
||||
|
||||
METRIC_TIMER("world_update_time", METRIC_TAG("type", "Update sessions"));
|
||||
UpdateSessions(diff);
|
||||
|
||||
/// <li> Handle weather updates when the timer has passed
|
||||
if (_timers[WUPDATE_WEATHERS].Passed())
|
||||
|
||||
Reference in New Issue
Block a user