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:
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user