# Didabu开发指南
Didabu采用面向接口的开发模式，通过实现指定的接口可实现不同功能的不同实现。

## 埋点事件
通过实现ILogEventReporter（EventReporterAbstract），Didabu可以将埋点事件同时上报到不同的事件分析平台，埋点事件上报器接口定义如下：
```csharp
    public interface ILogEventReporter
    {
        /// <summary>
        /// 事件上报器名称
        /// </summary>
        string Name { get; }

        /// <summary>
        /// 初始化事件上报器
        /// </summary>
        /// <param name="commonEventParameterNames">通用事件参数名称</param>
        /// <param name="eventNameMap">事件名称映射</param>
        /// <param name="eventParameterNameMap">事件参数名称映射</param>
        /// <returns></returns>
        Task Initialize(List<string> commonEventParameterNames,
            Dictionary<string, string> eventNameMap,
            Dictionary<string, string> eventParameterNameMap);

        /// <summary>
        /// 添加通用事件参数
        /// </summary>
        /// <param name="eventParameterName">事件参数名称</param>
        /// <param name="value">事件参数值</param>        
        void AddCommonEventParameter(string eventParameterName, string value);

        /// <summary>
        /// 上报事件
        /// </summary>
        /// <param name="name">事件名称</param>
        /// <param name="eventParameters">事件参数</param>
        /// <returns></returns>
        Task LogEvent(string name, Dictionary<string, string> eventParameters); 
    }

        public abstract class EventReporterAbstract : MogafaBase, ILogEventReporter
    {
        /// <summary>
        /// 名称
        /// </summary>
        public abstract string Name { get; }
        protected List<string> commonEventParameterNames;
        /// <summary>
        /// 通用事件参数, 每次上报事件时都需要的事件参数 比如用户ID等
        /// 这个配置是和不同log event Provider不想关
        /// </summary>
        protected Dictionary<string, string> commonEventParameters;
        /// <summary>
        /// 事件名称映射表，
        /// 每个不同的log event Provider配置不一样
        /// </summary>
        protected Dictionary<string, string> eventNameMap;
        /// <summary>
        /// 时间参数名称映射表
        /// 每个不同的log event Provider配置不一样
        /// </summary>
        protected Dictionary<string, string> eventParameterNameMap;

        protected EventReporterAbstract()
        {
            commonEventParameters = new Dictionary<string, string>();
        }

        public Task Initialize(List<string> commonEventParameterNames,
            Dictionary<string, string> eventNameMap,
            Dictionary<string, string> eventParameterNameMap)
        {
            Logger.LogInformation($"Log event reporter {Name} init start.");
            this.eventNameMap = eventNameMap;
            if(this.eventNameMap == null)
            {
                this.eventNameMap = new Dictionary<string, string>();
            }
            this.eventParameterNameMap = eventParameterNameMap;
            if(this.eventParameterNameMap == null)
            {
                this.eventParameterNameMap = new Dictionary<string, string>();
            }
            this.commonEventParameterNames = commonEventParameterNames;
            if(this.commonEventParameterNames == null)
            {
                this.commonEventParameterNames = new List<string>();
            }
            Logger.LogInformation($"Log event reporter {Name} init finished.");
            return Task.CompletedTask;
        }
        public void AddCommonEventParameter(string eventParameterName, string value)
        {
            if (!commonEventParameterNames.Contains(eventParameterName))
            {
                return;
            }
            if (commonEventParameters.ContainsKey(eventParameterName))
            {
                commonEventParameters.Remove(eventParameterName);
            }
            commonEventParameters.Add(eventParameterName, value);
        }
        public Task LogEvent(string name, Dictionary<string, string> eventParameters)
        {
            Logger.LogInformation($"Log event to: {Name}, event name: {name}");
            var cloneEventParameters = new Dictionary<string, string>(commonEventParameters);
            foreach (var parameter in eventParameters)
            {
                cloneEventParameters.Add(parameter.Key, parameter.Value);
            }
            var mapName = MapName(name, eventNameMap);
            var mapEventParameters = new Dictionary<string, string>();
            foreach (var parameter in cloneEventParameters)
            {
                var mapParameterName = MapName(parameter.Key, eventParameterNameMap);
                mapEventParameters.Add(mapParameterName, parameter.Value);
            }
            return LogEventInternal(mapName, mapEventParameters);
        }
        private string MapName(string eventName, Dictionary<string, string> nameMap)
        {
            if(nameMap == null || nameMap.Count == 0)
            {
                return eventName;
            }
            var needMapName = nameMap.Keys.FirstOrDefault(nm =>
                (nm.EndsWith("_") && (eventName.StartsWith(nm) || eventName == nm.Substring(0, nm.Length - 1)) ||
                (!nm.EndsWith("_") && eventName == nm)));
            if (needMapName == null)
            {
                return eventName;
            }
            var mapName = nameMap[needMapName];
            if (!needMapName.EndsWith("_"))
            {
                return mapName;
            }
            mapName = eventName.Replace(needMapName, $"{mapName}_");
            if (mapName.EndsWith("_"))
            {
                mapName = mapName.Substring(0, mapName.Length - 1);
            }
            return mapName;
        }
        protected abstract Task LogEventInternal(string name, Dictionary<string, string> eventParameters);
    }
```
针对不同的事件平台，比如（Firebase Analytics或AppsFlyer）实现ILogEventReporter（EventReporterAbstract）接口，并通过LogEventReporter管理器添加，然后就可以使用通用的埋点事件API向不同的事件平台发送，比如：
``` csharp
    public class FirebaseAnalyticsLogEventReporter : EventReporterAbstract
    {
        public override string Name => "FirebaseAnalytics";

        protected override Task LogEventInternal(string name, Dictionary<string, string> eventParameters)
        {
            Firebase.Analytics.FirebaseAnalytics.LogEvent(name, ToParameters(eventParameters));
            return Task.CompletedTask;
        }
        private Parameter[] ToParameters(Dictionary<string, string> eventParameters)
        {
            var parameters = new List<Parameter>(); 
            foreach(var key in eventParameters.Keys)
            {
                parameters.Add(new Parameter(key, eventParameters[key]));
            }
            return parameters.ToArray();
        }
    }
```
Firebase Analytics初始化代码（Didabu扩展方法）：
```csharp
    public static class DidabuFirebaseAnalyticsExtension
    {
        public static void InitFirebase(this Didabu didabu)
        {
            FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task => {
                var dependencyStatus = task.Result;
                if (dependencyStatus == DependencyStatus.Available)
                {
                    FirebaseAnalytics.SetAnalyticsCollectionEnabled(true);

                    // Set the user's sign up method.
                    FirebaseAnalytics.SetUserProperty(
                      FirebaseAnalytics.UserPropertySignUpMethod,
                      "Google");
                    // Set the user ID.
                    if (!string.IsNullOrEmpty(didabu.User.DidabuId))
                    {
                        FirebaseAnalytics.SetUserId(didabu.User.DidabuId);
                    }
                    else
                    {
                        didabu.OnInitialSuccessed += OnDidabuInitialSuccessed;
                        if (!string.IsNullOrEmpty(didabu.User.DidabuId))
                        {
                            FirebaseAnalytics.SetUserId(didabu.User.DidabuId);
                        }
                    }
                    
                    // Set default session duration values.
                    FirebaseAnalytics.SetSessionTimeoutDuration(new TimeSpan(0, 30, 0));
                }
                else
                {
                    Debug.LogError(
                      "Could not resolve all Firebase dependencies: " + dependencyStatus);
                }
            });

        }

        private static void OnDidabuInitialSuccessed(string obj)
        {
            FirebaseAnalytics.SetUserId(obj);
        }

        public static void UseFirebaseAnalyticsLogEvent(this Didabu didabu,
        List<string> commonEventParameterNames = null,
        Dictionary<string, string> eventNameMap = null,
        Dictionary<string, string> eventParameterNameMap = null)
        {
            didabu.LogEventReporter.AddEventReporter(new FirebaseAnalyticsLogEventReporter(),
                commonEventParameterNames, eventNameMap, eventParameterNameMap);
        }
    }
```
然后如下在App中使用Firebase Analytics：
```csharp
        //初始化Firebase
        Didabu.Application.InitFirebase();
        //使用Firebase Analytics上报埋点事件
        Didabu.Application.UseFirebaseAnalyticsLogEvent();

        Didabu.Application.LogEvent("test_event");
```
### LogEvent事件名称及事件参数名称映射
因为各Event平台会对特殊的事件名称和时间参数的名称会做汇总展示，比如AppsFlyer用af_purchase事件来记录App的购买事件，同时用af_revenue事件参数来记录购买金额，而Firebase Analytics用in_app_purchase事件来记录App的购买事件，同时用value事件参数来记录购买金额，而我们用Didabu来上报购买事件时，我们并不知道事件会上报到哪些平台，所以不能使用各平台特定的事件名和事件参数名来上报，这时就需要使用事件名称映射及时间参数名称映射，比如Didabu用ddb_purchase来记录App的购买事件，用purchase_value事件参数来记录购买金额，那么使用FirebaseAnalytics时可以传递如下参数来告诉Didabu如何映射事件名称和事件参数名称：
``` csharp
        //初始化时
        Didabu.Application.UseFirebaseAnalyticsLogEvent(eventNameMap:new Dictionary<string, string>
        {
            {"ddb_purchase","in_app_purchase" }
        },
        eventParameterNameMap:new Dictionary<string, string>
        {
            {"purchase_value", "value" }
        });
        //记录购买（广告收益）事件
        Didabu.Application.LogEvent("ddb_purchase", new Dictionary<string, string>{
            {"ddb_purchase", "0.00856"}
        });

        //同样对于AppsFlyer，初始化时映射好事件名称和事件参数名称，这样不需要修改记录购买事件的代码也能保证上报到AppsFlyer中的事件也能正确在AppsFlyer的控制台网页中正确展示出来
        Didabu.Application.UseFirebaseAnalyticsLogEvent(eventNameMap:new Dictionary<string, string>
        {
            {"ddb_purchase","af_purchase" }
        },
        eventParameterNameMap:new Dictionary<string, string>
        {
            {"purchase_value", "af_revenue" }
        });        
```
#### 参考：
https://support.appsflyer.com/hc/zh-cn/articles/115005544169-Rich-in-app-events-guide#%E4%BA%8B%E4%BB%B6%E7%BB%93%E6%9E%84-%E9%A2%84%E5%AE%9A%E4%B9%89%E4%BA%8B%E4%BB%B6%E5%8F%82%E6%95%B0

