using System;
using System.Collections;
using System.Threading.Tasks;
using TapSDK.Compliance.Internal;
using TapSDK.Core;
using TapSDK.Core.Internal.Log;
using TapSDK.Login;
using UnityEngine;
using Network = TapSDK.Compliance.Internal.Network;
namespace TapSDK.Compliance
{
///
/// 防沉迷轮询器
///
internal class CompliancePoll : MonoBehaviour
{
static readonly string ANTI_ADDICTION_POLL_NAME = "CompliancePoll";
static CompliancePoll current;
///
/// 轮询间隔,单位:秒
///
private static int pollInterval = 2 * 60;
private static Coroutine _pollCoroutine;
private static float? _elpased;
public static bool StartPoll;
///
/// 轮询生命周期的代次。StartUp / Logout 都是跨线程的独立公开入口:
/// StartUp 常挂在 HTTP 响应续体上(OnCheckedPlayableWithAdult → TryStartPoll,
/// 成年账号也走),跑在线程池、经闸门入队;而 Logout 可以从主线程直接进来
/// (OnApplicationPause → LeaveGame,以及游戏调 exit),在主线程会同步执行。
///
/// 于是存在这样一条真实序列:后台 StartUp 入队 → 主线程 Logout 立刻把
/// _pollCoroutine 置空并返回 → 下一帧队列里那个过期的启动动作照旧创建协程、
/// 把 StartPoll 重新置 true,结果用户已退出、合规心跳还在发。
///
/// 每次 StartUp / Logout 都递增代次,入队的启动动作执行前先比对自己捕获的代次,
/// 不一致就说明期间已有更新的意图,直接作废。
///
/// 代次和 StartPoll / _elpased 必须在同一个临界区里改:只用 Interlocked 递增代次的
/// 话,"递增" 和 "写 StartPoll" 之间仍有窗口——StartUp 递增到 1 后被 Logout 抢占
/// (代次到 2、StartPoll 置 false、协程同步停掉),StartUp 再把 StartPoll 写回 true,
/// 而它的启动动作稍后因代次失效被跳过,于是留下"没有协程在跑、StartPoll 却是 true"
/// 的脏状态:LeaveGame 会据此设 needResumePoll,EnterGame 再把轮询错误地拉起来。
///
private static int pollGeneration;
///
/// 保护 pollGeneration / StartPoll / _elpased / _pollCoroutine 的一致性。
/// 临界区里只有几个赋值和协程启停,不做 IO;Monitor 可重入,协程首段若回调进来也
/// 不会自锁。
///
private static readonly object pollStateLock = new object();
internal static void StartUp(int inverval = 0)
{
TapLog.Log("StartUp " );
if(inverval > 0){
pollInterval = inverval;
}
// new GameObject / DontDestroyOnLoad / AddComponent / StartCoroutine 全是只能在
// 主线程调的 Unity API,而这里的调用方 TryStartPoll 挂在 HTTP 响应的续体上——
// 成年账号(OnCheckedPlayableWithAdult → TryStartPoll)也走这条路,不只是弹面板
// 的未成年分支。之前没炸只是因为 current 早就在主线程建好了、走不到这个 if。
int generation;
// 代次递增和 StartPoll 置位必须原子:中间若被 Logout 抢占,这里的 StartPoll = true
// 会覆盖它的 false,而启动动作又会因代次失效被跳过,留下"无协程但 StartPoll=true"
// 的脏状态(详见 pollGeneration 的说明)。
//
// StartPoll 也不能等派发执行才置位:LeaveGame 用它判断"是否需要停"
// (if (CanPlay && CompliancePoll.StartPoll)),若等到下一帧,切后台时 LeaveGame
// 会看到 false 而跳过停止、也不设 needResumePoll,随后队列里的启动动作照旧把轮询
// 拉起来 —— 后台里继续发心跳,且回到前台后再也不会被恢复。
lock (pollStateLock)
{
generation = ++pollGeneration;
StartPoll = true;
}
ComplianceMainThread.Run(() =>
{
lock (pollStateLock)
{
// 期间发生过 Logout(或更晚的一次 StartUp),本次启动意图已过期
if (generation != pollGeneration)
{
return;
}
if (current == null)
{
GameObject pollGo = new GameObject(ANTI_ADDICTION_POLL_NAME);
DontDestroyOnLoad(pollGo);
current = pollGo.AddComponent();
_elpased = null;
}
if (_pollCoroutine == null)
{
_pollCoroutine = current.StartCoroutine(current.Poll());
StartPoll = true;
}
}
});
}
internal static void StartCountdownRemainTime()
{
TapLog.Log("StartCountdownRemainTime " );
// 同 StartUp:建 GameObject 只能在主线程。日志里这个方法确实出现在 IO 线程上
// ("(IO 31) StartCountdownRemainTime"),只是当时 current 已存在、提前 return
// 才没事——首次从后台线程进来就会踩中。_elpased 由 Update 读,一起放进闸门。
//
// 这里只校验代次、不递增:它与 StartUp 不互斥(一个是本地倒计时、一个是
// 服务端轮询),递增会误伤在途的 StartUp。校验的作用是防止 Logout 已经把
// _elpased 清空后,队列里这个过期动作又把倒计时重新点起来。
int generation;
lock (pollStateLock)
{
generation = pollGeneration;
}
ComplianceMainThread.Run(() =>
{
lock (pollStateLock)
{
if (generation != pollGeneration)
{
return;
}
if (current == null)
{
GameObject pollGo = new GameObject(ANTI_ADDICTION_POLL_NAME);
DontDestroyOnLoad(pollGo);
current = pollGo.AddComponent();
_elpased = null;
}
else
{
return;
}
_elpased = 0;
}
});
}
internal static void Logout()
{
// 先递增代次,作废所有仍在主线程队列里等待执行的 StartUp / 倒计时启动动作,
// 否则退出后它们会把轮询重新拉起来(详见 pollGeneration 的说明)。
int generation;
// 同 StartUp:代次递增与状态写入原子完成,避免与并发的 StartUp 互相覆盖。
// StartPoll / _elpased 留在当前线程立即生效——调用方(exit / 登出)依赖
// "调用返回后轮询判定立刻失效",延后一帧会让 Update 多跑一次心跳。
lock (pollStateLock)
{
generation = ++pollGeneration;
StartPoll = false;
_elpased = null;
}
// StopAllCoroutines 是 Unity API,只能主线程;_pollCoroutine 置空必须跟它同批,
// 否则 StartUp 可能在协程真正停掉之前就重启一个。
//
// 停止动作同样要校验代次,方向与 StartUp 相反但成因一样:后台线程 Logout 把
// 停止动作入队后,主线程可能紧接着 StartUp(同步)启动了新一轮轮询,此时这个
// 过期的停止动作若照旧执行,就会掐掉刚建立的新会话并把 _pollCoroutine 置空
// ——StartPoll 还是 true,状态显示在轮询、实际心跳已停,比直接报错更难查。
ComplianceMainThread.Run(() =>
{
lock (pollStateLock)
{
if (generation != pollGeneration)
{
return;
}
current?.StopAllCoroutines();
_pollCoroutine = null;
}
});
}
private void Update()
{
if (_elpased != null)
{
_elpased += Time.unscaledDeltaTime;
if (_elpased >= 1)
{
_elpased = 0;
if (TapTapComplianceManager.CurrentRemainSeconds != null)
TapTapComplianceManager.CurrentRemainSeconds--;
}
}
}
IEnumerator Poll()
{
// 记下自己属于哪一代。协程退出时只有在代次未变(自己仍是当前会话)的情况下才复位
// 共享状态,否则会误清掉一个更新的 StartUp 刚建立的轮询。
int generation;
lock (pollStateLock)
{
generation = pollGeneration;
}
// 用 try/finally 兜住所有退出路径(正常 break、Result 抛异常、被 StopAllCoroutines
// 中断)。原来没有这层兜底:checkPlayableTask 一旦 faulted 或 canceled,下面读
// .Result 会抛 AggregateException 把协程掀掉,而 _pollCoroutine 仍非 null、
// StartPoll 仍是 true —— 之后 StartUp() 因为 _pollCoroutine != null 不再启动,
// 轮询就永久停了,状态却一直显示在跑。
try
{
while (true)
{
// 上报/检查可玩
Task checkPlayableTask = TapTapComplianceManager.CheckPlayableOnPolling();
yield return new WaitUntil(() => checkPlayableTask.IsCompleted);
// 先判失败再读 Result,别让异常来终止协程
if (checkPlayableTask.IsFaulted || checkPlayableTask.IsCanceled)
{
TapLog.Error("[Compliance] 轮询检查可玩性失败,停止轮询: "
+ (checkPlayableTask.Exception?.GetBaseException().Message ?? "canceled"));
break;
}
PlayableResult playable = checkPlayableTask.Result;
if (playable.RemainTime <= 0)
{
break;
}
if(playable.RemainTime > 0 && playable.RemainTime < pollInterval){
pollInterval = playable.RemainTime;
}
if (_elpased == null)
_elpased = 0;
yield return new WaitForSecondsRealtime(pollInterval);
}
}
finally
{
lock (pollStateLock)
{
if (generation == pollGeneration)
{
_elpased = null;
_pollCoroutine = null;
StartPoll = false;
}
}
}
}
///
/// 切换后台
///
///
void OnApplicationPause(bool pauseStatus)
{
TapLog.Log("Anti OnApplicationPause " + pauseStatus);
if(pauseStatus){
TapTapComplianceManager.LeaveGame();
}else{
TapTapComplianceManager.EnterGame();
}
}
private static void SendPlayableRequest()
{
#pragma warning disable CS4014
Network.CheckPlayable();
#pragma warning restore CS4014
}
}
}