diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index da7a62806..713dbf5fb 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -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 # diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp index b823c7bcb..0fa79ac64 100644 --- a/src/server/game/DungeonFinding/LFGMgr.cpp +++ b/src/server/game/DungeonFinding/LFGMgr.cpp @@ -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) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index daa4ba995..635a65f12 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -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); diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index c4da190fb..20bf49b19 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -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, // prefix for something? PLAYER_FLAGS_UNK16 = 0x00010000, // pre-3.0.3 PLAYER_FLAGS_SANCTUARY flag for player entered sanctuary diff --git a/src/server/game/Entities/Player/PlayerQuest.cpp b/src/server/game/Entities/Player/PlayerQuest.cpp index 58476621b..7361ac62d 100644 --- a/src/server/game/Entities/Player/PlayerQuest.cpp +++ b/src/server/game/Entities/Player/PlayerQuest.cpp @@ -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); diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index 2c4ddf10c..0bc0ad0fe 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -5098,6 +5098,20 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons SetByteValue(PLAYER_BYTES_3, 0, fields[5].Get()); SetByteValue(PLAYER_BYTES_3, 1, fields[54].Get()); ReplaceAllPlayerFlags((PlayerFlags)fields[16].Get()); + + 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()); SetUInt64Value(PLAYER_FIELD_KNOWN_CURRENCIES, fields[52].Get()); diff --git a/src/server/game/Groups/Group.cpp b/src/server/game/Groups/Group.cpp index 8d960a56f..a194f57de 100644 --- a/src/server/game/Groups/Group.cpp +++ b/src/server/game/Groups/Group.cpp @@ -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 diff --git a/src/server/game/Handlers/AuthHandler.cpp b/src/server/game/Handlers/AuthHandler.cpp index 99506a786..bf530173e 100644 --- a/src/server/game/Handlers/AuthHandler.cpp +++ b/src/server/game/Handlers/AuthHandler.cpp @@ -15,17 +15,28 @@ * with this program. If not, see . */ +#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) diff --git a/src/server/game/Handlers/BattleGroundHandler.cpp b/src/server/game/Handlers/BattleGroundHandler.cpp index 07e2863ad..a981349fe 100644 --- a/src/server/game/Handlers/BattleGroundHandler.cpp +++ b/src/server/game/Handlers/BattleGroundHandler.cpp @@ -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; - } }); } diff --git a/src/server/game/Handlers/GroupHandler.cpp b/src/server/game/Handlers/GroupHandler.cpp index ad09e714e..b2f2f9173 100644 --- a/src/server/game/Handlers/GroupHandler.cpp +++ b/src/server/game/Handlers/GroupHandler.cpp @@ -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) diff --git a/src/server/game/Handlers/LootHandler.cpp b/src/server/game/Handlers/LootHandler.cpp index 50636f27c..a18781f0e 100644 --- a/src/server/game/Handlers/LootHandler.cpp +++ b/src/server/game/Handlers/LootHandler.cpp @@ -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::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()); diff --git a/src/server/game/Server/Packets/MiscPackets.cpp b/src/server/game/Server/Packets/MiscPackets.cpp index e20d422b9..5c2d1740c 100644 --- a/src/server/game/Server/Packets/MiscPackets.cpp +++ b/src/server/game/Server/Packets/MiscPackets.cpp @@ -168,3 +168,11 @@ WorldPacket const* WorldPackets::Misc::ComplainResult::Write() return &_worldPacket; } + +WorldPacket const* WorldPackets::Misc::PlayTimeWarning::Write() +{ + _worldPacket << Flag; + _worldPacket << PlayTimeRemaining; + + return &_worldPacket; +} diff --git a/src/server/game/Server/Packets/MiscPackets.h b/src/server/game/Server/Packets/MiscPackets.h index df2f70973..e86d775ef 100644 --- a/src/server/game/Server/Packets/MiscPackets.h +++ b/src/server/game/Server/Packets/MiscPackets.h @@ -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; + }; } } diff --git a/src/server/game/Server/WorldSession.cpp b/src/server/game/Server/WorldSession.cpp index dae5140f8..b144d483f 100644 --- a/src/server/game/Server/WorldSession.cpp +++ b/src/server/game/Server/WorldSession.cpp @@ -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) { diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h index fa2bfd561..bea564076 100644 --- a/src/server/game/Server/WorldSession.h +++ b/src/server/game/Server/WorldSession.h @@ -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; // 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 diff --git a/src/server/game/Server/WorldSessionMgr.cpp b/src/server/game/Server/WorldSessionMgr.cpp index b22726d5a..50598d391 100644 --- a/src/server/game/Server/WorldSessionMgr.cpp +++ b/src/server/game/Server/WorldSessionMgr.cpp @@ -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; diff --git a/src/server/game/Server/WorldSessionMgr.h b/src/server/game/Server/WorldSessionMgr.h index af85ed7a0..933000379 100644 --- a/src/server/game/Server/WorldSessionMgr.h +++ b/src/server/game/Server/WorldSessionMgr.h @@ -19,16 +19,24 @@ #define __WORLDSESSIONMGR_H #include "Common.h" +#include "Duration.h" #include "IWorld.h" #include "LockedQueue.h" #include "ObjectGuid.h" #include +#include #include 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 _accountsPlayHistory; typedef std::unordered_map DisconnectMap; DisconnectMap _disconnects; @@ -103,6 +112,7 @@ private: uint32 _maxQueuedSessionCount; uint32 _playerCount; uint32 _maxPlayerCount; + uint32 _accountsPlayHistoryPruneTimer; }; #define sWorldSessionMgr WorldSessionMgr::Instance() diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index 0f2eb0e80..df877be35 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -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 diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 26ca792c0..bd6f0a300 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -359,6 +359,8 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_TRIAL_MONEY_CAP, "Trial.MoneyCap", 100000); // copper, 10 gold SetConfigValue(CONFIG_TRIAL_TRADE_SKILL_CAP, "Trial.TradeSkillCap", 100); + SetConfigValue(CONFIG_CAIS_ENABLED, "CAIS.Enable", false); + SetConfigValue(CONFIG_EVENT_ANNOUNCE, "Event.Announce", 0); SetConfigValue(CONFIG_CREATURE_LEASH_RADIUS, "CreatureLeashRadius", 30.0f); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index e869d8b3b..d4e3c79fa 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -518,6 +518,8 @@ enum ServerConfigs CONFIG_TRIAL_MONEY_CAP, CONFIG_TRIAL_TRADE_SKILL_CAP, + CONFIG_CAIS_ENABLED, + MAX_NUM_SERVER_CONFIGS };