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:
Benjamin Jackson
2026-07-21 06:40:37 -04:00
committed by GitHub
parent 2adaf94e13
commit a16ef90f28
20 changed files with 344 additions and 46 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>());

View File

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

View File

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

View File

@@ -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;
}
});
}

View File

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

View File

@@ -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());

View File

@@ -168,3 +168,11 @@ WorldPacket const* WorldPackets::Misc::ComplainResult::Write()
return &_worldPacket;
}
WorldPacket const* WorldPackets::Misc::PlayTimeWarning::Write()
{
_worldPacket << Flag;
_worldPacket << PlayTimeRemaining;
return &_worldPacket;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -518,6 +518,8 @@ enum ServerConfigs
CONFIG_TRIAL_MONEY_CAP,
CONFIG_TRIAL_TRADE_SKILL_CAP,
CONFIG_CAIS_ENABLED,
MAX_NUM_SERVER_CONFIGS
};