Add TapADN smart preload attribution

This commit is contained in:
2026-06-05 21:44:35 +08:00
parent c21bdec3fe
commit fd98a7f541
48 changed files with 9441 additions and 18 deletions

View File

@@ -0,0 +1,981 @@
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<string, TapadnSmartLoadSceneState> _states = new Dictionary<string, TapadnSmartLoadSceneState>(StringComparer.Ordinal);
private static Dictionary<string, TapadnSmartLoadPolicyItem> _policies = new Dictionary<string, TapadnSmartLoadPolicyItem>(StringComparer.Ordinal);
private static Dictionary<int, TapadnSmartLoadCacheState> _cacheStates = new Dictionary<int, TapadnSmartLoadCacheState>();
private static bool _initialized;
private static bool _enabled;
public static bool IsEnabled => _enabled;
public static string GetDebugStateDump()
{
if (!_initialized)
{
return "{}";
}
var lines = new List<string>();
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<string, TapadnSmartLoadSceneState>(StringComparer.Ordinal);
_cacheStates = new Dictionary<int, TapadnSmartLoadCacheState>();
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);
}
public static void OnPlayRequestStarted(AD_Type adType, string scenario, bool cacheReadyAtRequest)
{
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);
}
else
{
MarkCacheExpiredIfStale(adType);
}
SaveStates();
}
public static void OnLoadRequested(AD_Type adType, string scenario)
{
if (!_initialized || !_enabled)
{
return;
}
PrepareSmartLoadRequest(adType, scenario);
}
public static void OnLoadStarted(AD_Type adType, string scenario)
{
if (!_initialized || !_enabled)
{
return;
}
BeginLoadRequest(adType, scenario);
SaveStates();
}
public static void OnLoadResult(AD_Type adType, string scenario, bool success)
{
if (!_initialized || !_enabled)
{
return;
}
CompleteLoadRequest(adType, scenario, success);
SaveStates();
}
public static void OnShowStart(AD_Type adType, string scenario)
{
if (!_initialized || !_enabled)
{
return;
}
var record = GetOrCreateState(adType, NormalizeScenario(scenario));
record.ShowStartCount = Math.Max(0, record.ShowStartCount) + 1;
record.LastUpdatedUnix = GetNowUnixSeconds();
MarkCacheConsumed(adType, scenario);
SaveStates();
}
public static void OnShowError(AD_Type adType, string scenario)
{
if (!_initialized || !_enabled)
{
return;
}
var record = GetOrCreateState(adType, NormalizeScenario(scenario));
record.ShowFailureCount = Math.Max(0, record.ShowFailureCount) + 1;
record.LastUpdatedUnix = GetNowUnixSeconds();
MarkCacheShowFailed(adType, scenario);
SaveStates();
}
private static void TryPreload(AD_Type adType, string scenario)
{
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);
OnLoadRequested(adType, scenario);
ADManager.Instance.LoadAD(adType);
var smartLoadStarted = HasPendingSmartLoadForScene(adType, scenario);
ClearPreparedLoadIfNotStarted(adType);
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)
{
var state = GetOrCreateCacheState(adType);
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)
{
var state = GetOrCreateCacheState(adType);
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)
{
var state = GetOrCreateCacheState(adType);
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)
{
var state = GetOrCreateCacheState(adType);
return state.PendingSmartLoad &&
string.Equals(NormalizeScenario(state.PendingOriginScene), NormalizeScenario(scenario), StringComparison.Ordinal);
}
private static void CompleteLoadRequest(AD_Type adType, string scenario, bool success)
{
var cacheState = GetOrCreateCacheState(adType);
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)
{
var consumedScene = NormalizeScenario(scenario);
var currentSceneRecord = GetOrCreateState(adType, consumedScene);
currentSceneRecord.ImmediateHitCount = Math.Max(0, currentSceneRecord.ImmediateHitCount) + 1;
currentSceneRecord.LastUpdatedUnix = GetNowUnixSeconds();
var cacheState = GetOrCreateCacheState(adType);
if (!cacheState.HasReadyCache)
{
currentSceneRecord.UnattributedCacheHitCount = Math.Max(0, currentSceneRecord.UnattributedCacheHitCount) + 1;
}
}
private static void MarkCacheConsumed(AD_Type adType, string scenario)
{
var cacheState = GetOrCreateCacheState(adType);
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)
{
var cacheState = GetOrCreateCacheState(adType);
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)
{
var cacheState = GetOrCreateCacheState(adType);
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)
{
var key = (int)adType;
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<string>();
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<TextAsset>(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<TapadnSmartLoadPolicyItemDto>(),
};
}
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<TapadnSmartLoadConfig>(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<TapadnSmartLoadPolicyItemDto>();
}
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<TapadnSmartLoadStateData>(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<TapadnSmartLoadSceneState>();
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 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<TapadnSmartLoadPolicyItemDto> 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<TapadnSmartLoadSceneState> 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;
}
}