using System; using System.Collections.Generic; using System.Text; using Runtime.ADAggregator; using UnityEngine; // 智能预加载实验:基于“场景出现次数/该场景实际播放请求次数”进行置信决策, // 并允许通过策略配置进行服务端默认值覆盖。 public static class TapadnSmartLoadOrchestrator { private const string StatsPrefsKey = "TapadnSmartLoadStats.v1"; private const string DefaultScene = "__default__"; private const string PolicyDefaultScene = "__default__"; private static TapadnSmartLoadConfig _runtimeConfig; private static Dictionary _states = new Dictionary(StringComparer.Ordinal); private static Dictionary _policies = new Dictionary(StringComparer.Ordinal); private static Dictionary _cacheStates = new Dictionary(StringComparer.Ordinal); private static bool _initialized; private static bool _enabled; public static bool IsEnabled => _enabled; public static string GetDebugStateDump() { if (!_initialized) { return "{}"; } var lines = new List(); foreach (var entry in _states.Values) { if (entry == null) { continue; } var adType = SafeToAdType(entry.AdType); if (!adType.HasValue) { continue; } var policy = ResolvePolicy(adType.Value, entry.Scenario); var prob = EstimateShowProbability(adType.Value, entry.Scenario); lines.Add( $"[{entry.AdType}]{entry.Scenario} e={entry.EnterCount} pReq={entry.PlayRequestCount} preReq={entry.PreloadRequestCount} preSuc={entry.PreloadSuccessCount} preFail={entry.PreloadFailureCount} showReq={entry.ShowRequestCount} showOk={entry.ShowStartCount} showFail={entry.ShowFailureCount} imm={entry.ImmediateHitCount} smartHit={entry.SmartCacheHitCount} smartUsed={entry.SmartPreloadConsumedCount} score={prob:F3} policy={policy?.PreloadThreshold:F2}"); } return $"enabled={_enabled},states={lines.Count},policies={_policies.Count},cache={GetCacheDebugDump()},snapshot={string.Join(" ; ", lines)}"; } public static string ExportSnapshotCsv() { if (!_initialized) { return GetSnapshotCsvHeader(); } var sb = new StringBuilder(); sb.AppendLine(GetSnapshotCsvHeader()); foreach (var entry in _states.Values) { if (entry == null) { continue; } sb.AppendLine(string.Join(",", entry.AdType, EncodeCsv(NormalizeScenario(entry.Scenario)), entry.EnterCount, entry.PlayRequestCount, entry.PreloadRequestCount, entry.PreloadSuccessCount, entry.PreloadFailureCount, entry.ShowRequestCount, entry.ShowStartCount, entry.ShowFailureCount, entry.ImmediateHitCount, entry.SmartCacheHitCount, entry.SmartCacheCrossSceneHitCount, entry.UnattributedCacheHitCount, entry.SmartPreloadConsumedCount, entry.SmartPreloadConsumedSameSceneCount, entry.SmartPreloadConsumedOtherSceneCount, entry.SmartPreloadShowFailureCount, entry.SmartPreloadExpiredCount, EncodeCsv(NormalizeScenario(entry.LastSmartPreloadConsumedByScene)), entry.LastSmartPreloadConsumedUnix, entry.LastPreloadUnix, entry.LastUpdatedUnix, EstimateShowProbability((AD_Type)entry.AdType, entry.Scenario))); } return sb.ToString(); } public static void ResetLearningState() { if (!_initialized) { return; } _states.Clear(); _cacheStates.Clear(); PlayerPrefs.DeleteKey(StatsPrefsKey); PlayerPrefs.Save(); } public static void Initialize(TapadnControllerOptions options) { _runtimeConfig = BuildConfig(options); _enabled = options?.SmartPreloadEnabled ?? false; _initialized = true; _states = new Dictionary(StringComparer.Ordinal); _cacheStates = new Dictionary(StringComparer.Ordinal); LoadStates(); EnsureDefaultPolicies(_runtimeConfig); } public static void OnEnterAdScenario(AD_Type adType, string scenario, string _slotId) { if (!_initialized || !_enabled) { return; } var normalizedScenario = NormalizeScenario(scenario); var record = GetOrCreateState(adType, normalizedScenario); record.EnterCount = Math.Max(0, record.EnterCount) + 1; record.LastUpdatedUnix = GetNowUnixSeconds(); record.ShowRequestCount = Math.Max(0, record.ShowRequestCount); SaveStates(); TryPreload(adType, normalizedScenario, _slotId); } public static void OnPlayRequestStarted(AD_Type adType, string scenario, bool cacheReadyAtRequest, string slotId = null) { if (!_initialized || !_enabled) { return; } var normalizedScenario = NormalizeScenario(scenario); var record = GetOrCreateState(adType, normalizedScenario); record.PlayRequestCount = Math.Max(0, record.PlayRequestCount) + 1; record.ShowRequestCount = Math.Max(0, record.ShowRequestCount) + 1; record.LastUpdatedUnix = GetNowUnixSeconds(); if (cacheReadyAtRequest) { MarkImmediateHit(adType, normalizedScenario, slotId); } else { MarkCacheExpiredIfStale(adType, slotId); } SaveStates(); } public static void OnLoadRequested(AD_Type adType, string scenario, string slotId = null) { if (!_initialized || !_enabled) { return; } PrepareSmartLoadRequest(adType, scenario, slotId); } public static void OnLoadStarted(AD_Type adType, string scenario, string slotId = null) { if (!_initialized || !_enabled) { return; } BeginLoadRequest(adType, scenario, slotId); SaveStates(); } public static void OnLoadResult(AD_Type adType, string scenario, bool success, string slotId = null) { if (!_initialized || !_enabled) { return; } CompleteLoadRequest(adType, scenario, success, slotId); SaveStates(); } public static void OnShowStart(AD_Type adType, string scenario, string slotId = null) { if (!_initialized || !_enabled) { return; } var record = GetOrCreateState(adType, NormalizeScenario(scenario)); record.ShowStartCount = Math.Max(0, record.ShowStartCount) + 1; record.LastUpdatedUnix = GetNowUnixSeconds(); MarkCacheConsumed(adType, scenario, slotId); SaveStates(); } public static void OnShowError(AD_Type adType, string scenario, string slotId = null) { if (!_initialized || !_enabled) { return; } var record = GetOrCreateState(adType, NormalizeScenario(scenario)); record.ShowFailureCount = Math.Max(0, record.ShowFailureCount) + 1; record.LastUpdatedUnix = GetNowUnixSeconds(); MarkCacheShowFailed(adType, scenario, slotId); SaveStates(); } private static void TryPreload(AD_Type adType, string scenario, string slotId) { if (!_initialized || !_enabled) { return; } if (!ADManager.Instance.CheckNetwork()) { return; } var policy = ResolvePolicy(adType, scenario); if (policy == null) { return; } if (!NeedPreload(adType, scenario, policy)) { return; } var now = GetNowUnixSeconds(); var state = GetOrCreateState(adType, scenario); if (now - state.LastPreloadUnix < policy.CooldownSeconds) { return; } if (ADManager.Instance.IsRealy(adType)) { return; } MarkCacheExpiredIfStale(adType, slotId); OnLoadRequested(adType, scenario, slotId); ADManager.Instance.LoadAD(adType); var smartLoadStarted = HasPendingSmartLoadForScene(adType, scenario, slotId); ClearPreparedLoadIfNotStarted(adType, slotId); if (!smartLoadStarted) { return; } state.LastPreloadUnix = now; state.LastUpdatedUnix = now; SaveStates(); Debug.Log($"[TapADN SmartLoad] Preload triggered: type={adType}, scenario={scenario}, score={EstimateShowProbability(adType, scenario):F3}"); } private static bool NeedPreload(AD_Type adType, string scenario, TapadnSmartLoadPolicyItem policy) { var score = EstimateShowProbability(adType, scenario); return score >= policy.PreloadThreshold; } private static float EstimateShowProbability(AD_Type adType, string scenario) { var policy = ResolvePolicy(adType, scenario); if (policy == null) { return 0f; } var state = GetOrCreateState(adType, scenario); if (state.EnterCount <= 0) { return policy.BaseProbability; } var observedRate = Mathf.Clamp01(state.PlayRequestCount / (float)state.EnterCount); var confidence = Mathf.Clamp01(state.EnterCount / (float)Mathf.Max(1, policy.MinSamplesForConfidence)); var ageHours = Mathf.Max(0f, (GetNowUnixSeconds() - state.LastUpdatedUnix) / 3600f); var decay = Mathf.Pow(0.5f, ageHours / Mathf.Max(1f, policy.DecayHalfLifeHours)); var trust = Mathf.Clamp01(confidence * decay); return Mathf.Lerp(policy.BaseProbability, observedRate, trust); } private static void PrepareSmartLoadRequest(AD_Type adType, string scenario, string slotId) { var state = GetOrCreateCacheState(adType, slotId); if (state.PendingSmartLoad) { return; } state.PreparedSmartLoad = true; state.PreparedOriginScene = NormalizeScenario(scenario); state.PreparedOriginScore = EstimateShowProbability(adType, scenario); state.PreparedRequestUnix = GetNowUnixSeconds(); } private static void BeginLoadRequest(AD_Type adType, string scenario, string slotId) { var state = GetOrCreateCacheState(adType, slotId); var now = GetNowUnixSeconds(); if (state.PreparedSmartLoad) { var originScene = NormalizeScenario(state.PreparedOriginScene); var record = GetOrCreateState(adType, originScene); record.PreloadRequestCount = Math.Max(0, record.PreloadRequestCount) + 1; record.LastUpdatedUnix = now; state.PendingSmartLoad = true; state.PendingOriginScene = originScene; state.PendingOriginScore = state.PreparedOriginScore; state.PendingRequestUnix = now; } else { state.PendingSmartLoad = false; state.PendingOriginScene = NormalizeScenario(scenario); state.PendingOriginScore = EstimateShowProbability(adType, scenario); state.PendingRequestUnix = now; } state.HasReadyCache = false; state.PreparedSmartLoad = false; state.PreparedOriginScene = null; state.PreparedOriginScore = 0f; state.PreparedRequestUnix = 0; } private static void ClearPreparedLoadIfNotStarted(AD_Type adType, string slotId) { var state = GetOrCreateCacheState(adType, slotId); if (state.PendingSmartLoad) { return; } state.PreparedSmartLoad = false; state.PreparedOriginScene = null; state.PreparedOriginScore = 0f; state.PreparedRequestUnix = 0; } private static bool HasPendingSmartLoadForScene(AD_Type adType, string scenario, string slotId) { var state = GetOrCreateCacheState(adType, slotId); return state.PendingSmartLoad && string.Equals(NormalizeScenario(state.PendingOriginScene), NormalizeScenario(scenario), StringComparison.Ordinal); } private static void CompleteLoadRequest(AD_Type adType, string scenario, bool success, string slotId) { var cacheState = GetOrCreateCacheState(adType, slotId); var normalizedScenario = NormalizeScenario(scenario); var originScene = cacheState.PendingSmartLoad ? NormalizeScenario(cacheState.PendingOriginScene) : normalizedScenario; var originRecord = GetOrCreateState(adType, originScene); if (cacheState.PendingSmartLoad) { if (success) { originRecord.PreloadSuccessCount = Math.Max(0, originRecord.PreloadSuccessCount) + 1; } else { originRecord.PreloadFailureCount = Math.Max(0, originRecord.PreloadFailureCount) + 1; } } originRecord.LastUpdatedUnix = GetNowUnixSeconds(); if (success) { cacheState.HasReadyCache = true; cacheState.ReadyFromSmart = cacheState.PendingSmartLoad; cacheState.ReadyOriginScene = originScene; cacheState.ReadyOriginScore = cacheState.PendingSmartLoad ? cacheState.PendingOriginScore : EstimateShowProbability(adType, originScene); cacheState.ReadyLoadedUnix = GetNowUnixSeconds(); cacheState.ReadyRequestUnix = cacheState.PendingRequestUnix; cacheState.Consumed = false; } else if (cacheState.PendingSmartLoad) { ClearReadyCache(cacheState); } cacheState.PendingSmartLoad = false; cacheState.PendingOriginScene = null; cacheState.PendingOriginScore = 0f; cacheState.PendingRequestUnix = 0; } private static void MarkImmediateHit(AD_Type adType, string scenario, string slotId) { var consumedScene = NormalizeScenario(scenario); var currentSceneRecord = GetOrCreateState(adType, consumedScene); currentSceneRecord.ImmediateHitCount = Math.Max(0, currentSceneRecord.ImmediateHitCount) + 1; currentSceneRecord.LastUpdatedUnix = GetNowUnixSeconds(); var cacheState = GetOrCreateCacheState(adType, slotId); if (!cacheState.HasReadyCache) { currentSceneRecord.UnattributedCacheHitCount = Math.Max(0, currentSceneRecord.UnattributedCacheHitCount) + 1; } } private static void MarkCacheConsumed(AD_Type adType, string scenario, string slotId) { var cacheState = GetOrCreateCacheState(adType, slotId); if (!cacheState.HasReadyCache || cacheState.Consumed) { return; } var consumedScene = NormalizeScenario(scenario); var originScene = NormalizeScenario(cacheState.ReadyOriginScene); var currentSceneRecord = GetOrCreateState(adType, consumedScene); var originSceneRecord = GetOrCreateState(adType, originScene); if (cacheState.ReadyFromSmart) { currentSceneRecord.SmartCacheHitCount = Math.Max(0, currentSceneRecord.SmartCacheHitCount) + 1; originSceneRecord.SmartPreloadConsumedCount = Math.Max(0, originSceneRecord.SmartPreloadConsumedCount) + 1; originSceneRecord.LastSmartPreloadConsumedByScene = consumedScene; originSceneRecord.LastSmartPreloadConsumedUnix = GetNowUnixSeconds(); if (string.Equals(originScene, consumedScene, StringComparison.Ordinal)) { originSceneRecord.SmartPreloadConsumedSameSceneCount = Math.Max(0, originSceneRecord.SmartPreloadConsumedSameSceneCount) + 1; } else { currentSceneRecord.SmartCacheCrossSceneHitCount = Math.Max(0, currentSceneRecord.SmartCacheCrossSceneHitCount) + 1; originSceneRecord.SmartPreloadConsumedOtherSceneCount = Math.Max(0, originSceneRecord.SmartPreloadConsumedOtherSceneCount) + 1; } } currentSceneRecord.LastUpdatedUnix = GetNowUnixSeconds(); originSceneRecord.LastUpdatedUnix = GetNowUnixSeconds(); cacheState.Consumed = true; ClearReadyCache(cacheState); } private static void MarkCacheShowFailed(AD_Type adType, string scenario, string slotId) { var cacheState = GetOrCreateCacheState(adType, slotId); if (!cacheState.HasReadyCache) { return; } var originScene = NormalizeScenario(cacheState.ReadyOriginScene); if (cacheState.ReadyFromSmart) { var originSceneRecord = GetOrCreateState(adType, originScene); originSceneRecord.SmartPreloadShowFailureCount = Math.Max(0, originSceneRecord.SmartPreloadShowFailureCount) + 1; originSceneRecord.LastSmartPreloadConsumedByScene = NormalizeScenario(scenario); originSceneRecord.LastSmartPreloadConsumedUnix = GetNowUnixSeconds(); originSceneRecord.LastUpdatedUnix = GetNowUnixSeconds(); } ClearReadyCache(cacheState); } private static void MarkCacheExpiredIfStale(AD_Type adType, string slotId) { var cacheState = GetOrCreateCacheState(adType, slotId); if (!cacheState.HasReadyCache) { return; } if (ADManager.Instance.IsRealy(adType)) { return; } if (cacheState.ReadyFromSmart) { var originSceneRecord = GetOrCreateState(adType, NormalizeScenario(cacheState.ReadyOriginScene)); originSceneRecord.SmartPreloadExpiredCount = Math.Max(0, originSceneRecord.SmartPreloadExpiredCount) + 1; originSceneRecord.LastUpdatedUnix = GetNowUnixSeconds(); } ClearReadyCache(cacheState); } private static TapadnSmartLoadCacheState GetOrCreateCacheState(AD_Type adType, string slotId) { var key = ComposeCacheKey(adType, slotId); if (_cacheStates.TryGetValue(key, out var state) && state != null) { return state; } state = new TapadnSmartLoadCacheState(); _cacheStates[key] = state; return state; } private static void ClearReadyCache(TapadnSmartLoadCacheState state) { if (state == null) { return; } state.HasReadyCache = false; state.ReadyFromSmart = false; state.ReadyOriginScene = null; state.ReadyOriginScore = 0f; state.ReadyLoadedUnix = 0; state.ReadyRequestUnix = 0; state.Consumed = false; } private static string GetCacheDebugDump() { if (_cacheStates == null || _cacheStates.Count == 0) { return "empty"; } var lines = new List(); foreach (var entry in _cacheStates) { var state = entry.Value; if (state == null) { continue; } lines.Add($"[{entry.Key}]prepared={state.PreparedSmartLoad}:{NormalizeScenario(state.PreparedOriginScene)} pending={state.PendingSmartLoad}:{NormalizeScenario(state.PendingOriginScene)} ready={state.HasReadyCache}:{state.ReadyFromSmart}:{NormalizeScenario(state.ReadyOriginScene)} score={state.ReadyOriginScore:F3}"); } return string.Join(" | ", lines); } private static string GetSnapshotCsvHeader() { return "ad_type,scenario,enter_count,play_request_count,preload_request,preload_success,preload_fail,show_request,show_start,show_fail,immediate_hit,smart_cache_hit,smart_cache_cross_scene_hit,unattributed_cache_hit,smart_preload_consumed,smart_preload_consumed_same_scene,smart_preload_consumed_other_scene,smart_preload_show_fail,smart_preload_expired,last_smart_preload_consumed_by_scene,last_smart_preload_consumed_unix,last_preload_unix,last_updated_unix,last_score"; } private static TapadnSmartLoadPolicyItem ResolvePolicy(AD_Type adType, string scenario) { if (_runtimeConfig == null) { _runtimeConfig = BuildDefaultConfig(); } var normalizedScenario = NormalizeScenario(scenario); var exactKey = ComposeKey(adType, normalizedScenario); if (_policies.TryGetValue(exactKey, out var exactPolicy)) { return exactPolicy; } var defaultScenarioKey = ComposeKey(adType, PolicyDefaultScene); if (_policies.TryGetValue(defaultScenarioKey, out var defaultPolicy)) { return defaultPolicy; } return _runtimeConfig?.GlobalDefault != null ? _runtimeConfig.GlobalDefault : BuildDefaultConfig().GlobalDefault; } private static void EnsureDefaultPolicies(TapadnSmartLoadConfig config) { _policies.Clear(); if (config == null || config.ScenePolicies == null) { config = BuildDefaultConfig(); } if (config.GlobalDefault == null) { config.GlobalDefault = BuildDefaultPolicy(); } foreach (var policy in config.ScenePolicies) { if (policy == null || string.IsNullOrWhiteSpace(policy.Scene)) { continue; } if (policy.AdType < 0 || policy.AdType > 2) { continue; } var key = ComposeKey((AD_Type)policy.AdType, NormalizeScenario(policy.Scene)); _policies[key] = new TapadnSmartLoadPolicyItem { AdType = policy.AdType, Scene = NormalizeScenario(policy.Scene), BaseProbability = Mathf.Clamp01(policy.BaseProbability), PreloadThreshold = Mathf.Clamp(policy.PreloadThreshold, 0f, 1f), CooldownSeconds = Math.Max(0, policy.CooldownSeconds), MinSamplesForConfidence = Math.Max(1, policy.MinSamplesForConfidence), DecayHalfLifeHours = Math.Max(1f, policy.DecayHalfLifeHours), }; } foreach (AD_Type adType in Enum.GetValues(typeof(AD_Type))) { var key = ComposeKey(adType, PolicyDefaultScene); if (!_policies.ContainsKey(key)) { _policies[key] = ClonePolicy(config.GlobalDefault, adType); } } } private static TapadnSmartLoadPolicyItem ClonePolicy(TapadnSmartLoadPolicyItem source, AD_Type adType) { if (source == null) { source = BuildDefaultPolicy(); } return new TapadnSmartLoadPolicyItem { AdType = (int)adType, Scene = PolicyDefaultScene, BaseProbability = source.BaseProbability, PreloadThreshold = source.PreloadThreshold, CooldownSeconds = source.CooldownSeconds, MinSamplesForConfidence = source.MinSamplesForConfidence, DecayHalfLifeHours = source.DecayHalfLifeHours }; } private static TapadnSmartLoadConfig BuildConfig(TapadnControllerOptions options) { var config = BuildDefaultConfig(); MergePolicyJson(config, ReadPolicyAssetJson(options)); MergePolicyJson(config, options?.SmartPreloadConfigJson); MergePolicyJson(config, options?.SmartPreloadRemoteConfigJson); return config; } private static string ReadPolicyAssetJson(TapadnControllerOptions options) { if (options == null || string.IsNullOrWhiteSpace(options.SmartPreloadConfigAssetPath)) { return null; } try { var asset = Resources.Load(options.SmartPreloadConfigAssetPath); return asset != null ? asset.text : null; } catch (Exception exception) { Debug.LogWarning($"[TapADN SmartLoad] Load policy asset failed: {exception.Message}"); return null; } } private static TapadnSmartLoadConfig BuildDefaultConfig() { return new TapadnSmartLoadConfig { GlobalDefault = BuildDefaultPolicy(), ScenePolicies = new List(), }; } private static TapadnSmartLoadPolicyItem BuildDefaultPolicy() { return new TapadnSmartLoadPolicyItem { AdType = -1, Scene = PolicyDefaultScene, BaseProbability = 0.08f, PreloadThreshold = 0.75f, CooldownSeconds = 120, MinSamplesForConfidence = 8, DecayHalfLifeHours = 72f }; } private static void MergePolicyJson(TapadnSmartLoadConfig target, string json) { if (string.IsNullOrWhiteSpace(json)) { return; } try { var parsed = JsonUtility.FromJson(json); if (parsed == null) { return; } if (parsed.GlobalDefault != null) { target.GlobalDefault = NormalizePolicy(parsed.GlobalDefault); } if (parsed.ScenePolicies == null || parsed.ScenePolicies.Count == 0) { return; } if (target.ScenePolicies == null) { target.ScenePolicies = new List(); } foreach (var policy in parsed.ScenePolicies) { if (policy == null) { continue; } if (policy.AdType < 0 || policy.AdType > 2) { continue; } if (string.IsNullOrWhiteSpace(policy.Scene)) { continue; } target.ScenePolicies.Add(policy); } } catch (Exception exception) { Debug.LogWarning($"[TapADN SmartLoad] Merge policy JSON failed: {exception.Message}"); } } private static string EncodeCsv(string value) { if (string.IsNullOrEmpty(value)) { return "0"; } if (value.Contains(",") || value.Contains("\"") || value.Contains("\n")) { return $"\"{value.Replace("\"", "\"\"")}\""; } return value; } private static TapadnSmartLoadPolicyItem NormalizePolicy(TapadnSmartLoadPolicyItem policy) { return new TapadnSmartLoadPolicyItem { AdType = policy.AdType, Scene = NormalizeScenario(policy.Scene), BaseProbability = Mathf.Clamp01(policy.BaseProbability), PreloadThreshold = Mathf.Clamp(policy.PreloadThreshold, 0f, 1f), CooldownSeconds = Math.Max(0, policy.CooldownSeconds), MinSamplesForConfidence = Math.Max(1, policy.MinSamplesForConfidence), DecayHalfLifeHours = Math.Max(1f, policy.DecayHalfLifeHours), }; } private static TapadnSmartLoadSceneState GetOrCreateState(AD_Type adType, string scenario) { var key = ComposeKey(adType, NormalizeScenario(scenario)); if (_states.TryGetValue(key, out var state)) { return state; } state = new TapadnSmartLoadSceneState { AdType = (int)adType, Scenario = NormalizeScenario(scenario), EnterCount = 0, PlayRequestCount = 0, LastUpdatedUnix = GetNowUnixSeconds() }; _states[key] = state; return state; } private static void LoadStates() { try { var raw = PlayerPrefs.GetString(StatsPrefsKey, string.Empty); if (string.IsNullOrWhiteSpace(raw)) { return; } var data = JsonUtility.FromJson(raw); if (data == null || data.Entries == null) { return; } foreach (var entry in data.Entries) { if (entry == null || entry.AdType < 0) { continue; } var adType = SafeToAdType(entry.AdType); var scenario = NormalizeScenario(entry.Scenario); if (adType == null) { continue; } var key = ComposeKey(adType.Value, scenario); _states[key] = entry; } } catch (Exception exception) { Debug.LogWarning($"[TapADN SmartLoad] Load state failed: {exception.Message}"); } } private static AD_Type? SafeToAdType(int value) { if (value < 0 || value > 2) { return null; } return (AD_Type)value; } private static void SaveStates() { var data = new TapadnSmartLoadStateData(); data.Entries = new List(); foreach (var entry in _states.Values) { data.Entries.Add(entry); } var raw = JsonUtility.ToJson(data); if (string.IsNullOrWhiteSpace(raw)) { return; } PlayerPrefs.SetString(StatsPrefsKey, raw); PlayerPrefs.Save(); } private static string ComposeKey(AD_Type adType, string scenario) { return ((int)adType) + "|" + NormalizeScenario(scenario); } private static string ComposeCacheKey(AD_Type adType, string slotId) { return ((int)adType) + "|" + NormalizeSlotId(slotId); } private static string NormalizeSlotId(string slotId) { return string.IsNullOrWhiteSpace(slotId) ? "__default_slot__" : slotId.Trim(); } private static string NormalizeScenario(string scenario) { if (string.IsNullOrWhiteSpace(scenario)) { return DefaultScene; } return scenario.Trim(); } private static long GetNowUnixSeconds() { return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; } [Serializable] private sealed class TapadnSmartLoadConfig { public TapadnSmartLoadPolicyItem GlobalDefault; public List ScenePolicies; } [Serializable] private sealed class TapadnSmartLoadPolicyItem { public int AdType; public string Scene; public float BaseProbability; public float PreloadThreshold; public int CooldownSeconds; public int MinSamplesForConfidence; public float DecayHalfLifeHours; } [Serializable] private sealed class TapadnSmartLoadPolicyItemDto { public int AdType; public string Scene; public float BaseProbability = 0.08f; public float PreloadThreshold = 0.75f; public int CooldownSeconds = 120; public int MinSamplesForConfidence = 8; public float DecayHalfLifeHours = 72f; } [Serializable] private sealed class TapadnSmartLoadStateData { public int Version = 1; public List Entries; } private sealed class TapadnSmartLoadCacheState { public bool PreparedSmartLoad; public string PreparedOriginScene; public float PreparedOriginScore; public long PreparedRequestUnix; public bool PendingSmartLoad; public string PendingOriginScene; public float PendingOriginScore; public long PendingRequestUnix; public bool HasReadyCache; public bool ReadyFromSmart; public string ReadyOriginScene; public float ReadyOriginScore; public long ReadyLoadedUnix; public long ReadyRequestUnix; public bool Consumed; } [Serializable] private sealed class TapadnSmartLoadSceneState { public int AdType; public string Scenario; public int EnterCount; public int PlayRequestCount; public int PreloadRequestCount; public int PreloadSuccessCount; public int PreloadFailureCount; public int ShowRequestCount; public int ShowStartCount; public int ShowFailureCount; public int ImmediateHitCount; public int SmartCacheHitCount; public int SmartCacheCrossSceneHitCount; public int UnattributedCacheHitCount; public int SmartPreloadConsumedCount; public int SmartPreloadConsumedSameSceneCount; public int SmartPreloadConsumedOtherSceneCount; public int SmartPreloadShowFailureCount; public int SmartPreloadExpiredCount; public string LastSmartPreloadConsumedByScene; public long LastSmartPreloadConsumedUnix; public long LastUpdatedUnix; public long LastPreloadUnix; } }