https://support.google.com/firebase/answer/9234069?hl=zh-Hans

### 事件上报拦截器
有时候我们需要对上报事件进行处理，比如要对click事件计数处理：第一次触发click事件时同时触发一个click_1的事件，第二次触发click事件时同时触发一个click_2的事件，这是就需要实现事件上报拦截器的接口，该接口定义如下：
```csharp
    public interface ILogEventIntercept
    {
        /// <summary>
        /// 拦截器名称
        /// </summary>
        string Name { get; }
        /// <summary>
        /// 拦截器排序
        /// </summary>
        int Order { get; }
        /// <summary>
        /// 拦截器执行逻辑
        /// </summary>
        /// <param name="eventName">需要拦截的事件名称</param>
        /// <param name="parameters">事件参数列表</param>
        /// <returns>返回新的事件列表（某一个事件最终可能会生成多个事件）</returns>
        Task<Dictionary<string, Dictionary<string, string>>> Execute(
            string eventName, 
            Dictionary<string, string> parameters);
    }
```
目前didabu提供三种事件拦截器：
1. 上面提到的事件计数拦截器，代码参见：https://github.com/mogafa/csharp-app/blob/main/src/LogEvents/Intercepts/EventCounterIntercept.cs
2. AB分组事件拦截器，比如当前分为A、B两组，需要分别记录每组的click事件，那么A组上报的click事件就应该为click_A，B组上报的click事件就应该为click_b，该拦截器就是实现该功能的，代码参见：https://github.com/mogafa/csharp-app/blob/main/src/LogEvents/Intercepts/AbGroupEventIntercept.cs
3. 资产上报拦截器，每次上报事件的时候都把用户当前的资产作为事件参数上报，代码参见：https://github.com/mogafa/csharp-app/blob/main/src/Assets/EventIntercepts/AssetEventIntercept.cs
   
