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
+12 -1
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)
@@ -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)
+73 -16
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());