Files
UnrealEngine/Engine/Plugins/Online/OnlineFramework/Source/PlayTimeLimit/Private/PlayTimeLimitImpl.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

344 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PlayTimeLimitImpl.h"
#include "Features/IModularFeatures.h"
#include "PlayTimeLimitModule.h"
#include "PlayTimeLimitUserMock.h"
#include "Online/CoreOnline.h"
// FPlayTimeLimitImpl
FPlayTimeLimitImpl::FPlayTimeLimitImpl()
{
}
FPlayTimeLimitImpl::~FPlayTimeLimitImpl()
{
}
FPlayTimeLimitImpl& FPlayTimeLimitImpl::Get()
{
static FPlayTimeLimitImpl Singleton;
return Singleton;
}
void FPlayTimeLimitImpl::Initialize()
{
IModularFeatures::Get().RegisterModularFeature(GetModularFeatureName(), this);
// @todo make this data driven
ConfigRates.Emplace(0, 60, 1.0f); // Notify every hour, 100% rewards at 0 hours
ConfigRates.Emplace(3 * 60, 30, 0.5f); // Notify every 30 minutes, 50% rewards at 3 hours
ConfigRates.Emplace(5 * 60, 15, 0.0f); // Notify every 15 minutes, 0% rewards at 5 hours
// For simplicity of usage, sort by start time.
// @todo Uncomment when we make this data driven, with hard coded values we can ensure the order
ConfigRates.Sort([](const FOnlinePlayLimitConfigEntry& A, const FOnlinePlayLimitConfigEntry& B) { return A.TimeStartMinutes < B.TimeStartMinutes; });
if (ensure(!TickHandle.IsValid()))
{
// Register delegate for ticker callback
FTickerDelegate TickDelegate = FTickerDelegate::CreateRaw(this, &FPlayTimeLimitImpl::Tick);
TickHandle = FTSTicker::GetCoreTicker().AddTicker(TickDelegate, 0.0f);
}
}
void FPlayTimeLimitImpl::Shutdown()
{
IModularFeatures::Get().UnregisterModularFeature(GetModularFeatureName(), this);
if (TickHandle.IsValid())
{
FTSTicker::RemoveTicker(TickHandle);
TickHandle.Reset();
}
Users.Empty();
}
bool FPlayTimeLimitImpl::Tick(float DeltaTime)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FPlayTimeLimitImpl_Tick);
const bool bRetick = true;
if (Users.Num() == 0)
{
return bRetick;
}
// Perform logic periodically
static constexpr double TickFrequencySeconds = 1.0;
const double Now = FPlatformTime::Seconds();
if ((LastTickLogicTime == 0) || ((Now - LastTickLogicTime) > TickFrequencySeconds))
{
for (const FPlayTimeLimitUserPtr& User : Users)
{
User->Tick();
if (User->HasTimeLimit())
{
const float LastKnownRewardRate = User->GetLastKnownRewardRate();
const float RewardRate = User->GetRewardRate();
const TOptional<double> NextNotificationTime = User->GetNextNotificationTime();
const bool bRewardRateChanged = !FMath::IsNearlyEqual(LastKnownRewardRate, RewardRate);
const bool bShouldNotifyFromPeriodicReminder = NextNotificationTime.IsSet() && NextNotificationTime.GetValue() < Now;
const bool bShouldNotify = bRewardRateChanged || bShouldNotifyFromPeriodicReminder;
if (bRewardRateChanged)
{
User->SetLastKnownRewardRate(RewardRate);
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
// Do we want this to log in shipping builds?
UE_LOG(LogPlayTimeLimit, Log, TEXT("FPlayTimeLimitImpl: User [%s] RewardRate changed from %0.2f to %0.2f"), *User->GetUserId()->ToDebugString(), LastKnownRewardRate, RewardRate);
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
}
if (bShouldNotify)
{
const int32 PlayTimeMinutes = User->GetPlayTimeMinutes();
GetWarnUserPlayTimeDelegate().Broadcast(*User->GetUserId(), PlayTimeMinutes, RewardRate, *User->OverrideDialogTitle, *User->OverrideDialogText, *User->OverrideButtonText);
UpdateNextNotificationTime(*User, PlayTimeMinutes);
User->ClearDialogOverrideText();
}
}
}
LastTickLogicTime = Now;
}
return bRetick;
}
void FPlayTimeLimitImpl::RegisterUser(const FUniqueNetId& UserId)
{
if (!Users.ContainsByPredicate([&UserId](const FPlayTimeLimitUserPtr& User) { return *User->GetUserId() == UserId; }))
{
if (OnRequestCreateUser.IsBound())
{
FPlayTimeLimitUserRawPtr NewUser = OnRequestCreateUser.Execute(UserId);
if (NewUser)
{
FPlayTimeLimitUserPtr User = MakeShareable(NewUser);
Users.Emplace(User);
User->Init();
const int32 PlayTimeMinutes = User->GetPlayTimeMinutes();
UpdateNextNotificationTime(*User, PlayTimeMinutes);
}
else
{
UE_LOG(LogPlayTimeLimit, Warning, TEXT("FPlayTimeLimitImpl: OnRequestCreateUser Delegate returned a null User."));
}
}
else
{
UE_LOG(LogPlayTimeLimit, Warning, TEXT("FPlayTimeLimitImpl: No OnRequestCreateUser delegate bound."));
}
}
else
{
UE_LOG(LogPlayTimeLimit, Log, TEXT("FPlayTimeLimitImpl: User [%s] already registered"), *UserId.ToDebugString());
}
}
void FPlayTimeLimitImpl::UnregisterUser(const FUniqueNetId& UserId)
{
const int32 Index = Users.IndexOfByPredicate([&UserId](const FPlayTimeLimitUserPtr& User) { return *User->GetUserId() == UserId; });
if (Index != INDEX_NONE)
{
Users.RemoveAtSwap(Index);
}
else
{
UE_LOG(LogPlayTimeLimit, Log, TEXT("FPlayTimeLimitImpl: User [%s] not registered"), *UserId.ToDebugString());
}
}
void FPlayTimeLimitImpl::MockUser(const FUniqueNetId& UserId, const bool bHasTimeLimit, const double CurrentPlayTimeMinutes)
{
#if ALLOW_PLAY_LIMIT_MOCK
const int32 ExistingIndex = Users.IndexOfByPredicate([&UserId](const FPlayTimeLimitUserPtr& User) { return *User->GetUserId() == UserId; });
if (ExistingIndex != INDEX_NONE)
{
Users.RemoveAtSwap(ExistingIndex);
const FPlayTimeLimitUserPtr& User = Users[Users.Emplace(new FPlayTimeLimitUserMock(UserId.AsShared(), bHasTimeLimit, CurrentPlayTimeMinutes))];
// Hacky solution to try to line up the next notification time based on the new play time minutes
// Pretend the user logged in at 0 minutes, so that notifications happen at exactly 60 minutes, 120 minutes, etc
// The behavior of the real system is 60 minutes (etc) from login time, because WeGame does not tell us the exact number of minutes the player has played
const FOnlinePlayLimitConfigEntry* ConfigRate = nullptr;
if (User->HasTimeLimit())
{
ConfigRate = GetConfigEntry(static_cast<int32>(CurrentPlayTimeMinutes));
}
const float RewardRate = (ConfigRate != nullptr) ? ConfigRate->RewardRate : 1.0f;
User->SetLastKnownRewardRate(RewardRate);
int32 SecondsToNextNotification = 0;
if (ConfigRate && ConfigRate->NotificationRateMinutes != 0)
{
const double NumMinutesInBracketAlready = CurrentPlayTimeMinutes - ConfigRate->TimeStartMinutes;
const int32 NumNotificationsInBracketAlready = static_cast<int32>(NumMinutesInBracketAlready) / ConfigRate->NotificationRateMinutes;
const double NowSeconds = FPlatformTime::Seconds();
const double BracketStartTime = NowSeconds - (NumMinutesInBracketAlready * 60);
const double NextNotificationTime = BracketStartTime + ((NumNotificationsInBracketAlready + 1) * ConfigRate->NotificationRateMinutes * 60);
User->SetNextNotificationTime(NextNotificationTime);
SecondsToNextNotification = static_cast<int32>(NextNotificationTime - NowSeconds);
}
else
{
User->SetNextNotificationTime(TOptional<double>());
}
UE_LOG(LogPlayTimeLimit, Log, TEXT("MockUser: UserId=%s, bHasTimeLimit=%s, CurrentPlayTimeMinutes=%d, SecondsToNextNotification=%d"), *UserId.ToDebugString(), bHasTimeLimit ? TEXT("true") : TEXT("false"), static_cast<int32>(CurrentPlayTimeMinutes), SecondsToNextNotification);
}
#endif
}
void FPlayTimeLimitImpl::NotifyNow()
{
// Well... on next Tick
LastTickLogicTime = 0.0;
double Now = FPlatformTime::Seconds();
for (const FPlayTimeLimitUserPtr& User : Users)
{
User->SetNextNotificationTime(Now);
}
}
void FPlayTimeLimitImpl::DumpState()
{
UE_LOG(LogPlayTimeLimit, Display, TEXT("FPlayTimeLimitImpl::DumpState: Begin"));
if (Users.Num() != 0)
{
const double Now = FPlatformTime::Seconds();
for (const FPlayTimeLimitUserPtr& User : Users)
{
FString NextNotificationTimeString;
const TOptional<double> NextNotificationTime = User->GetNextNotificationTime();
if (NextNotificationTime.IsSet())
{
double SecondsToNextNotification = NextNotificationTime.GetValue() - Now;
NextNotificationTimeString = FPlatformTime::PrettyTime(SecondsToNextNotification);
}
else
{
NextNotificationTimeString = TEXT("n/a");
}
UE_LOG(LogPlayTimeLimit, Display, TEXT(" User [%s]"), *User->GetUserId()->ToDebugString());
UE_LOG(LogPlayTimeLimit, Display, TEXT(" HasTimeLimit: [%s]"), User->HasTimeLimit() ? TEXT("true") : TEXT("false"));
UE_LOG(LogPlayTimeLimit, Display, TEXT(" NextNotificationTime: [%s]"), *NextNotificationTimeString);
UE_LOG(LogPlayTimeLimit, Display, TEXT(" LastKnownRewardRate: %0.2f"), User->GetLastKnownRewardRate());
UE_LOG(LogPlayTimeLimit, Display, TEXT(" RewardRate: %0.2f"), User->GetRewardRate());
UE_LOG(LogPlayTimeLimit, Display, TEXT(" PlayTimeMinutes: %d"), User->GetPlayTimeMinutes());
}
}
else
{
UE_LOG(LogPlayTimeLimit, Display, TEXT("No users"));
}
UE_LOG(LogPlayTimeLimit, Display, TEXT("FPlayTimeLimitImpl::DumpState: End"));
}
bool FPlayTimeLimitImpl::HasTimeLimit(const FUniqueNetId& UserId)
{
bool bHasTimeLimit = false;
const FPlayTimeLimitUserPtr* const User = Users.FindByPredicate([&UserId](const FPlayTimeLimitUserPtr& ExistingUser) { return *ExistingUser->GetUserId() == UserId; });
if (User != nullptr)
{
bHasTimeLimit = (*User)->HasTimeLimit();
}
else
{
UE_LOG(LogPlayTimeLimit, Warning, TEXT("HasTimeLimit: UserId [%s] is not registered"), *UserId.ToDebugString());
}
return bHasTimeLimit;
}
int32 FPlayTimeLimitImpl::GetPlayTimeMinutes(const FUniqueNetId& UserId)
{
int32 PlayTimeMinutes = 0;
const FPlayTimeLimitUserPtr* const User = Users.FindByPredicate([&UserId](const FPlayTimeLimitUserPtr& ExistingUser) { return *ExistingUser->GetUserId() == UserId; });
if (User != nullptr)
{
PlayTimeMinutes = (*User)->GetPlayTimeMinutes();
}
else
{
UE_LOG(LogPlayTimeLimit, Warning, TEXT("GetPlayTimeMinutes: UserId [%s] is not registered"), *UserId.ToDebugString());
}
return PlayTimeMinutes;
}
float FPlayTimeLimitImpl::GetRewardRate(const FUniqueNetId& UserId)
{
float RewardRate = 1.0f;
const FPlayTimeLimitUserPtr* const User = Users.FindByPredicate([&UserId](const FPlayTimeLimitUserPtr& ExistingUser) { return *ExistingUser->GetUserId() == UserId; });
if (User != nullptr)
{
RewardRate = (*User)->GetLastKnownRewardRate();
}
else
{
UE_LOG(LogPlayTimeLimit, Warning, TEXT("GetRewardRate: UserId [%s] is not registered"), *UserId.ToDebugString());
}
if (RewardRate > 1.0f || RewardRate < 0.0f)
{
// Warn once if we find something suspicious
static bool bWarned = false;
if (!bWarned)
{
const int32 PlayTimeMinutes = GetPlayTimeMinutes(UserId);
UE_LOG(LogPlayTimeLimit, Warning, TEXT("GetRewardRate: Received RewardRate=%0.2f (Expected range: [0.0, 1.0]). PlayTimeMinutes=%d. Clamping to the expected range."), RewardRate, PlayTimeMinutes);
bWarned = true;
}
RewardRate = FMath::Clamp(RewardRate, 0.0f, 1.0f);
}
return RewardRate;
}
FWarnUserPlayTime& FPlayTimeLimitImpl::GetWarnUserPlayTimeDelegate()
{
return WarnUserPlayTimeDelegate;
}
void FPlayTimeLimitImpl::GameExitByRequest()
{
OnGameExitRequestedDelegate.Broadcast();
}
const FOnlinePlayLimitConfigEntry* FPlayTimeLimitImpl::GetConfigEntry(const int32 PlayTimeMinutes) const
{
const FOnlinePlayLimitConfigEntry* Result = nullptr;
// Find the first entry that has a time start greater than the number of minutes played
// Note that the list is already sorted by TimeStartMinutes
for (const FOnlinePlayLimitConfigEntry& ConfigRate : ConfigRates)
{
if (PlayTimeMinutes >= ConfigRate.TimeStartMinutes)
{
// If we played this long, we are least this limited. No break, next limit might also apply to us
Result = &ConfigRate;
}
else
{
break;
}
}
return Result;
}
void FPlayTimeLimitImpl::UpdateNextNotificationTime(FPlayTimeLimitUser& User, const int32 PlayTimeMinutes) const
{
const FOnlinePlayLimitConfigEntry* ConfigRate = nullptr;
if (User.HasTimeLimit())
{
ConfigRate = GetConfigEntry(PlayTimeMinutes);
}
if (ConfigRate && ConfigRate->NotificationRateMinutes != 0)
{
User.SetNextNotificationTime(FPlatformTime::Seconds() + (ConfigRate->NotificationRateMinutes * 60));
}
else
{
User.SetNextNotificationTime(TOptional<double>());
}
}