mirror of
https://github.com/liyunfan1223/azerothcore-wotlk.git
synced 2026-08-04 13:57:52 +00:00
feat(Core): Implement account play time restrictions (CAIS) (#24576)
Co-authored-by: ratkosrb <35845488+ratkosrb@users.noreply.github.com> Co-authored-by: sudlud <sudlud@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: DDuarte <530940+DDuarte@users.noreply.github.com>
This commit is contained in:
@@ -3421,6 +3421,24 @@ DungeonAccessRequirements.OptionalStringID = 0
|
||||
#
|
||||
###################################################################################################
|
||||
|
||||
###################################################################################################
|
||||
# PLAY TIME LIMIT (CAIS)
|
||||
#
|
||||
# CAIS.Enable
|
||||
# Description: Enable the Chinese realm consecutive play time restrictions. After 3
|
||||
# hours of consecutive play, XP and looted money are halved; after 5
|
||||
# hours, the character can no longer gain XP, loot, or turn in quests.
|
||||
# Staying offline for 5 hours resets the accumulated time. The 3h/5h
|
||||
# thresholds are hardcoded client-side.
|
||||
# Note: A worldserver restart resets the accumulated time for every account.
|
||||
# Default: 0 - Disabled
|
||||
# 1 - Enabled
|
||||
|
||||
CAIS.Enable = 0
|
||||
|
||||
#
|
||||
###################################################################################################
|
||||
|
||||
###################################################################################################
|
||||
# DUNGEON AND BATTLEGROUND FINDER
|
||||
#
|
||||
|
||||
@@ -2393,6 +2393,10 @@ namespace lfg
|
||||
if (!reward)
|
||||
continue;
|
||||
|
||||
// CAIS full restriction: grant no dungeon reward and leave the daily available for later
|
||||
if (player->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
continue;
|
||||
|
||||
bool done = false;
|
||||
Quest const* quest = sObjectMgr->GetQuestTemplate(reward->firstQuest);
|
||||
if (!quest)
|
||||
|
||||
@@ -2402,24 +2402,16 @@ void Player::SendLogXPGain(uint32 GivenXP, Unit* victim, uint32 BonusXP, bool re
|
||||
void Player::GiveXP(uint32 xp, Unit* victim, float group_rate, bool isLFGReward)
|
||||
{
|
||||
if (xp < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAlive() && !GetBattlegroundId() && !isLFGReward)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasPlayerFlag(PLAYER_FLAGS_NO_XP_GAIN))
|
||||
{
|
||||
if (HasPlayerFlag(PLAYER_FLAGS_NO_XP_GAIN) || HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
return;
|
||||
}
|
||||
|
||||
if (victim && victim->IsCreature() && !victim->ToCreature()->hasLootRecipient())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint8 level = GetLevel();
|
||||
sScriptMgr->OnPlayerBeforeGetLevelForXPGain(this, level);
|
||||
@@ -2444,6 +2436,9 @@ void Player::GiveXP(uint32 xp, Unit* victim, float group_rate, bool isLFGReward)
|
||||
if (level >= maxLevel)
|
||||
return;
|
||||
|
||||
if (HasPlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME))
|
||||
xp = std::max(1u, xp / 2);
|
||||
|
||||
uint32 bonus_xp = 0;
|
||||
bool recruitAFriend = GetsRecruitAFriendBonus(true);
|
||||
|
||||
@@ -2456,9 +2451,7 @@ void Player::GiveXP(uint32 xp, Unit* victim, float group_rate, bool isLFGReward)
|
||||
// hooks and multipliers can modify the xp with a zero or negative value
|
||||
// check again before sending invalid xp to the client
|
||||
if (xp < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SendLogXPGain(xp, victim, bonus_xp, recruitAFriend, group_rate);
|
||||
|
||||
|
||||
@@ -468,8 +468,8 @@ enum PlayerFlags : uint32
|
||||
PLAYER_FLAGS_IN_PVP = 0x00000200,
|
||||
PLAYER_FLAGS_HIDE_HELM = 0x00000400,
|
||||
PLAYER_FLAGS_HIDE_CLOAK = 0x00000800,
|
||||
PLAYER_FLAGS_PLAYED_LONG_TIME = 0x00001000, // played long time
|
||||
PLAYER_FLAGS_PLAYED_TOO_LONG = 0x00002000, // played too long time
|
||||
PLAYER_FLAGS_PARTIAL_PLAY_TIME = 0x00001000, // played long time
|
||||
PLAYER_FLAGS_NO_PLAY_TIME = 0x00002000, // played too long time
|
||||
PLAYER_FLAGS_IS_OUT_OF_BOUNDS = 0x00004000,
|
||||
PLAYER_FLAGS_DEVELOPER = 0x00008000, // <Dev> prefix for something?
|
||||
PLAYER_FLAGS_UNK16 = 0x00010000, // pre-3.0.3 PLAYER_FLAGS_SANCTUARY flag for player entered sanctuary
|
||||
|
||||
@@ -474,6 +474,16 @@ bool Player::CanRewardQuest(Quest const* quest, uint32 reward, bool msg)
|
||||
if (!CanRewardQuest(quest, msg))
|
||||
return false;
|
||||
|
||||
// gate the actual turn-in here rather than in the overload above, which LFG also uses
|
||||
// as a "already did today's random" probe
|
||||
if (HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
{
|
||||
if (msg)
|
||||
GetSession()->SendPlayTimeWarning(PTF_UNHEALTHY_TIME, 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ItemPosCountVec dest;
|
||||
if (quest->GetRewChoiceItemsCount() > 0)
|
||||
{
|
||||
@@ -621,6 +631,9 @@ void Player::CompleteQuest(uint32 quest_id)
|
||||
Quest const* qInfo = sObjectMgr->GetQuestTemplate(quest_id);
|
||||
if (qInfo && qInfo->HasFlag(QUEST_FLAGS_TRACKING))
|
||||
{
|
||||
// auto-rewarded, so CAIS cannot gate it here: the status is already COMPLETE above and
|
||||
// nothing re-triggers the quest, which would destroy the reward rather than defer it.
|
||||
// RewardQuest still zeroes the money and GiveXP still blocks the XP.
|
||||
RewardQuest(qInfo, 0, this, false);
|
||||
}
|
||||
|
||||
@@ -766,6 +779,16 @@ void Player::RewardQuest(Quest const* quest, uint32 reward, Object* questGiver,
|
||||
moneyRew += rewOrReqMoney;
|
||||
}
|
||||
|
||||
// CAIS reduces quest money, mirroring looted money and XP. Applied here as well as at the
|
||||
// turn-in gate because LFG and auto-complete quests reach RewardQuest without CanRewardQuest.
|
||||
if (moneyRew > 0)
|
||||
{
|
||||
if (HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
moneyRew = 0;
|
||||
else if (HasPlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME))
|
||||
moneyRew /= 2;
|
||||
}
|
||||
|
||||
if (moneyRew)
|
||||
{
|
||||
ModifyMoney(moneyRew);
|
||||
|
||||
@@ -5098,6 +5098,20 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons
|
||||
SetByteValue(PLAYER_BYTES_3, 0, fields[5].Get<uint8>());
|
||||
SetByteValue(PLAYER_BYTES_3, 1, fields[54].Get<uint8>());
|
||||
ReplaceAllPlayerFlags((PlayerFlags)fields[16].Get<uint32>());
|
||||
|
||||
RemovePlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME);
|
||||
RemovePlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME);
|
||||
|
||||
if (GetSession()->IsAffectedByCAIS())
|
||||
{
|
||||
Seconds const accountPlayedTime = GetSession()->GetConsecutivePlayTime(GameTime::GetGameTime());
|
||||
|
||||
if (accountPlayedTime >= PLAY_TIME_LIMIT_FULL)
|
||||
SetPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME);
|
||||
else if (accountPlayedTime >= PLAY_TIME_LIMIT_PARTIAL)
|
||||
SetPlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME);
|
||||
}
|
||||
|
||||
SetInt32Value(PLAYER_FIELD_WATCHED_FACTION_INDEX, fields[53].Get<uint32>());
|
||||
|
||||
SetUInt64Value(PLAYER_FIELD_KNOWN_CURRENCIES, fields[52].Get<uint64>());
|
||||
|
||||
@@ -2115,9 +2115,7 @@ GroupJoinBattlegroundResult Group::CanJoinBattlegroundQueue(Battleground const*
|
||||
// check if someone in party is using dungeon system
|
||||
lfg::LfgState lfgState = sLFGMgr->GetState(member->GetGUID());
|
||||
if (lfgState > lfg::LFG_STATE_NONE && (lfgState != lfg::LFG_STATE_QUEUED || !sWorld->getBoolConfig(CONFIG_ALLOW_JOIN_BG_AND_LFG)))
|
||||
{
|
||||
return ERR_LFG_CANT_USE_BATTLEGROUND;
|
||||
}
|
||||
|
||||
// pussywizard: prevent joining when any member is in bg/arena
|
||||
if (member->InBattleground())
|
||||
@@ -2129,9 +2127,7 @@ GroupJoinBattlegroundResult Group::CanJoinBattlegroundQueue(Battleground const*
|
||||
|
||||
// don't let join if someone from the group is already in that bg queue
|
||||
if (member->InBattlegroundQueueForBattlegroundQueueType(bgQueueTypeId))
|
||||
{
|
||||
return ERR_BATTLEGROUND_JOIN_FAILED;
|
||||
}
|
||||
|
||||
// don't let join if someone from the group is in bg queue random
|
||||
if (member->InBattlegroundQueueForBattlegroundQueueType(bgQueueTypeIdRandom))
|
||||
@@ -2146,9 +2142,10 @@ GroupJoinBattlegroundResult Group::CanJoinBattlegroundQueue(Battleground const*
|
||||
return ERR_GROUP_JOIN_BATTLEGROUND_FAIL;
|
||||
|
||||
if (!member->GetBGAccessByLevel(bgTemplate->GetBgTypeID()))
|
||||
{
|
||||
return ERR_BATTLEGROUND_JOIN_TIMED_OUT;
|
||||
}
|
||||
|
||||
if (member->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME)) // Assumed to only apply to full restriction rather than partial
|
||||
return ERR_GROUP_JOIN_BATTLEGROUND_FAIL; // ERR_ARENA_EXPIRED_CAIS does not seem to be a result, so using this error instead
|
||||
}
|
||||
|
||||
// for arenas: check party size is proper
|
||||
|
||||
@@ -15,17 +15,28 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "GameTime.h"
|
||||
#include "Opcodes.h"
|
||||
#include "WorldPacket.h"
|
||||
#include "WorldSession.h"
|
||||
|
||||
void WorldSession::SendAuthResponse(uint8 code, bool shortForm, uint32 queuePos)
|
||||
{
|
||||
// BillingTimeRested: seconds of "healthy" play left before CAIS halves XP/loot from creatures
|
||||
// and quests (read by the client's GetBillingTimeRested()); 0 unless the CAIS flag is set.
|
||||
uint32 billingTimeRested = 0;
|
||||
if (IsAffectedByCAIS())
|
||||
{
|
||||
Seconds const played = GetConsecutivePlayTime(GameTime::GetGameTime());
|
||||
if (played < PLAY_TIME_LIMIT_PARTIAL)
|
||||
billingTimeRested = uint32((PLAY_TIME_LIMIT_PARTIAL - played).count());
|
||||
}
|
||||
|
||||
WorldPacket packet(SMSG_AUTH_RESPONSE, 1 + 4 + 1 + 4 + 1 + (shortForm ? 0 : (4 + 1)));
|
||||
packet << uint8(code);
|
||||
packet << uint32(0); // BillingTimeRemaining
|
||||
packet << GetBillingPlanFlags();
|
||||
packet << uint32(0); // BillingTimeRested
|
||||
packet << billingTimeRested;
|
||||
uint8 exp = Expansion(); // 0 - normal, 1 - TBC, 2 - WotLK, must be set in database manually for each account
|
||||
|
||||
if (exp >= MAX_EXPANSIONS)
|
||||
|
||||
@@ -192,6 +192,8 @@ void WorldSession::HandleBattlemasterJoinOpcode(WorldPacket& recvData)
|
||||
{
|
||||
err = ERR_BATTLEGROUND_NONE;
|
||||
}
|
||||
else if (_player->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME)) // Assumed to only apply to full restriction rather than partial
|
||||
err = ERR_GROUP_JOIN_BATTLEGROUND_FAIL; // ERR_ARENA_EXPIRED_CAIS does not seem to be a result, so using this error instead
|
||||
|
||||
if (err <= 0)
|
||||
{
|
||||
@@ -791,13 +793,11 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData)
|
||||
{
|
||||
lfg::LfgState lfgState = sLFGMgr->GetState(GetPlayer()->GetGUID());
|
||||
if (GetPlayer()->InBattleground()) // currently in battleground
|
||||
{
|
||||
err = ERR_BATTLEGROUND_NOT_IN_BATTLEGROUND;
|
||||
}
|
||||
else if (lfgState > lfg::LFG_STATE_NONE && (lfgState != lfg::LFG_STATE_QUEUED || !sWorld->getBoolConfig(CONFIG_ALLOW_JOIN_BG_AND_LFG))) // using lfg system
|
||||
{
|
||||
err = ERR_LFG_CANT_USE_BATTLEGROUND;
|
||||
}
|
||||
else if (GetPlayer()->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME)) // Assumed to only apply to full restriction rather than partial
|
||||
err = ERR_GROUP_JOIN_BATTLEGROUND_FAIL; // ERR_ARENA_EXPIRED_CAIS does not seem to be a result, so using this error instead
|
||||
|
||||
if (err <= 0)
|
||||
{
|
||||
@@ -870,9 +870,7 @@ void WorldSession::HandleBattlemasterJoinArena(WorldPacket& recvData)
|
||||
grp->DoForAllMembers([&bgQueue, &err](Player* member)
|
||||
{
|
||||
if (bgQueue.IsPlayerInvitedToRatedArena(member->GetGUID()))
|
||||
{
|
||||
err = ERR_BATTLEGROUND_JOIN_FAILED;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -559,6 +559,12 @@ void WorldSession::HandleLootRoll(WorldPacket& recvData)
|
||||
if (!group)
|
||||
return;
|
||||
|
||||
if (GetPlayer()->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
{
|
||||
SendPlayTimeWarning(PTF_UNHEALTHY_TIME, 0);
|
||||
rollType = ROLL_PASS;
|
||||
}
|
||||
|
||||
group->CountRollVote(GetPlayer()->GetGUID(), guid, rollType);
|
||||
|
||||
switch (rollType)
|
||||
|
||||
@@ -40,6 +40,14 @@ void WorldSession::HandleAutostoreLootItemOpcode(WorldPacket& recvData)
|
||||
|
||||
recvData >> lootSlot;
|
||||
|
||||
// full CAIS restriction blocks item pickup from every loot source (GO/gather/item/corpse),
|
||||
// not just the creature-corpse window guarded in HandleLootOpcode
|
||||
if (player->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
{
|
||||
player->SendLootError(lguid, LOOT_ERROR_PLAY_TIME_EXCEEDED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lguid.IsGameObject())
|
||||
{
|
||||
GameObject* go = player->GetMap()->GetGameObject(lguid);
|
||||
@@ -179,6 +187,15 @@ void WorldSession::HandleLootMoneyOpcode(WorldPacket& /*recvData*/)
|
||||
|
||||
if (loot)
|
||||
{
|
||||
// the money is zeroed and dropped from storage below no matter who is paid, so a fully
|
||||
// restricted looter would destroy it rather than receive it. Bail before that teardown;
|
||||
// reachable for windows not opened through HandleLootOpcode, e.g. chests and lockboxes.
|
||||
if (player->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
{
|
||||
player->SendLootError(guid, LOOT_ERROR_PLAY_TIME_EXCEEDED);
|
||||
return;
|
||||
}
|
||||
|
||||
sScriptMgr->OnPlayerBeforeLootMoney(player, loot);
|
||||
loot->NotifyMoneyRemoved();
|
||||
if (shareMoney && player->GetGroup()) //item, pickpocket and players can be looted only single player
|
||||
@@ -200,27 +217,59 @@ void WorldSession::HandleLootMoneyOpcode(WorldPacket& /*recvData*/)
|
||||
|
||||
for (std::vector<Player*>::const_iterator i = playersNear.begin(); i != playersNear.end(); ++i)
|
||||
{
|
||||
(*i)->ModifyMoney(goldPerPlayer);
|
||||
(*i)->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_MONEY, goldPerPlayer);
|
||||
uint32 finalGold = goldPerPlayer;
|
||||
|
||||
if ((*i)->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
continue;
|
||||
|
||||
if ((*i)->HasPlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME))
|
||||
{
|
||||
finalGold /= 2;
|
||||
|
||||
// a halved share that rounds down to nothing is not worth announcing
|
||||
if (!finalGold)
|
||||
continue;
|
||||
}
|
||||
|
||||
(*i)->ModifyMoney(finalGold);
|
||||
(*i)->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_MONEY, finalGold);
|
||||
|
||||
WorldPacket data(SMSG_LOOT_MONEY_NOTIFY, 4 + 1);
|
||||
data << uint32(goldPerPlayer);
|
||||
data << uint32(finalGold);
|
||||
data << uint8(playersNear.size() > 1 ? 0 : 1); // Controls the text displayed in chat. 0 is "Your share is..." and 1 is "You loot..."
|
||||
(*i)->SendDirectMessage(&data);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sScriptMgr->OnPlayerAfterCreatureLootMoney(player);
|
||||
player->ModifyMoney(loot->gold);
|
||||
player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_MONEY, loot->gold);
|
||||
uint32 finalGold = loot->gold;
|
||||
bool award = true; // full restriction already returned above
|
||||
|
||||
WorldPacket data(SMSG_LOOT_MONEY_NOTIFY, 4 + 1);
|
||||
data << uint32(loot->gold);
|
||||
data << uint8(1); // "You loot..."
|
||||
SendPacket(&data);
|
||||
if (player->HasPlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME))
|
||||
{
|
||||
finalGold /= 2;
|
||||
|
||||
// a halved amount that rounds down to nothing is not worth announcing
|
||||
award = finalGold != 0;
|
||||
}
|
||||
|
||||
// fire the hook regardless of the CAIS reduction, matching OnLootMoney below
|
||||
sScriptMgr->OnPlayerAfterCreatureLootMoney(player);
|
||||
|
||||
if (award)
|
||||
{
|
||||
player->ModifyMoney(finalGold);
|
||||
player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_MONEY, finalGold);
|
||||
|
||||
WorldPacket data(SMSG_LOOT_MONEY_NOTIFY, 4 + 1);
|
||||
data << uint32(finalGold);
|
||||
data << uint8(1); // "You loot..."
|
||||
SendPacket(&data);
|
||||
}
|
||||
}
|
||||
|
||||
// reports the amount that dropped, not the CAIS-reduced amount actually awarded;
|
||||
// a script that grants money from this hook bypasses the play time restriction
|
||||
sScriptMgr->OnLootMoney(player, loot->gold);
|
||||
|
||||
loot->gold = 0;
|
||||
@@ -242,15 +291,23 @@ void WorldSession::HandleLootOpcode(WorldPacket& recvData)
|
||||
ObjectGuid guid;
|
||||
recvData >> guid;
|
||||
|
||||
Player* player = GetPlayer();
|
||||
|
||||
// Check possible cheat
|
||||
if (!GetPlayer()->IsAlive() || !guid.IsCreatureOrVehicle())
|
||||
if (!player->IsAlive() || !guid.IsCreatureOrVehicle())
|
||||
return;
|
||||
|
||||
// interrupt cast
|
||||
if (GetPlayer()->IsNonMeleeSpellCast(false))
|
||||
GetPlayer()->InterruptNonMeleeSpells(false);
|
||||
if (player->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
{
|
||||
player->SendLootError(guid, LOOT_ERROR_PLAY_TIME_EXCEEDED);
|
||||
return;
|
||||
}
|
||||
|
||||
GetPlayer()->SendLoot(guid, LOOT_CORPSE);
|
||||
// interrupt cast
|
||||
if (player->IsNonMeleeSpellCast(false))
|
||||
player->InterruptNonMeleeSpells(false);
|
||||
|
||||
player->SendLoot(guid, LOOT_CORPSE);
|
||||
}
|
||||
|
||||
void WorldSession::HandleLootReleaseOpcode(WorldPacket& recvData)
|
||||
@@ -446,7 +503,7 @@ void WorldSession::HandleLootMasterGiveOpcode(WorldPacket& recvData)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_player->IsInRaidWith(target))
|
||||
if (!_player->IsInRaidWith(target) || target->HasPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME))
|
||||
{
|
||||
_player->SendLootError(lootguid, LOOT_ERROR_MASTER_OTHER);
|
||||
//LOG_DEBUG("network", "MasterLootItem: Player {} tried to give an item to ineligible player {} !", GetPlayer()->GetName(), target->GetName());
|
||||
|
||||
@@ -168,3 +168,11 @@ WorldPacket const* WorldPackets::Misc::ComplainResult::Write()
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* WorldPackets::Misc::PlayTimeWarning::Write()
|
||||
{
|
||||
_worldPacket << Flag;
|
||||
_worldPacket << PlayTimeRemaining;
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
@@ -236,6 +236,17 @@ namespace WorldPackets
|
||||
|
||||
uint8 Unk = 0;
|
||||
};
|
||||
|
||||
class PlayTimeWarning final : public ServerPacket
|
||||
{
|
||||
public:
|
||||
PlayTimeWarning() : ServerPacket(SMSG_PLAY_TIME_WARNING, 4 + 4) {}
|
||||
|
||||
WorldPacket const* Write() override;
|
||||
|
||||
uint32 Flag = 0; // PlayTimeFlag mask, set by WorldSession::SendPlayTimeWarning
|
||||
int32 PlayTimeRemaining = 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "Log.h"
|
||||
#include "MapMgr.h"
|
||||
#include "Metric.h"
|
||||
#include "MiscPackets.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Opcodes.h"
|
||||
@@ -121,6 +122,9 @@ WorldSession::WorldSession(uint32 id, std::string&& name, uint32 accountFlags, s
|
||||
_accountFlags(accountFlags),
|
||||
m_expansion(expansion),
|
||||
m_total_time(TotalTime),
|
||||
_lastUpdateTime(GameTime::GetGameTime()),
|
||||
_createTime(GameTime::GetGameTime()),
|
||||
_previousPlayTime(0),
|
||||
_logoutTime(0),
|
||||
m_inQueue(false),
|
||||
m_playerLoading(false),
|
||||
@@ -227,6 +231,14 @@ bool WorldSession::IsRecurringBillingAccount() const
|
||||
return HasAccountFlag(ACCOUNT_FLAG_RECURRING_BILLING);
|
||||
}
|
||||
|
||||
bool WorldSession::IsAffectedByCAIS() const
|
||||
{
|
||||
// China realm system for restricting consecutive play time (anti-addiction).
|
||||
// There is no known per-account flag for it, so a realm-wide config gate is used;
|
||||
// swap this out if a per-account/realm-region source is found later.
|
||||
return sWorld->getBoolConfig(CONFIG_CAIS_ENABLED);
|
||||
}
|
||||
|
||||
uint8 WorldSession::GetBillingPlanFlags() const
|
||||
{
|
||||
uint8 flags = SESSION_NONE;
|
||||
@@ -240,6 +252,9 @@ uint8 WorldSession::GetBillingPlanFlags() const
|
||||
if (IsInternetGameRoomAccount())
|
||||
flags |= SESSION_IGR;
|
||||
|
||||
if (IsAffectedByCAIS())
|
||||
flags |= SESSION_ENABLE_CAIS;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
@@ -380,6 +395,12 @@ bool WorldSession::Update(uint32 diff, PacketFilter& updater)
|
||||
uint32 processedPackets = 0;
|
||||
time_t currentTime = GameTime::GetGameTime().count();
|
||||
|
||||
if (GetPlayer() && GetPlayer()->IsInWorld() && IsAffectedByCAIS())
|
||||
{
|
||||
CheckPlayedTimeLimit(Seconds(currentTime));
|
||||
_lastUpdateTime = Seconds(currentTime);
|
||||
}
|
||||
|
||||
constexpr uint32 MAX_PROCESSED_PACKETS_IN_SAME_WORLDSESSION_UPDATE = 150;
|
||||
|
||||
while (m_Socket && _recvQueue.next(packet, updater))
|
||||
@@ -610,6 +631,47 @@ bool WorldSession::IsSocketClosed() const
|
||||
return !m_Socket || !m_Socket->IsOpen();
|
||||
}
|
||||
|
||||
void WorldSession::CheckPlayedTimeLimit(Seconds now)
|
||||
{
|
||||
Seconds const previousPlayed = GetConsecutivePlayTime(_lastUpdateTime);
|
||||
Seconds const currentPlayed = GetConsecutivePlayTime(now);
|
||||
|
||||
if ((previousPlayed < PLAY_TIME_LIMIT_FULL) &&
|
||||
(currentPlayed >= PLAY_TIME_LIMIT_FULL))
|
||||
{
|
||||
SendPlayTimeWarning(PTF_UNHEALTHY_TIME, 0);
|
||||
GetPlayer()->SetPlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME);
|
||||
GetPlayer()->RemovePlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME);
|
||||
}
|
||||
else if ((previousPlayed < PLAY_TIME_LIMIT_APPROACHING_FULL) &&
|
||||
(currentPlayed >= PLAY_TIME_LIMIT_APPROACHING_FULL))
|
||||
{
|
||||
SendPlayTimeWarning(PTF_APPROACHING_NO_PLAY_TIME, int32((PLAY_TIME_LIMIT_FULL - currentPlayed).count()));
|
||||
GetPlayer()->SetPlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME);
|
||||
GetPlayer()->RemovePlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME);
|
||||
}
|
||||
else if ((previousPlayed < PLAY_TIME_LIMIT_PARTIAL) &&
|
||||
(currentPlayed >= PLAY_TIME_LIMIT_PARTIAL))
|
||||
{
|
||||
SendPlayTimeWarning(PTF_APPROACHING_NO_PLAY_TIME, int32((PLAY_TIME_LIMIT_FULL - currentPlayed).count()));
|
||||
GetPlayer()->SetPlayerFlag(PLAYER_FLAGS_PARTIAL_PLAY_TIME);
|
||||
GetPlayer()->RemovePlayerFlag(PLAYER_FLAGS_NO_PLAY_TIME);
|
||||
}
|
||||
else if ((previousPlayed < PLAY_TIME_LIMIT_APPROACHING_PARTIAL) &&
|
||||
(currentPlayed >= PLAY_TIME_LIMIT_APPROACHING_PARTIAL))
|
||||
{
|
||||
SendPlayTimeWarning(PTF_APPROACHING_PARTIAL_PLAY_TIME, int32((PLAY_TIME_LIMIT_PARTIAL - currentPlayed).count()));
|
||||
}
|
||||
}
|
||||
|
||||
void WorldSession::SendPlayTimeWarning(PlayTimeFlag flag, int32 playTimeRemaining)
|
||||
{
|
||||
WorldPackets::Misc::PlayTimeWarning playTimeWarning;
|
||||
playTimeWarning.Flag = flag;
|
||||
playTimeWarning.PlayTimeRemaining = playTimeRemaining;
|
||||
SendPacket(playTimeWarning.Write());
|
||||
}
|
||||
|
||||
/// %Log the player out
|
||||
void WorldSession::LogoutPlayer(bool save)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "CircularBuffer.h"
|
||||
#include "Common.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Duration.h"
|
||||
#include "GossipDef.h"
|
||||
#include "Packet.h"
|
||||
#include "SharedDefines.h"
|
||||
@@ -289,6 +290,20 @@ enum CharterTypes
|
||||
ARENA_TEAM_CHARTER_5v5_TYPE = 5
|
||||
};
|
||||
|
||||
constexpr Seconds PLAY_TIME_LIMIT_APPROACHING_PARTIAL = Hours(2) + Minutes(30);
|
||||
constexpr Seconds PLAY_TIME_LIMIT_PARTIAL = Hours(3);
|
||||
constexpr Seconds PLAY_TIME_LIMIT_APPROACHING_FULL = Hours(4) + Minutes(30);
|
||||
constexpr Seconds PLAY_TIME_LIMIT_FULL = Hours(5);
|
||||
|
||||
enum PlayTimeFlag : uint32
|
||||
{
|
||||
PTF_APPROACHING_PARTIAL_PLAY_TIME = 0x1000,
|
||||
PTF_APPROACHING_NO_PLAY_TIME = 0x2000,
|
||||
PTF_UNK_1 = 0x20000000,
|
||||
PTF_UNK_2 = 0x40000000,
|
||||
PTF_UNHEALTHY_TIME = 0x80000000,
|
||||
};
|
||||
|
||||
//class to deal with packet processing
|
||||
//allows to determine if next packet is safe to be processed
|
||||
class PacketFilter
|
||||
@@ -405,6 +420,7 @@ public:
|
||||
bool IsTrialAccount() const;
|
||||
bool IsInternetGameRoomAccount() const;
|
||||
bool IsRecurringBillingAccount() const;
|
||||
bool IsAffectedByCAIS() const;
|
||||
|
||||
uint8 GetBillingPlanFlags() const;
|
||||
|
||||
@@ -483,6 +499,16 @@ public:
|
||||
/// Session in auth.queue currently
|
||||
void SetInQueue(bool state) { m_inQueue = state; }
|
||||
|
||||
// Playtime limit
|
||||
Seconds GetCreateTime() const { return _createTime; }
|
||||
// Measured from session creation (authentication), so login queue and character select count
|
||||
// toward the limits. Matches the VMaNGOS behaviour this is ported from.
|
||||
Seconds GetConsecutivePlayTime(Seconds now) const { return (now - _createTime) + _previousPlayTime; }
|
||||
Seconds GetPreviousPlayedTime() const { return _previousPlayTime; }
|
||||
void SetPreviousPlayedTime(Seconds playedTime) { _previousPlayTime = playedTime; }
|
||||
void CheckPlayedTimeLimit(Seconds now);
|
||||
void SendPlayTimeWarning(PlayTimeFlag flag, int32 playTimeRemaining);
|
||||
|
||||
/// Is the user engaged in a log out process?
|
||||
bool isLogingOut() const { return _logoutTime || m_playerLogout; }
|
||||
|
||||
@@ -1255,6 +1281,9 @@ private:
|
||||
// Warden
|
||||
std::unique_ptr<Warden> _warden; // Remains nullptr if Warden system is not enabled by config
|
||||
|
||||
Seconds _lastUpdateTime; // last time session was updated by world
|
||||
Seconds _createTime; // when session was created
|
||||
Seconds _previousPlayTime; // play time from previous session less than 5 hours ago
|
||||
time_t _logoutTime;
|
||||
bool m_inQueue; // session wait in auth.queue
|
||||
bool m_playerLoading; // code processed in LoginPlayer
|
||||
|
||||
@@ -38,6 +38,7 @@ WorldSessionMgr::WorldSessionMgr()
|
||||
_maxQueuedSessionCount = 0;
|
||||
_playerCount = 0;
|
||||
_maxPlayerCount = 0;
|
||||
_accountsPlayHistoryPruneTimer = 0;
|
||||
}
|
||||
|
||||
WorldSessionMgr::~WorldSessionMgr()
|
||||
@@ -91,6 +92,22 @@ WorldSession* WorldSessionMgr::FindOfflineSessionForCharacterGUID(ObjectGuid::Lo
|
||||
|
||||
void WorldSessionMgr::UpdateSessions(uint32 const diff)
|
||||
{
|
||||
// Drop play-history entries past the reset window so the map stays bounded even for
|
||||
// accounts that disconnect and never reconnect (login only erases their own entry).
|
||||
_accountsPlayHistoryPruneTimer += diff;
|
||||
if (_accountsPlayHistoryPruneTimer >= 10 * MINUTE * IN_MILLISECONDS)
|
||||
{
|
||||
_accountsPlayHistoryPruneTimer = 0;
|
||||
Seconds const now = GameTime::GetGameTime();
|
||||
for (auto itr = _accountsPlayHistory.begin(); itr != _accountsPlayHistory.end();)
|
||||
{
|
||||
if ((now - itr->second.logoutTime) >= PLAY_TIME_LIMIT_FULL)
|
||||
itr = _accountsPlayHistory.erase(itr);
|
||||
else
|
||||
++itr;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
METRIC_DETAILED_NO_THRESHOLD_TIMER("world_update_time",
|
||||
METRIC_TAG("type", "Add sessions"),
|
||||
@@ -117,8 +134,19 @@ void WorldSessionMgr::UpdateSessions(uint32 const diff)
|
||||
// pussywizard:
|
||||
if (pSession->HandleSocketClosed())
|
||||
{
|
||||
Seconds const now = GameTime::GetGameTime();
|
||||
|
||||
if (pSession->IsAffectedByCAIS())
|
||||
{
|
||||
// a reconnect builds a fresh session and carries time from _accountsPlayHistory, so persist it
|
||||
// here at socket-close time rather than during offline cleanup (which runs ~60s later)
|
||||
AccountPlayHistory& history = _accountsPlayHistory[pSession->GetAccountId()];
|
||||
history.playedTime = pSession->GetConsecutivePlayTime(now);
|
||||
history.logoutTime = now;
|
||||
}
|
||||
|
||||
if (!RemoveQueuedPlayer(pSession) && sWorld->getIntConfig(CONFIG_INTERVAL_DISCONNECT_TOLERANCE))
|
||||
_disconnects[pSession->GetAccountId()] = GameTime::GetGameTime().count();
|
||||
_disconnects[pSession->GetAccountId()] = now.count();
|
||||
_sessions.erase(itr);
|
||||
// there should be no offline session if current one is logged onto a character
|
||||
SessionMap::iterator iter;
|
||||
@@ -128,7 +156,7 @@ void WorldSessionMgr::UpdateSessions(uint32 const diff)
|
||||
_offlineSessions.erase(iter);
|
||||
delete tmp;
|
||||
}
|
||||
pSession->SetOfflineTime(GameTime::GetGameTime().count());
|
||||
pSession->SetOfflineTime(now.count());
|
||||
_offlineSessions[pSession->GetAccountId()] = pSession;
|
||||
continue;
|
||||
}
|
||||
@@ -138,8 +166,17 @@ void WorldSessionMgr::UpdateSessions(uint32 const diff)
|
||||
|
||||
if (!pSession->Update(diff, updater))
|
||||
{
|
||||
Seconds const now = GameTime::GetGameTime();
|
||||
|
||||
if (pSession->IsAffectedByCAIS())
|
||||
{
|
||||
AccountPlayHistory& history = _accountsPlayHistory[pSession->GetAccountId()];
|
||||
history.playedTime = pSession->GetConsecutivePlayTime(now);
|
||||
history.logoutTime = now;
|
||||
}
|
||||
|
||||
if (!RemoveQueuedPlayer(pSession) && sWorld->getIntConfig(CONFIG_INTERVAL_DISCONNECT_TOLERANCE))
|
||||
_disconnects[pSession->GetAccountId()] = GameTime::GetGameTime().count();
|
||||
_disconnects[pSession->GetAccountId()] = now.count();
|
||||
_sessions.erase(itr);
|
||||
delete pSession;
|
||||
}
|
||||
@@ -296,6 +333,9 @@ void WorldSessionMgr::AddSession_(WorldSession* session)
|
||||
if (!RemoveQueuedPlayer(oldSession) && sWorld->getIntConfig(CONFIG_INTERVAL_DISCONNECT_TOLERANCE))
|
||||
_disconnects[session->GetAccountId()] = GameTime::GetGameTime().count();
|
||||
|
||||
// don't allow resetting consecutive play time on double login to same account
|
||||
session->SetPreviousPlayedTime(old->second->GetConsecutivePlayTime(GameTime::GetGameTime()));
|
||||
|
||||
// pussywizard:
|
||||
if (oldSession->HandleSocketClosed())
|
||||
{
|
||||
@@ -315,6 +355,19 @@ void WorldSessionMgr::AddSession_(WorldSession* session)
|
||||
delete oldSession;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto itr = _accountsPlayHistory.find(session->GetAccountId());
|
||||
if (itr != _accountsPlayHistory.end())
|
||||
{
|
||||
// carry consecutive play time only if the offline gap was under the reset window
|
||||
if ((GameTime::GetGameTime() - itr->second.logoutTime) < PLAY_TIME_LIMIT_FULL)
|
||||
session->SetPreviousPlayedTime(itr->second.playedTime);
|
||||
|
||||
// entry consumed on login; erase so _accountsPlayHistory cannot grow unbounded
|
||||
_accountsPlayHistory.erase(itr);
|
||||
}
|
||||
}
|
||||
|
||||
_sessions[session->GetAccountId()] = session;
|
||||
|
||||
|
||||
@@ -19,16 +19,24 @@
|
||||
#define __WORLDSESSIONMGR_H
|
||||
|
||||
#include "Common.h"
|
||||
#include "Duration.h"
|
||||
#include "IWorld.h"
|
||||
#include "LockedQueue.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
class Player;
|
||||
class WorldPacket;
|
||||
class WorldSession;
|
||||
|
||||
struct AccountPlayHistory
|
||||
{
|
||||
Seconds logoutTime = Seconds::zero();
|
||||
Seconds playedTime = Seconds::zero(); // reset after 5 hours offline time
|
||||
};
|
||||
|
||||
class WorldSessionMgr
|
||||
{
|
||||
public:
|
||||
@@ -91,6 +99,7 @@ private:
|
||||
|
||||
SessionMap _sessions;
|
||||
SessionMap _offlineSessions;
|
||||
std::map<uint32 /*accountId*/, AccountPlayHistory> _accountsPlayHistory;
|
||||
|
||||
typedef std::unordered_map<uint32, time_t> DisconnectMap;
|
||||
DisconnectMap _disconnects;
|
||||
@@ -103,6 +112,7 @@ private:
|
||||
uint32 _maxQueuedSessionCount;
|
||||
uint32 _playerCount;
|
||||
uint32 _maxPlayerCount;
|
||||
uint32 _accountsPlayHistoryPruneTimer;
|
||||
};
|
||||
|
||||
#define sWorldSessionMgr WorldSessionMgr::Instance()
|
||||
|
||||
@@ -80,7 +80,7 @@ enum BillingPlanFlags
|
||||
SESSION_USAGE = 0x10, // Unk, NYI
|
||||
SESSION_TIME_MIXTURE = 0x20, // Unk, NYI
|
||||
SESSION_RESTRICTED = 0x40, // Unk, NYI
|
||||
SESSION_ENABLE_CAIS = 0x80, // Unk, NYI, possibly account play time limit related for China?
|
||||
SESSION_ENABLE_CAIS = 0x80, // Account play time limit related for China
|
||||
};
|
||||
|
||||
enum RealmZone
|
||||
|
||||
@@ -359,6 +359,8 @@ void WorldConfig::BuildConfigCache()
|
||||
SetConfigValue<uint32>(CONFIG_TRIAL_MONEY_CAP, "Trial.MoneyCap", 100000); // copper, 10 gold
|
||||
SetConfigValue<uint32>(CONFIG_TRIAL_TRADE_SKILL_CAP, "Trial.TradeSkillCap", 100);
|
||||
|
||||
SetConfigValue<bool>(CONFIG_CAIS_ENABLED, "CAIS.Enable", false);
|
||||
|
||||
SetConfigValue<uint32>(CONFIG_EVENT_ANNOUNCE, "Event.Announce", 0);
|
||||
|
||||
SetConfigValue<float>(CONFIG_CREATURE_LEASH_RADIUS, "CreatureLeashRadius", 30.0f);
|
||||
|
||||
@@ -518,6 +518,8 @@ enum ServerConfigs
|
||||
CONFIG_TRIAL_MONEY_CAP,
|
||||
CONFIG_TRIAL_TRADE_SKILL_CAP,
|
||||
|
||||
CONFIG_CAIS_ENABLED,
|
||||
|
||||
MAX_NUM_SERVER_CONFIGS
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user