另外还有一个事件生成器不是实现事件拦截器接口来实现的，而是通过监听用户资产变化来生成新事件以达到如下需求：当用户的某项资产达到指定值时需要上报相应的事件，比如美元达到100，200，500时分别需要上报dollar_100, dollar_200, dollar_500事件，代码参见：https://github.com/mogafa/csharp-app/blob/main/src/Assets/EventIntercepts/AssetChangedEventGenerator.cs

### LogEvent相关代码仓库
https://github.com/mogafa/csharp-common

https://github.com/mogafa/csharp-app

https://github.com/mogafa/unity-firebase-analytics

https://github.com/mogafa/unity-appsflyer

https://github.com/didabu/unity-firebase-analytics

https://github.com/didabu/unity-appsflyer

## 广告组件
目前Didabu支持三种广告模式：
1. 激励视频（Rewarded Video）
2. 插屏（Interstitial Video）
3. Banner

### 接口定义   
这里以激励视频插件开发介绍一下开发流程，激励视频有各种回调数据，我们先把回调事件定义好：
```csharp
    /// <summary>
    /// 激励视频点击回调
    /// </summary>
    /// <param name="adUnitId">广告单元ID</param>
    /// <param name="placement">广告位置</param>
    public delegate void RewardedVideoClickedHandler(string adUnitId, string placement);
    /// <summary>
    /// 激励视频关闭回调
    /// </summary>
    /// <param name="adUnitId">广告单元ID</param>
    /// <param name="placement">广告位置</param>
    public delegate void RewardedVideoClosedHandler(string adUnitId, string placement);
    /// <summary>
    /// 激励视频完成回调
    /// </summary>
    /// <param name="adUnitId">广告单元ID</param>
    /// <param name="placement">广告位置</param>
    public delegate void RewardedVideoCompletedHandler(string adUnitId, string placement);
    /// <summary>
    /// 激励视频展示回调
    /// </summary>
    /// <param name="adUnitId">广告单元ID</param>
    /// <param name="placement">广告位置</param>
    public delegate void RewardedVideoShownHandler(string adUnitId, string placement);
    /// <summary>
    /// 激励视频展示失败回调
    /// </summary>
    /// <param name="adUnitId">广告单元ID</param>
    /// <param name="error">错误信息</param>
    /// <param name="placement">广告位置</param>
    public delegate void RewardedVideoShowFailedHandler(string adUnitId, string error, string placement);
    /// <summary>
    /// 激励视频加载成功回调
    /// </summary>
    /// <param name="adUnitId">广告单元ID</param>
    /// <param name="placement">广告位置</param>
    public delegate void RewardedVideoLoadedHandler(string adUnitId, string placement);
    /// <summary>
    /// 激励视频加载失败回调
    /// </summary>
    /// <param name="adUnitId">广告单元ID</param>
    /// <param name="error">错误信息</param>
    public delegate void RewardedVideoLoadFailedHandler(string adUnitId, string error);
    /// <summary>
    /// 激励视频收益回调
    /// </summary>
    /// <param name="adUnitId">广告单元ID</param>
    /// <param name="label">广告收益标签</param>
    /// <param name="value">广告收益</param>
    /// <param name="placement">广告位置</param>
    public delegate void RewardedVideoRevenuePaidHandler(string adUnitId, string label, float value, string placement);
```
然后是激励视频播放接口定义：
```csharp
    /// <summary>
    /// Interface of rewarded video player
    /// </summary>
    public interface IRewardedVideoPlayer
    {
        /// <summary>
        /// Occurs when [on clicked].
        /// </summary>
        event RewardedVideoClickedHandler OnClicked;
        /// <summary>
        /// Occurs when [on closed].
        /// </summary>
        event RewardedVideoClosedHandler OnClosed;
        /// <summary>
        /// Occurs when [on shown].
        /// </summary>
        event RewardedVideoShownHandler OnShown;
        /// <summary>
        /// Occurs when [on loaded].
        /// </summary>
        event RewardedVideoLoadedHandler OnLoaded;
        /// <summary>
        /// Occurs when [on failed].
        /// </summary>
        event RewardedVideoLoadFailedHandler OnLoadFailed;
        /// <summary>
        /// Occurs when [on failed to play].
        /// </summary>
        event RewardedVideoShowFailedHandler OnShowFailed;
        /// <summary>
        /// Occurs when [on revenue to pay].
        /// </summary>
        event RewardedVideoRevenuePaidHandler OnRevenuePaid;
        event RewardedVideoCompletedHandler OnCompleted;
        /// <summary>
        /// Requests the rewarded video.
        /// </summary>
        /// <param name="adUnitId">The ad unit identifier.</param>
        /// <returns></returns>
        void RequestRewardedVideo(string adUnitId);
        /// <summary>
        /// Determines whether [has rewarded video] [the specified ad unit identifier].
        /// </summary>
        /// <param name="adUnitId">The ad unit identifier.</param>
        /// <returns></returns>
        bool HasRewardedVideo(string adUnitId);
        /// <summary>
        /// Shows the rewarded video.
        /// </summary>
        /// <param name="adUnitId">The ad unit identifier.</param>
        /// <param name="placement">The placement.</param>
        void ShowRewardedVideo(string adUnitId, string placement);
    }
```
这里的接口设计尽量简单，不涉及广告请求策略等，只需要提供广告单元ID去请求广告，判断广告是否加载成功以及展示广告。
各广告平台实现这个接口就可以了，我们来看看Applovin Max平台：
```csharp
    public class MaxRewardedVideoPlayer : MogafaBase, IRewardedVideoPlayer
    {
        public MaxRewardedVideoPlayer()
        {
            MaxSdkCallbacks.Rewarded.OnAdLoadedEvent += OnRewardedAdLoadedEvent;
            MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent += OnRewardedAdFailedEvent;
            MaxSdkCallbacks.Rewarded.OnAdDisplayFailedEvent += OnRewardedAdFailedToDisplayEvent;
            MaxSdkCallbacks.Rewarded.OnAdDisplayedEvent += OnRewardedAdDisplayedEvent;
            MaxSdkCallbacks.Rewarded.OnAdClickedEvent += OnRewardedAdClickedEvent;
            MaxSdkCallbacks.Rewarded.OnAdHiddenEvent += OnRewardedAdDismissedEvent;
            MaxSdkCallbacks.Rewarded.OnAdRevenuePaidEvent += OnRewardedAdRevenuePaidEvent;
        }

        public event RewardedVideoClickedHandler OnClicked;
        public event RewardedVideoClosedHandler OnClosed;
        public event RewardedVideoShownHandler OnShown;
        public event RewardedVideoShowFailedHandler OnShowFailed;
        public event RewardedVideoLoadedHandler OnLoaded;
        public event RewardedVideoLoadFailedHandler OnLoadFailed;
        public event RewardedVideoRevenuePaidHandler OnRevenuePaid;
        public event RewardedVideoCompletedHandler OnCompleted;

        public void OnRewardedAdLoadedEvent(string adUnitId, AdInfo adInfo)
        {
            Logger.LogDebug($"Rewarded video loaded, ad info:{JsonConvert.SerializeObject(adInfo)}");
            OnLoaded?.Invoke(adInfo.AdUnitIdentifier, adInfo.Placement);
        }
        public void OnRewardedAdFailedEvent(string adUnitId, ErrorInfo errorInfo)
        {
            var errorMessage = $"Rewarded video load failed. ErrorCode:{errorInfo.Code},Message:{errorInfo.Message}, FailureInfo:{errorInfo.AdLoadFailureInfo}";
            Logger.LogError(errorMessage);
            OnLoadFailed?.Invoke(adUnitId, errorMessage);
        }
        public void OnRewardedAdFailedToDisplayEvent(string adUnitId, ErrorInfo errorInfo, AdInfo adInfo)
        {
            var errorMessage = $"Rewarded video show failed. ErrorCode:{errorInfo.Code},Message:{errorInfo.Message}, FailureInfo:{errorInfo.AdLoadFailureInfo}";
            Logger.LogError(errorMessage);
            OnShowFailed?.Invoke(adUnitId, errorMessage, adInfo.Placement);
        }
        public void OnRewardedAdDisplayedEvent(string adUnitId, AdInfo adInfo)
        {
            Logger.LogDebug($"Rewarded shown, ad info:{JsonConvert.SerializeObject(adInfo)}");
            OnShown?.Invoke(adUnitId, adInfo.Placement);
        }
        public void OnRewardedAdClickedEvent(string adUnitId, AdInfo adInfo)
        {
            Logger.LogDebug($"Rewarded clicked, ad info:{JsonConvert.SerializeObject(adInfo)}");
            OnClicked?.Invoke(adUnitId, adInfo.Placement);
        }
        public void OnRewardedAdDismissedEvent(string adUnitId, AdInfo adInfo)
        {
            Logger.LogDebug($"Rewarded hidden, ad info:{JsonConvert.SerializeObject(adInfo)}");
            OnClosed?.Invoke(adUnitId, adInfo.Placement);
        }
        public void OnRewardedAdRevenuePaidEvent(string adUnitId, AdInfo adInfo)
        {
            Logger.LogDebug($"Rewarded revenue paid, ad info:{JsonConvert.SerializeObject(adInfo)}");
            OnRevenuePaid?.Invoke(adUnitId, adInfo.NetworkName, (float)adInfo.Revenue, adInfo.Placement);
            OnCompleted?.Invoke(adUnitId, adInfo.Placement);
        }

        public bool HasRewardedVideo(string adUnitId)
        {
            return MaxSdk.IsRewardedAdReady(adUnitId);
        }

        public void RequestRewardedVideo(string adUnitId)
        {
            MaxSdk.LoadRewardedAd(adUnitId);
        }

        public void ShowRewardedVideo(string adUnitId, string placement)
        {
            MaxSdk.ShowRewardedAd(adUnitId, placement);
        }
    }
```

### 广告策略
```csharp
    public interface IAdPlayingStrategy
    {
        string StrategyName { get; }
        string GetRewardedVideoAdUnitId(string placement);
        string GetInterstitialVideoAdUnitId(string placement);
        string GetBannerAdUnitId(string placement);
        void SetRewardedVideoPlayer(IRewardedVideoPlayer rewardedVideoPlayer);
        void SetInterstitialVideoPlayer(IInterstitialVideoPlayer interstitialVideoPlayer);
        void SetBannerPlayer(IBannerPlayer bannerVideoPlayer);
    }
```