commit d88855e35ef80f9f5a8313eb23f69df6ca7998c4 Author: CORE-FOLDCC\Core <1813547935@qq.com> Date: Thu Jun 4 17:16:17 2026 +0800 Implement TapADN commercialization module diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90cb4a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Bb]uild/ +[Bb]uilds/ +[Ll]ogs/ +[Uu]ser[Ss]ettings/ +Packages/CC-Framework.Commercialization/ + +.vs/ +.idea/ +.gradle/ +.vscode/ + +*.csproj +*.sln +*.suo +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +*.pidb.meta +*.pdb.meta +*.mdb.meta +sysinfo.txt +*.apk +*.aab +*.unitypackage +.DS_Store diff --git a/Assets/CHANGELOG.md b/Assets/CHANGELOG.md new file mode 100644 index 0000000..20b61b7 --- /dev/null +++ b/Assets/CHANGELOG.md @@ -0,0 +1,9 @@ +# [1.0.0] + +### 新增 + +* 接入 Dirichlet/TapADN 聚合 Unity SDK `4.2.5.0`。 +* 新增 `TapadnAdController`、激励视频、插屏、开屏播放器,实现 `CC-Framework.Commercialization` 抽象层。 +* 新增 Android 构建后处理:Manifest 权限、TapADN FileProvider、微信 OpenSDK WXEntryActivity/queries、AndroidX/Jetifier 属性。 +* 新增 `TapadnCommercialization` 便捷入口,由实现模块创建 controller 并初始化 `ADManager`。 +* 官方 Demo Sample 从主包剔除,调试内容改为可选 `Samples~`。 diff --git a/Assets/CHANGELOG.md.meta b/Assets/CHANGELOG.md.meta new file mode 100644 index 0000000..66c1416 --- /dev/null +++ b/Assets/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ae16d7859fcf496b80493823cb6fa18d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/DirichletMediation.meta b/Assets/DirichletMediation.meta new file mode 100644 index 0000000..a024212 --- /dev/null +++ b/Assets/DirichletMediation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9d3b1d66c5f548b0a92be7e4a9c7d4c1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DirichletMediation/Editor.meta b/Assets/DirichletMediation/Editor.meta new file mode 100644 index 0000000..eb68427 --- /dev/null +++ b/Assets/DirichletMediation/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d76a8c4482f84d83ab0b17d2cf0b8346 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DirichletMediation/Editor/DirichletGradlePostProcessor.cs b/Assets/DirichletMediation/Editor/DirichletGradlePostProcessor.cs new file mode 100644 index 0000000..4a47bba --- /dev/null +++ b/Assets/DirichletMediation/Editor/DirichletGradlePostProcessor.cs @@ -0,0 +1,272 @@ +#if UNITY_ANDROID +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEditor.Android; +using UnityEngine; + +namespace Dirichlet.Mediation.Editor +{ + /// + /// Post-processes the Gradle files to inject Dirichlet SDK dependencies. + /// + /// This approach allows coexistence with other SDKs (e.g., TapSDK) by injecting + /// dependencies into Unity-generated Gradle files rather than shipping static templates. + /// + public class DirichletGradlePostProcessor : IPostGenerateGradleAndroidProject + { + private const string TAG = "[DirichletMediation]"; + + // Marker comments to identify our injected content + private const string DIRICHLET_DEPS_START = "// Dirichlet Mediation Dependencies Start"; + private const string DIRICHLET_DEPS_END = "// Dirichlet Mediation Dependencies End"; + private const string DIRICHLET_REPOS_START = "// Dirichlet Mediation Repositories Start"; + private const string DIRICHLET_REPOS_END = "// Dirichlet Mediation Repositories End"; + + public int callbackOrder => 100; // Run after EDM4U (which uses lower values) + + public void OnPostGenerateGradleAndroidProject(string path) + { + var enableCsj = EditorPrefs.GetBool("Dirichlet.Android.EnableCSJ", true); + var enableGdt = EditorPrefs.GetBool("Dirichlet.Android.EnableGDT", true); + var enableIqy = EditorPrefs.GetBool("Dirichlet.Android.EnableIQY", true); + + Debug.Log($"{TAG} Processing Gradle project at: {path}"); + Debug.Log($"{TAG} CSJ enabled: {enableCsj}, GDT enabled: {enableGdt}, IQY enabled: {enableIqy}"); + + ProcessBuildGradle(path, enableCsj, enableGdt, enableIqy); + ProcessSettingsGradle(path, enableCsj, enableGdt, enableIqy); + } + + private void ProcessBuildGradle(string projectPath, bool enableCsj, bool enableGdt, bool enableIqy) + { + // Unity 2019.3+: projectPath is unityLibrary folder, build.gradle is directly inside + // Unity 2019.2 and below: projectPath might be the root, need to search + var gradlePath = Path.Combine(projectPath, "build.gradle"); + + if (!File.Exists(gradlePath)) + { + // Fallback: try to find build.gradle in subdirectories + var searchPaths = new[] + { + Path.Combine(projectPath, "unityLibrary", "build.gradle"), + Path.Combine(projectPath, "src", "main", "build.gradle") + }; + + foreach (var path in searchPaths) + { + if (File.Exists(path)) + { + gradlePath = path; + break; + } + } + } + + if (!File.Exists(gradlePath)) + { + Debug.LogWarning($"{TAG} Could not find build.gradle at {projectPath}"); + return; + } + + Debug.Log($"{TAG} Found build.gradle at: {gradlePath}"); + + var content = File.ReadAllText(gradlePath); + Debug.Log($"{TAG} Original build.gradle length: {content.Length}"); + + // Remove any previously injected content (for clean re-injection) + content = RemoveInjectedContent(content, DIRICHLET_DEPS_START, DIRICHLET_DEPS_END); + content = RemoveInjectedContent(content, DIRICHLET_REPOS_START, DIRICHLET_REPOS_END); + + // Inject repositories + content = InjectRepositories(content, enableCsj, enableGdt, enableIqy); + + // Inject dependencies + content = InjectDependencies(content, enableCsj, enableGdt, enableIqy); + + File.WriteAllText(gradlePath, content); + Debug.Log($"{TAG} Updated build.gradle with Dirichlet Mediation dependencies"); + } + + private string InjectRepositories(string content, bool enableCsj, bool enableGdt, bool enableIqy) + { + // Check if our repos are already injected + if (content.Contains(DIRICHLET_REPOS_START)) + { + return content; + } + + var reposBlock = new StringBuilder(); + reposBlock.AppendLine(DIRICHLET_REPOS_START); + reposBlock.AppendLine(" google()"); + reposBlock.AppendLine(" mavenCentral()"); + reposBlock.AppendLine(" flatDir {"); + reposBlock.AppendLine(" dirs 'libs', 'DirichletMediation/libs'"); + reposBlock.AppendLine(" }"); + + if (enableCsj) + { + reposBlock.AppendLine(" maven { url 'https://artifact.bytedance.com/repository/pangle' }"); + } + if (enableGdt) + { + reposBlock.AppendLine(" maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }"); + } + + reposBlock.AppendLine($" {DIRICHLET_REPOS_END}"); + + // Try to find repositories block and inject after opening brace + var reposPattern = new Regex(@"(repositories\s*\{)"); + if (reposPattern.IsMatch(content)) + { + content = reposPattern.Replace(content, m => + m.Groups[1].Value + "\n " + reposBlock.ToString(), 1); + Debug.Log($"{TAG} Injected repositories block"); + } + else + { + Debug.LogWarning($"{TAG} Could not find repositories block, adding one"); + var applyPattern = new Regex(@"(apply plugin:\s*'com\.android\.library'[^\n]*\n)"); + if (applyPattern.IsMatch(content)) + { + content = applyPattern.Replace(content, m => + m.Groups[1].Value + "\nrepositories {\n " + reposBlock.ToString() + "}\n", 1); + } + } + + return content; + } + + private string InjectDependencies(string content, bool enableCsj, bool enableGdt, bool enableIqy) + { + // Check if our deps are already injected + if (content.Contains(DIRICHLET_DEPS_START)) + { + return content; + } + + var depsBlock = new StringBuilder(); + depsBlock.AppendLine(DIRICHLET_DEPS_START); + + // Core Mediation AAR + depsBlock.AppendLine(" implementation(name: 'DirichletAD_Mediation_4.2.5.0', ext: 'aar')"); + + // CSJ (穿山甲) Adapter and SDK + if (enableCsj) + { + depsBlock.AppendLine(" implementation(name: 'DirichletAD_CSJ_Adapter_4.2.5.0', ext: 'aar')"); + depsBlock.AppendLine(" implementation(name: 'open_ad_sdk_7.4.2.2', ext: 'aar')"); + } + + // GDT (广点通) Adapter and SDK + if (enableGdt) + { + depsBlock.AppendLine(" implementation(name: 'DirichletAD_GDT_Adapter_4.2.5.0', ext: 'aar')"); + depsBlock.AppendLine(" implementation(name: 'GDTSDK.unionNormal.4.671.1541', ext: 'aar')"); + } + + // IQY (爱奇艺) Adapter and SDK + if (enableIqy) + { + depsBlock.AppendLine(" implementation(name: 'DirichletAD_IQY_Adapter_4.2.5.0', ext: 'aar')"); + depsBlock.AppendLine(" implementation(name: 'iadsdk-release-2.3.102.110', ext: 'aar')"); + depsBlock.AppendLine(" implementation 'com.android.support.constraint:constraint-layout:1.1.3'"); + } + + // Maven dependencies (required for SDK functionality) + depsBlock.AppendLine(" implementation 'com.android.support:recyclerview-v7:28.0.0'"); + depsBlock.AppendLine(" implementation 'com.github.bumptech.glide:glide:4.9.0'"); + depsBlock.AppendLine(" implementation 'com.android.support:support-v4:28.0.0'"); + depsBlock.AppendLine(" implementation 'com.android.support:support-annotations:28.0.0'"); + depsBlock.AppendLine(" implementation 'com.android.support:appcompat-v7:28.0.0'"); + depsBlock.AppendLine(" implementation 'com.squareup.okhttp3:okhttp:3.12.1'"); + + depsBlock.AppendLine($" {DIRICHLET_DEPS_END}"); + + // Find dependencies block and inject after opening brace + var depsPattern = new Regex(@"(dependencies\s*\{)"); + if (depsPattern.IsMatch(content)) + { + content = depsPattern.Replace(content, m => + m.Groups[1].Value + "\n " + depsBlock.ToString(), 1); + Debug.Log($"{TAG} Injected dependencies block"); + } + else + { + Debug.LogWarning($"{TAG} Could not find dependencies block"); + } + + return content; + } + + private void ProcessSettingsGradle(string projectPath, bool enableCsj, bool enableGdt, bool enableIqy) + { + var parentDir = Directory.GetParent(projectPath)?.FullName; + if (string.IsNullOrEmpty(parentDir)) + { + Debug.LogWarning($"{TAG} Could not get parent directory"); + return; + } + + var settingsPath = Path.Combine(parentDir, "settings.gradle"); + if (!File.Exists(settingsPath)) + { + Debug.LogWarning($"{TAG} Could not find settings.gradle at {settingsPath}"); + return; + } + + var content = File.ReadAllText(settingsPath); + + // Check if already injected + if (content.Contains(DIRICHLET_REPOS_START)) + { + Debug.Log($"{TAG} settings.gradle already has Dirichlet repos"); + return; + } + + // Remove any previously injected content + content = RemoveInjectedContent(content, DIRICHLET_REPOS_START, DIRICHLET_REPOS_END); + + var reposBlock = new StringBuilder(); + reposBlock.AppendLine(DIRICHLET_REPOS_START); + reposBlock.AppendLine(" google()"); + reposBlock.AppendLine(" mavenCentral()"); + reposBlock.AppendLine(" flatDir {"); + reposBlock.AppendLine(" dirs \"${project(':unityLibrary').projectDir}/libs\", \"${project(':unityLibrary').projectDir}/DirichletMediation/libs\""); + reposBlock.AppendLine(" }"); + + if (enableCsj) + { + reposBlock.AppendLine(" maven { url 'https://artifact.bytedance.com/repository/pangle' }"); + } + if (enableGdt) + { + reposBlock.AppendLine(" maven { url 'https://mirrors.tencent.com/nexus/repository/maven-public/' }"); + } + + reposBlock.AppendLine($" {DIRICHLET_REPOS_END}"); + + // Find dependencyResolutionManagement repositories block + var reposPattern = new Regex(@"(dependencyResolutionManagement\s*\{[\s\S]*?repositories\s*\{)"); + if (reposPattern.IsMatch(content)) + { + content = reposPattern.Replace(content, m => + m.Groups[1].Value + "\n " + reposBlock.ToString(), 1); + File.WriteAllText(settingsPath, content); + Debug.Log($"{TAG} Updated settings.gradle with Dirichlet Mediation repositories"); + } + else + { + Debug.LogWarning($"{TAG} Could not find dependencyResolutionManagement repositories block in settings.gradle"); + } + } + + private string RemoveInjectedContent(string content, string startMarker, string endMarker) + { + var pattern = new Regex($@"\s*{Regex.Escape(startMarker)}[\s\S]*?{Regex.Escape(endMarker)}\s*"); + return pattern.Replace(content, "\n"); + } + } +} +#endif diff --git a/Assets/DirichletMediation/Editor/DirichletGradlePostProcessor.cs.meta b/Assets/DirichletMediation/Editor/DirichletGradlePostProcessor.cs.meta new file mode 100644 index 0000000..e35ef36 --- /dev/null +++ b/Assets/DirichletMediation/Editor/DirichletGradlePostProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9018fbefe0e148e0843d119260e4c8f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DirichletMediation/Editor/DirichletMediationDependencies.xml b/Assets/DirichletMediation/Editor/DirichletMediationDependencies.xml new file mode 100644 index 0000000..a2db33c --- /dev/null +++ b/Assets/DirichletMediation/Editor/DirichletMediationDependencies.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + https://maven.google.com + + + + + + https://maven.google.com + + + + + + https://maven.google.com + + + + + + https://maven.google.com + + + + + + + https://repo.maven.apache.org/maven2 + + + + + + https://repo.maven.apache.org/maven2 + + + + + + https://maven.google.com + + + + + + + + + + + + + diff --git a/Assets/DirichletMediation/Editor/DirichletMediationDependencies.xml.meta b/Assets/DirichletMediation/Editor/DirichletMediationDependencies.xml.meta new file mode 100644 index 0000000..801cae7 --- /dev/null +++ b/Assets/DirichletMediation/Editor/DirichletMediationDependencies.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 414bdf38f1dc04fbd9e3d14650f42ada +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs b/Assets/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs new file mode 100644 index 0000000..cf8c4f2 --- /dev/null +++ b/Assets/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs @@ -0,0 +1,854 @@ +#if UNITY_IOS +using System.IO; +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; +using UnityEditor.iOS.Xcode.Extensions; +using UnityEngine; + +namespace Dirichlet.Mediation.Editor +{ + /// + /// iOS build post-processor for Dirichlet Mediation SDK. + /// Generates dynamic Podfile based on adapter settings and runs CocoaPods installation. + /// + /// 重构说明(2026-02-05): + /// - 不再在 Podfile 内做 target 猜测,改为 Post Process 直接从 pbxproj 读取真实 target 名称 + /// - 解析失败时直接中断并输出可诊断信息,不再静默兜底 + /// + /// 修复(2026-03-12): + /// - 所有 pods 统一放到 Framework target(UnityFramework),不再分层 + /// - 原因:SDK 通过 NSClassFromString 动态查找 Adapter 类,Adapter 必须与 SDK 在同一 target, + /// 否则静态库符号会被 strip 导致运行时找不到适配器类 + /// + public class DirichletMediationIOSPostProcessor + { + private const string SDKVersion = "4.2.0.2"; + private const string MinIOSVersion = "11.0"; + + // 环境变量 override(仅在解析失败或接入方工程极端定制时使用) + private const string ENV_FRAMEWORK_TARGET = "DIRICHLET_UNITY_FRAMEWORK_TARGET"; + private const string ENV_APP_TARGET = "DIRICHLET_UNITY_APP_TARGET"; + + /// + /// 解析出的 target 信息 + /// + private class TargetInfo + { + public string FrameworkTargetName { get; set; } + public string AppTargetName { get; set; } + public string FrameworkTargetGuid { get; set; } + public string AppTargetGuid { get; set; } + } + + [PostProcessBuild(100)] + public static void OnPostprocessBuild(BuildTarget buildTarget, string pathToBuiltProject) + { + if (buildTarget != BuildTarget.iOS) + { + return; + } + + Debug.Log("[DirichletMediation] Starting iOS post-process..."); + + try + { + // 确保默认值:iOS 适配器默认勾选(避免历史版本的 EditorPrefs 默认值导致“默认不勾选”) + EnsureDefaultAdapterPrefs(); + + // 0. 解析 target 信息(核心改动:不做猜测,直接从 pbxproj 读取) + var targetInfo = ResolveTargetInfo(pathToBuiltProject); + + // 1. Generate Podfile dynamically based on adapter settings and resolved targets + GeneratePodfile(pathToBuiltProject, targetInfo); + + // 2. Modify Xcode project settings (before pod install) + ModifyXcodeProject(pathToBuiltProject, targetInfo); + + // 3. Modify Info.plist for required permissions + ModifyInfoPlist(pathToBuiltProject); + + // 4. Run pod install + RunPodInstall(pathToBuiltProject); + + // 5. Embed GDT dynamic frameworks into app target (must run after pod install) + EmbedGDTDynamicFrameworks(pathToBuiltProject, targetInfo); + + Debug.Log("[DirichletMediation] iOS post-process completed successfully."); + } + catch (System.Exception ex) + { + Debug.LogError($"[DirichletMediation] iOS post-process failed: {ex.Message}"); + Debug.LogError($"[DirichletMediation] Stack trace: {ex.StackTrace}"); + throw; // 重新抛出,让 Unity 构建失败(不静默跳过) + } + } + + /// + /// 从 pbxproj 解析出 framework target 和 app target 的真实名称 + /// 不做猜测,解析失败时直接中断 + /// + private static TargetInfo ResolveTargetInfo(string projectPath) + { + Debug.Log("[DirichletMediation] Resolving target info from pbxproj..."); + + // 检查环境变量 override + var envFrameworkTarget = System.Environment.GetEnvironmentVariable(ENV_FRAMEWORK_TARGET); + var envAppTarget = System.Environment.GetEnvironmentVariable(ENV_APP_TARGET); + + if (!string.IsNullOrEmpty(envFrameworkTarget) && !string.IsNullOrEmpty(envAppTarget)) + { + Debug.Log($"[DirichletMediation] Using targets from environment variables:"); + Debug.Log($" Framework target: {envFrameworkTarget}"); + Debug.Log($" App target: {envAppTarget}"); + return new TargetInfo + { + FrameworkTargetName = envFrameworkTarget, + AppTargetName = envAppTarget, + // GUID 在需要时从 pbxproj 反查 + }; + } + + // 查找 .xcodeproj 文件 + var xcodeProjectName = DetectXcodeProjectName(projectPath); + var projectFilePath = Path.Combine(projectPath, $"{xcodeProjectName}.xcodeproj/project.pbxproj"); + + if (!File.Exists(projectFilePath)) + { + throw new System.Exception($"project.pbxproj not found at: {projectFilePath}"); + } + + var pbxProject = new PBXProject(); + pbxProject.ReadFromFile(projectFilePath); + +#if UNITY_2019_3_OR_NEWER + var frameworkGuid = pbxProject.GetUnityFrameworkTargetGuid(); + var appGuid = pbxProject.GetUnityMainTargetGuid(); +#else + // Unity 2019.3 之前只有单一 target + var frameworkGuid = pbxProject.TargetGuidByName("Unity-iPhone"); + var appGuid = frameworkGuid; +#endif + + // 直接从 pbxproj 解析 GUID -> name 映射(最稳健,避免 TargetGuidByName 行为差异) + var nativeTargetMap = GetNativeTargetGuidToNameMap(projectFilePath); + + if (string.IsNullOrEmpty(frameworkGuid) || string.IsNullOrEmpty(appGuid)) + { + var allTargets = nativeTargetMap.Values.Distinct().ToArray(); + var targetList = allTargets.Any() ? string.Join(", ", allTargets) : "(none)"; + throw new System.Exception( + $"Unity PBXProject API returned empty target GUID.\n" + + $" .xcodeproj: {xcodeProjectName}\n" + + $" Available targets: {targetList}\n" + + $" Framework GUID: {frameworkGuid ?? "(null)"}\n" + + $" App GUID: {appGuid ?? "(null)"}\n\n" + + $"解决方案:\n" + + $" 1. 升级 Unity 到 2019.3+(或团结引擎对应版本),确保导出包含 UnityFramework/App 双 target\n" + + $" 2. 或使用环境变量强制指定 target:\n" + + $" export {ENV_FRAMEWORK_TARGET}=YourFrameworkTarget\n" + + $" export {ENV_APP_TARGET}=YourAppTarget" + ); + } + + // 从 GUID 反查 target name + nativeTargetMap.TryGetValue(frameworkGuid, out var frameworkTargetName); + nativeTargetMap.TryGetValue(appGuid, out var appTargetName); + + // 兜底:某些 Unity 版本可能返回非 PBXNativeTarget GUID,这时再尝试旧逻辑反查 + frameworkTargetName = frameworkTargetName ?? GetTargetNameByGuid(pbxProject, frameworkGuid, projectFilePath); + appTargetName = appTargetName ?? GetTargetNameByGuid(pbxProject, appGuid, projectFilePath); + + // 验证解析结果 + if (string.IsNullOrEmpty(frameworkTargetName) || string.IsNullOrEmpty(appTargetName)) + { + // 输出所有 targets 帮助诊断 + var allTargets = nativeTargetMap.Values.Distinct().ToArray(); + var targetList = allTargets.Any() ? string.Join(", ", allTargets) : "(none)"; + + throw new System.Exception( + $"Failed to resolve target names from pbxproj.\n" + + $" .xcodeproj: {xcodeProjectName}\n" + + $" Available targets: {targetList}\n" + + $" Framework GUID: {frameworkGuid ?? "(null)"}\n" + + $" App GUID: {appGuid ?? "(null)"}\n\n" + + $"解决方案:\n" + + $" 1. 设置环境变量强制指定 target:\n" + + $" export {ENV_FRAMEWORK_TARGET}=YourFrameworkTarget\n" + + $" export {ENV_APP_TARGET}=YourAppTarget\n" + + $" 2. 检查导出工程是否包含 UnityFramework 和 Unity-iPhone(或对应的团结引擎 target)" + ); + } + + Debug.Log($"[DirichletMediation] Resolved targets:"); + Debug.Log($" Framework target: {frameworkTargetName} (GUID: {frameworkGuid})"); + Debug.Log($" App target: {appTargetName} (GUID: {appGuid})"); + + return new TargetInfo + { + FrameworkTargetName = frameworkTargetName, + AppTargetName = appTargetName, + FrameworkTargetGuid = frameworkGuid, + AppTargetGuid = appGuid + }; + } + + /// + /// 从 GUID 反查 target name + /// Unity PBXProject API 没有直接提供此方法,需要通过 TargetGuidByName 反向验证 + /// + private static string GetTargetNameByGuid(PBXProject pbxProject, string targetGuid, string projectFilePath) + { + if (string.IsNullOrEmpty(targetGuid)) + { + return null; + } + + // 最可靠的方式:直接从 pbxproj 解析 GUID -> name + var nativeTargetMap = GetNativeTargetGuidToNameMap(projectFilePath); + if (nativeTargetMap.TryGetValue(targetGuid, out var parsedName) && !string.IsNullOrEmpty(parsedName)) + { + return parsedName; + } + + // 常见的 Unity/Tuanjie target 名称 + var commonTargetNames = new[] + { + "UnityFramework", "Unity-iPhone", + "TuanjieFramework", "Tuanjie-iPhone", + "GameAssembly", // 部分定制工程 + }; + + foreach (var name in commonTargetNames) + { + try + { + var guid = pbxProject.TargetGuidByName(name); + if (guid == targetGuid) + { + return name; + } + } + catch + { + // Target 不存在,继续尝试下一个 + } + } + + // 如果常见名称都不匹配,尝试从文件中解析所有 target + var allTargets = GetAllTargetNames(projectFilePath); + foreach (var name in allTargets) + { + try + { + var guid = pbxProject.TargetGuidByName(name); + if (guid == targetGuid) + { + return name; + } + } + catch + { + // 继续尝试 + } + } + + return null; + } + + /// + /// 从 project.pbxproj 解析 PBXNativeTarget 的 GUID -> name 映射 + /// + private static System.Collections.Generic.Dictionary GetNativeTargetGuidToNameMap(string projectFilePath) + { + var result = new System.Collections.Generic.Dictionary(System.StringComparer.OrdinalIgnoreCase); + + try + { + // 形如: + // 9D25AB9C213FB47800354C27 /* UnityFramework */ = { + var entryStartRegex = new System.Text.RegularExpressions.Regex( + @"^\s*([0-9A-Fa-f]{24})\s*/\*\s*(.*?)\s*\*/\s*=\s*\{\s*$", + System.Text.RegularExpressions.RegexOptions.CultureInvariant + ); + + string currentGuid = null; + string currentName = null; + var braceDepth = 0; + var isNativeTarget = false; + + foreach (var line in File.ReadLines(projectFilePath)) + { + if (braceDepth == 0) + { + var m = entryStartRegex.Match(line); + if (!m.Success) + { + continue; + } + + currentGuid = m.Groups[1].Value.Trim(); + currentName = m.Groups[2].Value.Trim(); + braceDepth = CountBraceDelta(line); // start line includes '{' + isNativeTarget = false; + continue; + } + + if (!isNativeTarget && line.IndexOf("isa = PBXNativeTarget;", System.StringComparison.Ordinal) >= 0) + { + isNativeTarget = true; + } + + braceDepth += CountBraceDelta(line); + + if (braceDepth <= 0) + { + if (isNativeTarget && !string.IsNullOrEmpty(currentGuid) && !string.IsNullOrEmpty(currentName)) + { + result[currentGuid] = currentName; + } + + currentGuid = null; + currentName = null; + braceDepth = 0; + isNativeTarget = false; + } + } + } + catch + { + // ignore + } + + return result; + } + + private static int CountBraceDelta(string line) + { + if (string.IsNullOrEmpty(line)) + { + return 0; + } + + var delta = 0; + foreach (var c in line) + { + if (c == '{') delta++; + else if (c == '}') delta--; + } + return delta; + } + + /// + /// 从 pbxproj 文件解析所有 target 名称(用于诊断输出) + /// + private static string[] GetAllTargetNames(string projectFilePath) + { + var map = GetNativeTargetGuidToNameMap(projectFilePath); + return map.Values.Distinct().ToArray(); + } + + /// + /// 生成 Podfile + /// 所有 pods 统一放到 Framework target(UnityFramework),因为: + /// - Bridge (.mm) 和 DirichletMediationSDK 都编译/运行在 Framework target 中 + /// - SDK 通过 NSClassFromString 动态查找 Adapter 类,Adapter 必须与 SDK 在同一 target + /// 否则静态库符号可能被 strip 或不在同一二进制中,导致运行时找不到类 + /// + private static void GeneratePodfile(string projectPath, TargetInfo targetInfo) + { + // Read adapter settings from EditorPrefs + // Note: DirichletAdSDK (DRA adapter) is always enabled as core SDK + var enableCsj = EditorPrefs.GetBool("Dirichlet.iOS.EnableCSJ", true); + var enableGdt = EditorPrefs.GetBool("Dirichlet.iOS.EnableGDT", true); + + var podfileContent = new StringBuilder(); + podfileContent.AppendLine("# Generated by Dirichlet Mediation Unity Plugin"); + podfileContent.AppendLine("#"); + podfileContent.AppendLine($"# Framework target: {targetInfo.FrameworkTargetName}"); + podfileContent.AppendLine($"# App target: {targetInfo.AppTargetName}"); + podfileContent.AppendLine(); + podfileContent.AppendLine("source 'https://cdn.cocoapods.org/'"); + podfileContent.AppendLine(); + podfileContent.AppendLine($"platform :ios, '{MinIOSVersion}'"); + podfileContent.AppendLine("use_frameworks! :linkage => :static"); + podfileContent.AppendLine(); + + // 所有 pods 统一放到 Framework target + // SDK 通过 NSClassFromString 查找 Adapter 类,必须在同一 target 中 + podfileContent.AppendLine($"target '{targetInfo.FrameworkTargetName}' do"); + podfileContent.AppendLine($" pod 'DirichletMediationSDK', '{SDKVersion}'"); + + if (enableCsj) + { + podfileContent.AppendLine($" pod 'DirichletMediationAdapterCSJ', '{SDKVersion}'"); + } + + if (enableGdt) + { + podfileContent.AppendLine($" pod 'DirichletMediationAdapterGDT', '{SDKVersion}'"); + } + + // DirichletAdSDK (DRA adapter) is always included as core SDK + podfileContent.AppendLine($" pod 'DirichletMediationAdapterDRA', '{SDKVersion}'"); + + podfileContent.AppendLine("end"); + podfileContent.AppendLine(); + + // Post-install: 基础构建设置 + podfileContent.AppendLine("post_install do |installer|"); + podfileContent.AppendLine(" installer.pods_project.targets.each do |target|"); + podfileContent.AppendLine(" target.build_configurations.each do |config|"); + podfileContent.AppendLine($" config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '{MinIOSVersion}'"); + podfileContent.AppendLine(" config.build_settings['ENABLE_BITCODE'] = 'NO'"); + podfileContent.AppendLine(" end"); + podfileContent.AppendLine(" end"); + podfileContent.AppendLine("end"); + + var podfilePath = Path.Combine(projectPath, "Podfile"); + File.WriteAllText(podfilePath, podfileContent.ToString()); + + Debug.Log($"[DirichletMediation] Generated Podfile at {podfilePath}"); + Debug.Log($"[DirichletMediation] All pods allocated to {targetInfo.FrameworkTargetName} (CSJ={enableCsj}, GDT={enableGdt}, DRA=always)"); + } + + /// + /// 检测 Xcode 项目名称,兼容 Unity/Tuanjie/自定义项目 + /// 动态搜索目录下的 .xcodeproj 文件 + /// + private static string DetectXcodeProjectName(string projectPath) + { + // 搜索所有 .xcodeproj 目录 + var xcodeprojDirs = Directory.GetDirectories(projectPath, "*.xcodeproj"); + + if (xcodeprojDirs.Length == 0) + { + throw new System.Exception( + $"No .xcodeproj found in: {projectPath}\n" + + "请确认 Unity 导出路径正确,且导出已完成。" + ); + } + + // 获取第一个 xcodeproj 的名称(不含扩展名) + var xcodeprojPath = xcodeprojDirs[0]; + var xcodeprojName = Path.GetFileNameWithoutExtension(xcodeprojPath); + + Debug.Log($"[DirichletMediation] Detected Xcode project: {xcodeprojName}"); + return xcodeprojName; + } + + /// + /// 修改 Xcode 工程设置(使用已解析的 target 信息) + /// + private static void ModifyXcodeProject(string projectPath, TargetInfo targetInfo) + { + var xcodeProjectName = DetectXcodeProjectName(projectPath); + var projectFilePath = Path.Combine(projectPath, $"{xcodeProjectName}.xcodeproj/project.pbxproj"); + var pbxProject = new PBXProject(); + pbxProject.ReadFromFile(projectFilePath); + + // 使用已解析的 target GUID,或通过名称反查 + var targetGuid = targetInfo.FrameworkTargetGuid; + var mainTargetGuid = targetInfo.AppTargetGuid; + + if (string.IsNullOrEmpty(targetGuid)) + { + targetGuid = pbxProject.TargetGuidByName(targetInfo.FrameworkTargetName); + } + if (string.IsNullOrEmpty(mainTargetGuid)) + { + mainTargetGuid = pbxProject.TargetGuidByName(targetInfo.AppTargetName); + } + + Debug.Log($"[DirichletMediation] Modifying Xcode project:"); + Debug.Log($" Framework target: {targetInfo.FrameworkTargetName} (GUID: {targetGuid})"); + Debug.Log($" App target: {targetInfo.AppTargetName} (GUID: {mainTargetGuid})"); + + // NOTE: System frameworks (AdSupport, AVFoundation, WebKit, CoreVideo, etc.) + // are declared in SDK podspecs and will be automatically linked by CocoaPods. + // No need to manually add them here. + // - DirichletAdSDK.podspec: AdSupport, SystemConfiguration, Security + // - DirichletCoreSDK.podspec: SystemConfiguration, Security + // - DirichletMediationAdapterCSJ.podspec: CoreVideo + // - Third-party SDKs (Ads-CN, GDTMobSDK) declare their own framework dependencies. + + // Set build settings for framework target + pbxProject.SetBuildProperty(targetGuid, "ENABLE_BITCODE", "NO"); + pbxProject.SetBuildProperty(targetGuid, "CLANG_ENABLE_MODULES", "YES"); + + // Set build settings for main target + pbxProject.SetBuildProperty(mainTargetGuid, "ENABLE_BITCODE", "NO"); + pbxProject.SetBuildProperty(mainTargetGuid, "CLANG_ENABLE_MODULES", "YES"); + pbxProject.SetBuildProperty(mainTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES"); + + // Set LD_RUNPATH_SEARCH_PATHS to allow dynamic frameworks to be found at runtime + pbxProject.SetBuildProperty(mainTargetGuid, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks"); + pbxProject.SetBuildProperty(targetGuid, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"); + + pbxProject.WriteToFile(projectFilePath); + Debug.Log("[DirichletMediation] Modified Xcode project settings"); + } + + /// + /// GDTMobSDK 提供的是预编译动态库(GDTMobSDK.framework, Tquic.framework), + /// 必须 embed 到 app bundle 中才能在运行时加载。 + /// 由于所有 pods 都在 Framework target 上,CocoaPods 不会自动 embed 到 app target, + /// 需要在 pod install 之后手动处理。 + /// 依赖 UnityEditor.iOS.Xcode.Extensions 中的 AddFileToEmbedFrameworks 扩展方法。 + /// + private static void EmbedGDTDynamicFrameworks(string projectPath, TargetInfo targetInfo) + { + var enableGdt = EditorPrefs.GetBool("Dirichlet.iOS.EnableGDT", true); + if (!enableGdt) + { + Debug.Log("[DirichletMediation] GDT adapter disabled, skipping dynamic framework embedding"); + return; + } + + var xcodeProjectName = DetectXcodeProjectName(projectPath); + var projectFilePath = Path.Combine(projectPath, $"{xcodeProjectName}.xcodeproj/project.pbxproj"); + var pbxProject = new PBXProject(); + pbxProject.ReadFromFile(projectFilePath); + + var mainTargetGuid = targetInfo.AppTargetGuid; + if (string.IsNullOrEmpty(mainTargetGuid)) + { + mainTargetGuid = pbxProject.TargetGuidByName(targetInfo.AppTargetName); + } + + // GDTMobSDK 的动态框架列表 + var dynamicFrameworkNames = new[] { "GDTMobSDK.framework", "Tquic.framework" }; + var podsDir = Path.Combine(projectPath, "Pods"); + var embedded = 0; + + foreach (var frameworkName in dynamicFrameworkNames) + { + var frameworkPath = FindDynamicFramework(podsDir, frameworkName); + if (string.IsNullOrEmpty(frameworkPath)) + { + Debug.LogWarning($"[DirichletMediation] Dynamic framework not found: {frameworkName}"); + continue; + } + + var relativePath = frameworkPath.StartsWith(projectPath) + ? frameworkPath.Substring(projectPath.Length + 1) + : frameworkPath; + + var fileGuid = pbxProject.AddFile(relativePath, "Frameworks/" + frameworkName); + PBXProjectExtensions.AddFileToEmbedFrameworks(pbxProject, mainTargetGuid, fileGuid); + embedded++; + Debug.Log($"[DirichletMediation] Embedded dynamic framework: {frameworkName}"); + } + + if (embedded > 0) + { + pbxProject.WriteToFile(projectFilePath); + Debug.Log($"[DirichletMediation] Embedded {embedded} GDT dynamic frameworks into {targetInfo.AppTargetName}"); + } + } + + /// + /// 在 Pods 目录中递归搜索指定的 .framework(优先 ios-arm64 真机 slice) + /// + private static string FindDynamicFramework(string podsDir, string frameworkName) + { + if (!Directory.Exists(podsDir)) + { + return null; + } + + var candidates = Directory.GetDirectories(podsDir, frameworkName, SearchOption.AllDirectories); + + foreach (var candidate in candidates) + { + if (candidate.Contains("ios-arm64") && !candidate.Contains("simulator")) + { + return candidate; + } + } + + foreach (var candidate in candidates) + { + if (!candidate.Contains("simulator")) + { + return candidate; + } + } + + return candidates.Length > 0 ? candidates[0] : null; + } + + private static void ModifyInfoPlist(string projectPath) + { + var plistPath = Path.Combine(projectPath, "Info.plist"); + var plist = new PlistDocument(); + plist.ReadFromFile(plistPath); + + var rootDict = plist.root; + + // Note: The following Info.plist keys should be configured by the developer manually + // to avoid potential App Store review issues: + // - NSAppTransportSecurity: Configure based on your app's network requirements + // - NSUserTrackingUsageDescription: Required for iOS 14+ IDFA access, use your custom description + // - NSLocationWhenInUseUsageDescription: Only add if your app uses location services + // + // Reference: https://ssp.dirichlet.cn/docs/dirichlet-mediation-sdk/dirichlet-mediation-sdk-guide-ios/ + + // Add SKAdNetwork identifiers for attribution tracking + AddSKAdNetworkIds(rootDict); + + plist.WriteToFile(plistPath); + Debug.Log("[DirichletMediation] Modified Info.plist (SKAdNetwork IDs only)"); + } + + private static void AddSKAdNetworkIds(PlistElementDict rootDict) + { + if (rootDict.values.ContainsKey("SKAdNetworkItems")) + { + Debug.Log("[DirichletMediation] SKAdNetworkItems already exists, skipping"); + return; + } + + // Add SKAdNetwork IDs required by CSJ (Pangle/穿山甲) + // Reference: https://www.csjplatform.com/supportcenter/5377 + var skAdNetworkArray = rootDict.CreateArray("SKAdNetworkItems"); + + var commonSkAdNetworkIds = new[] + { + "238da6jt44.skadnetwork", // 穿山甲 SKAdNetwork ID + "x2jnk7ly8j.skadnetwork", // 穿山甲 SKAdNetwork ID + "22mmun2rn5.skadnetwork" // 穿山甲 SKAdNetwork ID + }; + + foreach (var skAdNetworkId in commonSkAdNetworkIds) + { + var dict = skAdNetworkArray.AddDict(); + dict.SetString("SKAdNetworkIdentifier", skAdNetworkId); + } + + Debug.Log($"[DirichletMediation] Added {commonSkAdNetworkIds.Length} SKAdNetwork IDs to Info.plist"); + } + + private static void RunPodInstall(string projectPath) + { + var podfilePath = Path.Combine(projectPath, "Podfile"); + if (!File.Exists(podfilePath)) + { + Debug.LogWarning("[DirichletMediation] Podfile not found, skipping pod install"); + return; + } + + Debug.Log("[DirichletMediation] Running 'pod install'..."); + Debug.Log("[DirichletMediation] Note: This may take a few minutes on first build or when adapters change."); + + try + { + // Try to find pod executable + var podPath = FindPodExecutable(); + if (string.IsNullOrEmpty(podPath)) + { + Debug.LogWarning("[DirichletMediation] CocoaPods not found. Please install CocoaPods:"); + Debug.LogWarning(" sudo gem install cocoapods"); + Debug.LogWarning("Then run 'pod install' manually in:"); + Debug.LogWarning($" {projectPath}"); + return; + } + + // 优先不做 repo update(更快、更稳定);如遇到“找不到 podspec”再退化为 --repo-update + var firstArgs = "install"; + var exitCode = RunPodCommand(podPath, firstArgs, projectPath, out var output, out var error); + + if (exitCode != 0 && ShouldRetryWithRepoUpdate(output, error)) + { + Debug.LogWarning("[DirichletMediation] pod install failed (missing specs suspected). Retrying with --repo-update..."); + var retryArgs = "install --repo-update"; + exitCode = RunPodCommand(podPath, retryArgs, projectPath, out output, out error); + } + + if (exitCode == 0) + { + Debug.Log("[DirichletMediation] pod install completed successfully"); + if (!string.IsNullOrEmpty(output) && output.Length < 2000) + { + Debug.Log($"Output:\n{output}"); + } + return; + } + + Debug.LogError($"[DirichletMediation] pod install failed with exit code {exitCode}"); + if (!string.IsNullOrEmpty(error)) + { + Debug.LogError($"Error:\n{error}"); + } + if (!string.IsNullOrEmpty(output)) + { + Debug.LogError($"Output:\n{output}"); + } + + if (IsCocoaPodsCdnHttp2Error(output, error)) + { + Debug.LogWarning( + "[DirichletMediation] Detected CocoaPods CDN HTTP/2 error (e.g. 'Error in the HTTP2 framing layer'). " + + "This is usually a network/proxy/SSL issue. Try rerun without repo update, switch network, upgrade CocoaPods, " + + "or run 'pod install' manually. (Repo doc: docs/FAQ/CocoaPods-SSL证书问题排查SOP.md)" + ); + } + + // 失败即中断:避免导出工程处于“半配置”状态,后续 Xcode build 更难排查 + throw new System.Exception($"pod install failed with exit code {exitCode}"); + } + catch (System.Exception ex) + { + Debug.LogError($"[DirichletMediation] Failed to run pod install: {ex.Message}"); + Debug.LogWarning("[DirichletMediation] Please run 'pod install' manually in the Xcode project directory:"); + Debug.LogWarning($" cd {projectPath}"); + Debug.LogWarning(" pod install"); + throw; + } + } + + private static int RunPodCommand(string podPath, string arguments, string projectPath, out string output, out string error) + { + var processInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = podPath, + Arguments = arguments, + WorkingDirectory = projectPath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Set UTF-8 encoding to avoid CocoaPods encoding issues + processInfo.EnvironmentVariables["LANG"] = "en_US.UTF-8"; + processInfo.EnvironmentVariables["LC_ALL"] = "en_US.UTF-8"; + + using (var process = System.Diagnostics.Process.Start(processInfo)) + { + output = process.StandardOutput.ReadToEnd(); + error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + return process.ExitCode; + } + } + + private static bool ShouldRetryWithRepoUpdate(string output, string error) + { + var combined = (output ?? string.Empty) + "\n" + (error ?? string.Empty); + + // 常见“本地 specs 未更新导致无法解析依赖”的报错关键字 + return combined.Contains("Unable to find a specification for") + || combined.Contains("None of your spec sources contain a spec satisfying") + || combined.Contains("spec satisfying") + || combined.Contains("No podspec found for") + || combined.Contains("could not find compatible versions for pod"); + } + + private static bool IsCocoaPodsCdnHttp2Error(string output, string error) + { + var combined = (output ?? string.Empty) + "\n" + (error ?? string.Empty); + return combined.Contains("CDN: trunk Repo update failed") + || combined.Contains("Error in the HTTP2 framing layer") + || combined.Contains("URL couldn't be downloaded: https://cdn.cocoapods.org/"); + } + + private static string FindPodExecutable() + { + // 1. POD_BINARY 环境变量优先,方便 CI 或接入方手动配置 + var envValue = System.Environment.GetEnvironmentVariable("POD_BINARY"); + if (!string.IsNullOrEmpty(envValue)) + { + var expanded = envValue.Trim(); + if (File.Exists(expanded)) + { + Debug.Log($"[DirichletMediation] Found pod via POD_BINARY: {expanded}"); + return expanded; + } + + Debug.LogWarning($"[DirichletMediation] POD_BINARY points to non-existing path: {expanded}"); + } + + var possiblePaths = new[] + { + "/usr/local/bin/pod", + "/opt/homebrew/bin/pod", + "/usr/bin/pod", + Path.Combine(System.Environment.GetEnvironmentVariable("HOME") ?? "", ".rbenv/shims/pod"), + Path.Combine(System.Environment.GetEnvironmentVariable("HOME") ?? "", ".rvm/wrappers/default/pod") + }; + + foreach (var path in possiblePaths) + { + if (File.Exists(path)) + { + Debug.Log($"[DirichletMediation] Found pod at: {path}"); + return path; + } + } + + // Try using 'which pod' + try + { + var processInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "/usr/bin/env", + Arguments = "which pod", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using (var process = System.Diagnostics.Process.Start(processInfo)) + { + var output = process.StandardOutput.ReadToEnd().Trim(); + var error = process.StandardError.ReadToEnd().Trim(); + process.WaitForExit(); + + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) + { + Debug.Log($"[DirichletMediation] Found pod via 'which': {output}"); + return output; + } + + if (!string.IsNullOrEmpty(error)) + { + Debug.LogWarning($"[DirichletMediation] 'which pod' failed: {error}"); + } + } + } + catch (System.Exception ex) + { + Debug.LogWarning($"[DirichletMediation] Failed to resolve pod via 'which': {ex.Message}"); + } + + return null; + } + + private const string PrefKeyDefaultsInitialized = "Dirichlet.Mediation.AdapterSettings.DefaultsInitialized.v1"; + + private static void EnsureDefaultAdapterPrefs() + { + if (EditorPrefs.GetBool(PrefKeyDefaultsInitialized, false)) + { + return; + } + + // 默认“勾上”:CSJ/GDT(iOS) + // 注意:如接入方需要关闭,可在 Adapter Settings 窗口中手动取消勾选。 + EditorPrefs.SetBool("Dirichlet.iOS.EnableCSJ", true); + EditorPrefs.SetBool("Dirichlet.iOS.EnableGDT", true); + + // 同时保证 Android 默认一致(无副作用) + EditorPrefs.SetBool("Dirichlet.Android.EnableCSJ", true); + EditorPrefs.SetBool("Dirichlet.Android.EnableGDT", true); + + EditorPrefs.SetBool(PrefKeyDefaultsInitialized, true); + } + } +} + +#endif diff --git a/Assets/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs.meta b/Assets/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs.meta new file mode 100644 index 0000000..0da8425 --- /dev/null +++ b/Assets/DirichletMediation/Editor/DirichletMediationIOSPostProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5bc3db08a1b434ef2bc8aba5413e588c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DirichletMediation/Runtime.meta b/Assets/DirichletMediation/Runtime.meta new file mode 100644 index 0000000..fe8311c --- /dev/null +++ b/Assets/DirichletMediation/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 33121b74910d6437b86b802fedfe5af4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef b/Assets/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef new file mode 100644 index 0000000..91d5894 --- /dev/null +++ b/Assets/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef @@ -0,0 +1,15 @@ +{ + "name": "Dirichlet.Mediation.Runtime", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} + + diff --git a/Assets/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef.meta b/Assets/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef.meta new file mode 100644 index 0000000..dcf1729 --- /dev/null +++ b/Assets/DirichletMediation/Runtime/Dirichlet.Mediation.Runtime.asmdef.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 556788bfca0f498d945d7fa53061b066 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: + + diff --git a/Assets/DirichletMediation/Runtime/DirichletAdTypes.cs b/Assets/DirichletMediation/Runtime/DirichletAdTypes.cs new file mode 100644 index 0000000..7dbfcd4 --- /dev/null +++ b/Assets/DirichletMediation/Runtime/DirichletAdTypes.cs @@ -0,0 +1,1376 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using UnityEngine; + +namespace Dirichlet.Mediation +{ + /// + /// Platform handle for ad instances. This is a simple wrapper around the handle string + /// returned from native bridge, matching the native SDK pattern where ad objects are + /// returned directly from loadXXXAd() methods. + /// + public sealed class DirichletPlatformAdHandle + { + public string DebugId { get; } + + internal DirichletPlatformAdHandle(string debugId) + { + DebugId = debugId ?? throw new ArgumentNullException(nameof(debugId)); + } + + internal static DirichletPlatformAdHandle FromNative(string handleId) + { + if (string.IsNullOrEmpty(handleId)) + { + handleId = Guid.NewGuid().ToString("N"); + } + return new DirichletPlatformAdHandle(handleId); + } + + internal static DirichletPlatformAdHandle CreateStub() + { + return new DirichletPlatformAdHandle(Guid.NewGuid().ToString("N")); + } + + public override string ToString() => $"DirichletPlatformAdHandle(DebugId={DebugId})"; + } + + public enum DirichletAdType + { + RewardVideo = 0, + Interstitial = 1, + Banner = 2, + Splash = 3, + ExpressFeed = 4, + NativeFeed = 5 + } + + /// + /// Mirrors the Android-side DirichletAdManager to keep configuration state and create ad natives. + /// + public static class DirichletAdManager + { + private static readonly object StateLock = new object(); + private static DirichletAdConfig currentConfig; + + /// + /// Returns the last configuration applied to the native SDK. + /// + public static DirichletAdConfig CurrentConfig + { + get + { + lock (StateLock) + { + return currentConfig; + } + } + } + + internal static void ApplyConfig(DirichletAdConfig config) + { + lock (StateLock) + { + currentConfig = config; + } + } + + internal static void Clear() + { + lock (StateLock) + { + currentConfig = null; + } + } + + /// + /// Creates a Unity-side DirichletAdNative wrapper that mirrors the Android aggregator API. + /// + public static DirichletAdNative CreateAdNative() + { + return new DirichletAdNative(DirichletSdk.GetBridge()); + } + + internal static DirichletAdNative CreateAdNative(IDirichletPlatformBridge bridge) + { + return new DirichletAdNative(bridge); + } + } + + /// + /// Thin wrapper around native ad handles provided by the platform bridge. + /// + + public sealed class DirichletAdRequest + { + public long SpaceId { get; } + public string Extra1 { get; } + public string UserId { get; } + public string RewardName { get; } + public int? RewardAmount { get; } + public string Query { get; } + public int? ExpressViewWidth { get; } + public int? ExpressViewHeight { get; } + public int? ExpressImageWidth { get; } + public int? ExpressImageHeight { get; } + public long? MinaId { get; } + + /// + /// Banner auto-ad slide rotation interval, in seconds. Native default is 30. + /// + public int? SlideInterval { get; } + + private DirichletAdRequest(Builder builder) + { + SpaceId = builder.spaceId; + Extra1 = builder.extra1; + UserId = builder.userId; + RewardName = builder.rewardName; + RewardAmount = builder.rewardAmount; + Query = builder.query; + ExpressViewWidth = builder.expressViewWidth; + ExpressViewHeight = builder.expressViewHeight; + ExpressImageWidth = builder.expressImageWidth; + ExpressImageHeight = builder.expressImageHeight; + MinaId = builder.minaId; + SlideInterval = builder.slideInterval; + } + + public Builder ToBuilder() + { + var builder = new Builder() + .WithSpaceId(SpaceId) + .WithExtra1(Extra1) + .WithUserId(UserId) + .WithRewardName(RewardName) + .WithQuery(Query); + + if (RewardAmount.HasValue) + { + builder.WithRewardAmount(RewardAmount.Value); + } + + if (ExpressViewWidth.HasValue || ExpressViewHeight.HasValue) + { + builder.WithExpressViewSize(ExpressViewWidth ?? -1, ExpressViewHeight ?? -1); + } + + if (ExpressImageWidth.HasValue || ExpressImageHeight.HasValue) + { + builder.WithExpressImageSize(ExpressImageWidth ?? -1, ExpressImageHeight ?? -1); + } + + if (MinaId.HasValue) + { + builder.WithMinaId(MinaId.Value); + } + + if (SlideInterval.HasValue) + { + builder.WithSlideInterval(SlideInterval.Value); + } + + return builder; + } + + internal Dictionary ToBridgePayload() + { + var payload = new Dictionary(StringComparer.Ordinal) + { + ["space_id"] = SpaceId + }; + + if (!string.IsNullOrEmpty(Extra1)) + { + payload["extra1"] = Extra1; + } + + if (!string.IsNullOrEmpty(UserId)) + { + payload["user_id"] = UserId; + } + + if (!string.IsNullOrEmpty(RewardName)) + { + payload["reward_name"] = RewardName; + } + + if (RewardAmount.HasValue) + { + payload["reward_amount"] = RewardAmount.Value; + } + + if (!string.IsNullOrEmpty(Query)) + { + payload["query"] = Query; + } + + if (ExpressViewWidth.HasValue) + { + payload["express_width"] = ExpressViewWidth.Value; + } + + if (ExpressViewHeight.HasValue) + { + payload["express_height"] = ExpressViewHeight.Value; + } + + if (ExpressImageWidth.HasValue) + { + payload["express_image_width"] = ExpressImageWidth.Value; + } + + if (ExpressImageHeight.HasValue) + { + payload["express_image_height"] = ExpressImageHeight.Value; + } + + if (MinaId.HasValue) + { + payload["mina_id"] = MinaId.Value; + } + + if (SlideInterval.HasValue) + { + // Native Java API is `withSlideInternal` (spelled "Internal" by upstream); + // keep the wire key matching the native method name. + payload["slide_internal"] = SlideInterval.Value; + } + + return payload; + } + + public sealed class Builder + { + internal long spaceId; + internal string extra1; + internal string userId; + internal string rewardName; + internal int? rewardAmount; + internal string query; + internal int? expressViewWidth; + internal int? expressViewHeight; + internal int? expressImageWidth; + internal int? expressImageHeight; + internal long? minaId; + internal int? slideInterval; + + public Builder WithSpaceId(long value) + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "SpaceId must be greater than zero."); + } + + spaceId = value; + return this; + } + + public Builder WithExtra1(string value) + { + extra1 = value; + return this; + } + + public Builder WithUserId(string value) + { + userId = value; + return this; + } + + public Builder WithRewardName(string value) + { + rewardName = value; + return this; + } + + public Builder WithRewardAmount(int value) + { + rewardAmount = value; + return this; + } + + public Builder WithQuery(string value) + { + query = value; + return this; + } + + public Builder WithExpressViewSize(int width, int height) + { + expressViewWidth = width; + expressViewHeight = height; + return this; + } + + public Builder WithExpressImageSize(int width, int height) + { + expressImageWidth = width; + expressImageHeight = height; + return this; + } + + public Builder WithMinaId(long value) + { + minaId = value; + return this; + } + + /// + /// Sets banner auto-ad slide rotation interval, in seconds (recommended 30–120, native default 30). + /// + public Builder WithSlideInterval(int seconds) + { + slideInterval = seconds; + return this; + } + + public DirichletAdRequest Build() + { + if (spaceId <= 0) + { + throw new InvalidOperationException("SpaceId must be set to a value greater than zero before building the request."); + } + + return new DirichletAdRequest(this); + } + } + } + + public enum DirichletBannerAlignment + { + Top = 0, + Bottom = 1 + } + + public sealed class DirichletAdShowOptions + { + public DirichletBannerAlignment BannerAlignment { get; set; } = DirichletBannerAlignment.Bottom; + public int BannerOffset { get; set; } + + internal Dictionary ToBridgePayload() + { + var payload = new Dictionary(StringComparer.Ordinal); + + // Banner options are always included if set, Bridge will handle based on ad type + payload["banner_baseline"] = (int)BannerAlignment; + payload["banner_offset"] = BannerOffset; + + return payload; + } + } + + public sealed class DirichletAdNative + { + private readonly IDirichletPlatformBridge bridge; + private static readonly object RewardAutoSessionLock = new object(); + private static readonly Dictionary RewardAutoSessions = new Dictionary(StringComparer.Ordinal); + + public static DirichletAdNative Create() => DirichletAdManager.CreateAdNative(); + + internal DirichletAdNative(IDirichletPlatformBridge bridge) + { + this.bridge = bridge ?? throw new ArgumentNullException(nameof(bridge)); + } + + public void LoadRewardVideoAd(DirichletAdRequest request, Action onLoaded, Action onFailure) + { + if (!ValidateRequest(request, onFailure)) + { + return; + } + + bridge.LoadRewardVideoAd(request, + handle => + { + var ad = DirichletRewardVideoAd.Create(bridge, handle, request.SpaceId); + ad.MarkLoaded(); + onLoaded?.Invoke(ad); + }, + onFailure); + } + + public void LoadInterstitialAd(DirichletAdRequest request, Action onLoaded, Action onFailure) + { + if (!ValidateRequest(request, onFailure)) + { + return; + } + + bridge.LoadInterstitialAd(request, + handle => + { + var ad = DirichletInterstitialAd.Create(bridge, handle, request.SpaceId); + ad.MarkLoaded(); + onLoaded?.Invoke(ad); + }, + onFailure); + } + + public void LoadBannerAd(DirichletAdRequest request, Action onLoaded, Action onFailure) + { + if (!ValidateRequest(request, onFailure)) + { + return; + } + + bridge.LoadBannerAd(request, + handle => + { + var ad = DirichletBannerAd.Create(bridge, handle, request.SpaceId); + ad.MarkLoaded(); + onLoaded?.Invoke(ad); + }, + onFailure); + } + + public void LoadSplashAd(DirichletAdRequest request, Action onLoaded, Action onFailure) + { + if (!ValidateRequest(request, onFailure)) + { + return; + } + + bridge.LoadSplashAd(request, + handle => + { + var ad = DirichletSplashAd.Create(bridge, handle, request.SpaceId); + ad.MarkLoaded(); + onLoaded?.Invoke(ad); + }, + onFailure); + } + + public void LoadExpressFeedAd(DirichletAdRequest request, Action> onLoaded, Action onFailure) + { + NotifyNotSupported("express_feed", onFailure); + } + + public void LoadNativeFeedAd(DirichletAdRequest request, Action> onLoaded, Action onFailure) + { + NotifyNotSupported("native_feed", onFailure); + } + + /// + /// Shows a reward video ad with automatic load-and-show logic. + /// This method combines loading and showing into a single operation. + /// + /// - Android: 使用 native 侧的 AutoAd(可能包含缓存策略,保持原有行为) + /// - iOS: 使用 Unity C# 侧的“load 成功后立刻 show”的简化方案(不做缓存) + /// + /// Ad request parameters + /// Listener for all ad events (show/close/reward/click/error) + public void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener) + { + if (!ValidateRequest(request, error => listener?.OnError(error))) + { + return; + } + +#if UNITY_ANDROID && !UNITY_EDITOR + bridge.ShowRewardVideoAutoAd(request, listener); +#else + // iOS/Editor/other platforms: use simplified load+show implementation (no cache). + ShowRewardVideoLoadAndShowInternal(request, listener); +#endif + } + + /// + /// Shows an interstitial ad with automatic load-and-show logic. + /// Android only - iOS receives OnError with not_supported. + /// + public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener) + { + if (!ValidateRequest(request, error => listener?.OnError(error))) + { + return; + } + + bridge.ShowInterstitialAutoAd(request, listener); + } + + /// + /// Shows a banner ad with automatic load + rotation logic. Container is created internally and + /// anchored at the bottom of the screen by default. Use + /// to control rotation interval. Android only - iOS receives OnError with not_supported. + /// + public void ShowBannerAutoAd(DirichletAdRequest request, IDirichletBannerAutoAdListener listener) + { + ShowBannerAutoAd(request, null, listener); + } + + /// + /// Banner auto-ad with explicit alignment/offset overrides. Advanced overload — + /// the standard form is . + /// + public void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener) + { + if (!ValidateRequest(request, error => listener?.OnError(error))) + { + return; + } + + bridge.ShowBannerAutoAd(request, options ?? new DirichletAdShowOptions(), listener); + } + + /// + /// Shows a splash ad with automatic load logic. Container is a fullscreen overlay created internally. + /// Android only - iOS receives OnError with not_supported. + /// + public void ShowSplashAutoAd(DirichletAdRequest request, IDirichletSplashAutoAdListener listener) + { + ShowSplashAutoAd(request, null, listener); + } + + /// + /// Splash auto-ad advanced overload (currently no effective options for splash). + /// + public void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener) + { + if (!ValidateRequest(request, error => listener?.OnError(error))) + { + return; + } + + bridge.ShowSplashAutoAd(request, options ?? new DirichletAdShowOptions(), listener); + } + + /// + /// Preloads an ad for later auto-show. Currently only type=3 (reward video) is handled by the native SDK. + /// Android only - iOS is a no-op. + /// + public void PreLoad(DirichletAdRequest request, int type) + { + if (!ValidateRequest(request, null)) + { + return; + } + + bridge.PreLoad(request, type); + } + + private void ShowRewardVideoLoadAndShowInternal(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener) + { + LoadRewardVideoAd( + request, + ad => + { + if (ad == null) + { + listener?.OnError(new DirichletError("invalid_ad", "Load callback returned null ad")); + return; + } + + var sessionId = Guid.NewGuid().ToString("N"); + var session = new AutoRewardVideoSession(sessionId, ad, listener); + RegisterRewardAutoSession(session); + ad.SetInteractionListener(session); + + // Load succeeded; show immediately. + var shown = ad.Show(); + if (!shown) + { + session.FailAndDispose(new DirichletError("show_failed", "ShowRewardVideoAd returned false")); + } + }, + error => + { + listener?.OnError(error ?? new DirichletError("load_failed", "LoadRewardVideoAd failed")); + }); + } + + private static bool ValidateRequest(DirichletAdRequest request, Action onFailure) + { + if (request == null) + { + onFailure?.Invoke(new DirichletError("invalid_request", "DirichletAdRequest cannot be null")); + return false; + } + + if (request.SpaceId <= 0) + { + onFailure?.Invoke(new DirichletError("invalid_space_id", "DirichletAdRequest.SpaceId must be greater than zero")); + return false; + } + + return true; + } + + private void NotifyNotSupported(string feature, Action onFailure) + { + onFailure?.Invoke(new DirichletError("not_supported", $"Dirichlet Unity bridge does not yet support {feature} ads.")); + } + + private static void RegisterRewardAutoSession(AutoRewardVideoSession session) + { + if (session == null) + { + return; + } + + lock (RewardAutoSessionLock) + { + RewardAutoSessions[session.SessionId] = session; + } + } + + private static void UnregisterRewardAutoSession(string sessionId) + { + if (string.IsNullOrEmpty(sessionId)) + { + return; + } + + lock (RewardAutoSessionLock) + { + RewardAutoSessions.Remove(sessionId); + } + } + + private sealed class AutoRewardVideoSession : IDirichletRewardAdInteractionListener + { + public string SessionId { get; } + + private readonly DirichletRewardVideoAd ad; + private readonly IDirichletRewardVideoAutoAdListener listener; + private bool disposed; + + public AutoRewardVideoSession(string sessionId, DirichletRewardVideoAd ad, IDirichletRewardVideoAutoAdListener listener) + { + SessionId = string.IsNullOrEmpty(sessionId) ? Guid.NewGuid().ToString("N") : sessionId; + this.ad = ad ?? throw new ArgumentNullException(nameof(ad)); + this.listener = listener; + } + + public void OnAdShow() + { + if (disposed) + { + return; + } + + listener?.OnAdShow(); + } + + public void OnAdClick() + { + if (disposed) + { + return; + } + + listener?.OnAdClick(); + } + + public void OnAdClose() + { + if (disposed) + { + return; + } + + listener?.OnAdClose(); + Dispose(); + } + + public void OnRewardVerify(DirichletRewardVerificationEventArgs args) + { + if (disposed) + { + return; + } + + listener?.OnRewardVerify(args); + } + + public void FailAndDispose(DirichletError error) + { + if (disposed) + { + return; + } + + listener?.OnError(error ?? new DirichletError("unknown_error", "Unknown error")); + Dispose(); + } + + private void Dispose() + { + if (disposed) + { + return; + } + + disposed = true; + UnregisterRewardAutoSession(SessionId); + + try + { + ad?.Destroy(); + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet] Auto reward ad Destroy failed: {ex.Message}"); + } + } + } + } + + /// + /// Base Unity-side ad handle. Responsible for keeping track of lifecycle state. + /// Matches the native SDK pattern where ad objects are returned from loadXXXAd() methods. + /// + public abstract class DirichletAd + { + protected readonly DirichletPlatformAdHandle PlatformHandle; + private readonly IDirichletPlatformBridge bridge; + private readonly long spaceId; + + public string SlotId => spaceId > 0 ? spaceId.ToString(CultureInfo.InvariantCulture) : string.Empty; + public bool IsLoaded { get; protected set; } + + /// + /// Returns whether the ad is still valid and can be shown. + /// This checks the native ad object's validity, which may expire after some time. + /// Always call this before Show() to ensure the ad hasn't expired. + /// + public bool IsValid => bridge?.IsAdValid(PlatformHandle) ?? false; + + /// + /// Raised when the native layer confirms the ad was shown to the user. + /// + public event Action Shown; + + /// + /// Raised when the user clicks the ad. + /// + public event Action Clicked; + + /// + /// Raised when the ad is closed (either by user action or channel logic). + /// + public event Action Closed; + + internal DirichletAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle platformHandle, long spaceId) + { + this.bridge = bridge ?? throw new ArgumentNullException(nameof(bridge)); + PlatformHandle = platformHandle ?? throw new ArgumentNullException(nameof(platformHandle)); + this.spaceId = spaceId; + if (!string.IsNullOrEmpty(PlatformHandle.DebugId)) + { + DirichletAdEventRouter.Register(PlatformHandle.DebugId, this); + } + } + + internal IDirichletPlatformBridge Bridge => bridge; + + public virtual bool Show() + { + if (!IsLoaded) + { + Debug.LogWarning($"[Dirichlet] Attempted to show ad before load success. Slot={SlotId}"); + } + + var shown = ShowInternal(null); + if (shown) + { + IsLoaded = false; + } + return shown; + } + + public void Destroy() + { + if (!string.IsNullOrEmpty(PlatformHandle?.DebugId)) + { + DirichletAdEventRouter.Unregister(PlatformHandle.DebugId); + } + DestroyInternal(); + IsLoaded = false; + } + + internal void MarkLoaded() + { + IsLoaded = true; + } + + protected abstract bool ShowInternal(DirichletAdShowOptions options); + + protected abstract void DestroyInternal(); + + internal virtual void HandleNativeEvent(DirichletNativeEvent nativeEvent) + { + switch (nativeEvent.EventName) + { + case DirichletNativeEventNames.Show: + OnShown(); + break; + case DirichletNativeEventNames.Click: + OnClicked(); + break; + case DirichletNativeEventNames.Close: + OnClosed(); + break; + } + } + + protected virtual void OnShown() + { + Shown?.Invoke(); + } + + protected virtual void OnClicked() + { + Clicked?.Invoke(); + } + + protected virtual void OnClosed() + { + Closed?.Invoke(); + } + } + + public abstract class DirichletRewardAdBase : DirichletAd + { + public event Action RewardVerified; + private IDirichletRewardAdInteractionListener interactionListener; + + internal DirichletRewardAdBase(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + : base(bridge, handle, spaceId) + { + } + + public void SetInteractionListener(IDirichletRewardAdInteractionListener listener) + { + interactionListener = listener; + } + + public void SetRewardAdInteractionListener(IDirichletRewardAdInteractionListener listener) + { + SetInteractionListener(listener); + } + + protected override void OnShown() + { + base.OnShown(); + interactionListener?.OnAdShow(); + } + + protected override void OnClicked() + { + base.OnClicked(); + interactionListener?.OnAdClick(); + } + + protected override void OnClosed() + { + base.OnClosed(); + interactionListener?.OnAdClose(); + } + + internal override void HandleNativeEvent(DirichletNativeEvent nativeEvent) + { + base.HandleNativeEvent(nativeEvent); + + if (nativeEvent.EventName == DirichletNativeEventNames.Reward && nativeEvent.Data != null) + { + var data = nativeEvent.Data; + var args = new DirichletRewardVerificationEventArgs( + data.RewardVerify, + data.RewardAmount, + data.RewardName, + data.Code, + data.Message); + RewardVerified?.Invoke(args); + interactionListener?.OnRewardVerify(args); + } + } + + protected override bool ShowInternal(DirichletAdShowOptions options) + { + return Bridge.ShowRewardVideoAd(PlatformHandle); + } + + protected override void DestroyInternal() + { + Bridge.DestroyAd(PlatformHandle); + } + } + + public sealed class DirichletRewardVideoAd : DirichletRewardAdBase + { + private DirichletRewardVideoAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + : base(bridge, handle, spaceId) + { + } + + internal static DirichletRewardVideoAd Create(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + { + return new DirichletRewardVideoAd(bridge, handle, spaceId); + } + } + + [Obsolete("Use DirichletRewardVideoAd instead.")] + public sealed class DirichletRewardAd : DirichletRewardAdBase + { + internal DirichletRewardAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) : base(bridge, handle, spaceId) + { + } + } + + public sealed class DirichletInterstitialAd : DirichletAd + { + private IDirichletInterstitialAdInteractionListener interactionListener; + + private DirichletInterstitialAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + : base(bridge, handle, spaceId) + { + } + + public void SetInteractionListener(IDirichletInterstitialAdInteractionListener listener) + { + interactionListener = listener; + } + + protected override void OnShown() + { + base.OnShown(); + interactionListener?.OnAdShow(); + } + + protected override void OnClicked() + { + base.OnClicked(); + interactionListener?.OnAdClick(); + } + + protected override void OnClosed() + { + base.OnClosed(); + interactionListener?.OnAdClose(); + } + + protected override bool ShowInternal(DirichletAdShowOptions options) + { + return Bridge.ShowInterstitialAd(PlatformHandle); + } + + protected override void DestroyInternal() + { + Bridge.DestroyAd(PlatformHandle); + } + + internal static DirichletInterstitialAd Create(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + { + return new DirichletInterstitialAd(bridge, handle, spaceId); + } + } + + public sealed class DirichletBannerAd : DirichletAd + { + private IDirichletBannerAdInteractionListener interactionListener; + + private DirichletBannerAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + : base(bridge, handle, spaceId) + { + } + + public void SetInteractionListener(IDirichletBannerAdInteractionListener listener) + { + interactionListener = listener; + } + + public bool Show(DirichletAdShowOptions options) + { + var showOptions = options ?? new DirichletAdShowOptions(); + var shown = ShowInternal(showOptions); + if (shown) + { + IsLoaded = false; + } + return shown; + } + + public override bool Show() + { + return Show(null); + } + + protected override void OnShown() + { + base.OnShown(); + interactionListener?.OnAdShow(); + } + + protected override void OnClicked() + { + base.OnClicked(); + interactionListener?.OnAdClick(); + } + + protected override void OnClosed() + { + base.OnClosed(); + interactionListener?.OnAdClose(); + } + + protected override bool ShowInternal(DirichletAdShowOptions options) + { + return Bridge.ShowBannerAd(PlatformHandle, options ?? new DirichletAdShowOptions()); + } + + protected override void DestroyInternal() + { + Bridge.DestroyAd(PlatformHandle); + } + + internal static DirichletBannerAd Create(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + { + return new DirichletBannerAd(bridge, handle, spaceId); + } + } + + public sealed class DirichletSplashAd : DirichletAd + { + private IDirichletSplashAdInteractionListener interactionListener; + + private DirichletSplashAd(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + : base(bridge, handle, spaceId) + { + } + + public void SetInteractionListener(IDirichletSplashAdInteractionListener listener) + { + interactionListener = listener; + } + + protected override void OnShown() + { + base.OnShown(); + interactionListener?.OnAdShow(); + } + + protected override void OnClicked() + { + base.OnClicked(); + interactionListener?.OnAdClick(); + } + + protected override void OnClosed() + { + base.OnClosed(); + interactionListener?.OnAdClose(); + } + + protected override bool ShowInternal(DirichletAdShowOptions options) + { + return Bridge.ShowSplashAd(PlatformHandle, options ?? new DirichletAdShowOptions()); + } + + protected override void DestroyInternal() + { + Bridge.DestroyAd(PlatformHandle); + } + + internal static DirichletSplashAd Create(IDirichletPlatformBridge bridge, DirichletPlatformAdHandle handle, long spaceId) + { + return new DirichletSplashAd(bridge, handle, spaceId); + } + } + + internal static class DirichletNativeEventNames + { + internal const string Show = "show"; + internal const string Click = "click"; + internal const string Close = "close"; + internal const string Reward = "reward"; + } + + internal readonly struct DirichletNativeEvent + { + public string EventName { get; } + public string AdType { get; } + public DirichletNativeEventData Data { get; } + + public DirichletNativeEvent(string eventName, string adType, DirichletNativeEventData data) + { + EventName = string.IsNullOrEmpty(eventName) ? string.Empty : eventName; + AdType = string.IsNullOrEmpty(adType) ? string.Empty : adType; + Data = data; + } + } + + internal sealed class DirichletNativeEventData + { + public bool RewardVerify { get; } + public int RewardAmount { get; } + public string RewardName { get; } + public int Code { get; } + public string Message { get; } + + public DirichletNativeEventData(bool rewardVerify, int rewardAmount, string rewardName, int code, string message) + { + RewardVerify = rewardVerify; + RewardAmount = rewardAmount; + RewardName = string.IsNullOrEmpty(rewardName) ? string.Empty : rewardName; + Code = code; + Message = message ?? string.Empty; + } + } + + public sealed class DirichletRewardVerificationEventArgs : EventArgs + { + public bool IsVerified { get; } + public int RewardAmount { get; } + public string RewardName { get; } + public int Code { get; } + public string Message { get; } + + internal DirichletRewardVerificationEventArgs(bool rewardVerified, int rewardAmount, string rewardName, int code, string message) + { + IsVerified = rewardVerified; + RewardAmount = rewardAmount; + RewardName = string.IsNullOrEmpty(rewardName) ? string.Empty : rewardName; + Code = code; + Message = message ?? string.Empty; + } + } + + public interface IDirichletRewardAdInteractionListener + { + void OnAdShow(); + void OnAdClick(); + void OnAdClose(); + void OnRewardVerify(DirichletRewardVerificationEventArgs args); + } + + public interface IDirichletInterstitialAdInteractionListener + { + void OnAdShow(); + void OnAdClick(); + void OnAdClose(); + } + + public interface IDirichletBannerAdInteractionListener + { + void OnAdShow(); + void OnAdClick(); + void OnAdClose(); + } + + public interface IDirichletSplashAdInteractionListener + { + void OnAdShow(); + void OnAdClick(); + void OnAdClose(); + } + + /// + /// Listener interface for auto interstitial ad callbacks. + /// Android only - iOS will receive OnError with not_supported error. + /// + public interface IDirichletInterstitialAutoAdListener + { + void OnError(DirichletError error); + void OnAdShow(); + void OnAdClose(); + void OnAdClick(); + } + + /// + /// Listener interface for auto banner ad callbacks. + /// Android only - iOS will receive OnError with not_supported error. + /// + public interface IDirichletBannerAutoAdListener + { + void OnError(DirichletError error); + void OnAdShow(); + void OnAdClose(); + void OnAdClick(); + } + + /// + /// Listener interface for auto splash ad callbacks. + /// Android only - iOS will receive OnError with not_supported error. + /// + public interface IDirichletSplashAutoAdListener + { + void OnError(DirichletError error); + void OnAdShow(); + void OnAdClose(); + void OnAdClick(); + } + + /// + /// Listener interface for auto reward video ad callbacks. + /// Used with ShowRewardVideoAutoAd which combines load and show into one operation. + /// Android only - iOS will receive OnError with not_supported error. + /// + public interface IDirichletRewardVideoAutoAdListener + { + /// + /// Called when ad fails to load or show. + /// + void OnError(DirichletError error); + + /// + /// Called when ad is shown to the user. + /// + void OnAdShow(); + + /// + /// Called when ad is closed. + /// + void OnAdClose(); + + /// + /// Called when reward verification is completed. + /// + void OnRewardVerify(DirichletRewardVerificationEventArgs args); + + /// + /// Called when ad is clicked. + /// + void OnAdClick(); + } + + internal static class DirichletAdEventRouter + { + private const string CallbackObjectName = "DirichletMediationEventReceiver"; + private static readonly Dictionary Ads = new Dictionary(StringComparer.Ordinal); + private static bool receiverInitialized; + + internal static void Register(string handleId, DirichletAd ad) + { + if (string.IsNullOrEmpty(handleId) || ad == null) + { + return; + } + + EnsureReceiver(); + Ads[handleId] = new WeakReference(ad); + } + + internal static void Unregister(string handleId) + { + if (string.IsNullOrEmpty(handleId)) + { + return; + } + + Ads.Remove(handleId); + } + + private static void EnsureReceiver() + { + if (receiverInitialized) + { + return; + } + + if (!DirichletSdk.IsUnityThread) + { + DirichletSdk.DispatchToUnityThread(EnsureReceiver); + return; + } + + var existing = GameObject.Find(CallbackObjectName); + if (existing == null) + { + var host = new GameObject(CallbackObjectName) + { + hideFlags = HideFlags.HideAndDontSave + }; + UnityEngine.Object.DontDestroyOnLoad(host); + host.AddComponent(); + } + + receiverInitialized = true; + } + + internal static void HandleNativeEvent(string payload) + { + if (string.IsNullOrEmpty(payload)) + { + return; + } + + NativeEventPayload message; + try + { + message = JsonUtility.FromJson(payload); + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet] Failed to parse native ad event: {ex.Message}\n{payload}"); + return; + } + + if (message == null || string.IsNullOrEmpty(message.handle) || string.IsNullOrEmpty(message.eventName)) + { + return; + } + + if (!Ads.TryGetValue(message.handle, out var weakReference)) + { + return; + } + + if (!(weakReference.Target is DirichletAd ad) || ad == null) + { + Ads.Remove(message.handle); + return; + } + + var data = message.data != null + ? new DirichletNativeEventData( + message.data.rewardVerify, + message.data.rewardAmount, + message.data.rewardName, + message.data.code, + message.data.message) + : null; + + var nativeEvent = new DirichletNativeEvent(message.eventName, message.adType, data); + + if (DirichletSdk.IsUnityThread) + { + ad.HandleNativeEvent(nativeEvent); + } + else + { + DirichletSdk.DispatchToUnityThread(() => ad.HandleNativeEvent(nativeEvent)); + } + } + + [Serializable] + private class NativeEventPayload + { + public string handle; + public string eventName; + public string adType; + public NativeEventPayloadData data; + } + + [Serializable] + private class NativeEventPayloadData + { + public bool rewardVerify; + public int rewardAmount; + public string rewardName; + public int code; + public string message; + } + + private sealed class DirichletAdEventReceiver : MonoBehaviour + { + public void OnNativeEvent(string payload) + { + HandleNativeEvent(payload); + } + } + } +} + + diff --git a/Assets/DirichletMediation/Runtime/DirichletAdTypes.cs.meta b/Assets/DirichletMediation/Runtime/DirichletAdTypes.cs.meta new file mode 100644 index 0000000..dc03408 --- /dev/null +++ b/Assets/DirichletMediation/Runtime/DirichletAdTypes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eec9af0b7d54e4522b6d9c84d29e65de +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DirichletMediation/Runtime/DirichletMediationSdk.cs b/Assets/DirichletMediation/Runtime/DirichletMediationSdk.cs new file mode 100644 index 0000000..cf49d50 --- /dev/null +++ b/Assets/DirichletMediation/Runtime/DirichletMediationSdk.cs @@ -0,0 +1,2036 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using UnityEngine; + +namespace Dirichlet.Mediation +{ + /// + /// Entry point of the Dirichlet Mediation Unity wrapper. + /// Provides initialization, configuration, and shared helpers that are platform agnostic. + /// + public static class DirichletSdk + { + private static readonly IDirichletPlatformBridge Bridge = DirichletPlatformBridgeFactory.Create(); + + private static SynchronizationContext unityContext; + private static int unityThreadId; + private static readonly Queue pendingActions = new Queue(); + private static readonly object pendingActionsLock = new object(); + private static UnityThreadPump pump; + + /// + /// Indicates whether the mediation SDK was initialized successfully. + /// + public static bool IsInitialized { get; private set; } + + public static bool IsUnityThread => unityThreadId != 0 && Thread.CurrentThread.ManagedThreadId == unityThreadId; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void CaptureUnitySynchronizationContext() + { + unityThreadId = Thread.CurrentThread.ManagedThreadId; + unityContext = SynchronizationContext.Current; + EnsureUnityThreadPump(); + } + + // Internal thread dispatcher. Public SDK callbacks are already marshalled to Unity thread when needed. + internal static void DispatchToUnityThread(Action action) + { + if (action == null) + { + return; + } + + if (IsUnityThread) + { + try + { + action(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + return; + } + + if (unityContext != null) + { + unityContext.Post(_ => + { + try + { + action(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + }, null); + return; + } + + lock (pendingActionsLock) + { + pendingActions.Enqueue(action); + } + + EnsureUnityThreadPump(); + } + + /// + /// Initializes the mediation SDK using the aggregator-style configuration. + /// + public static void Init( + DirichletAdConfig config, + Action onSuccess = null, + Action onFailure = null) + { + InitializeInternal(config, onSuccess, onFailure); + } + + private static void InitializeInternal( + DirichletAdConfig config, + Action onSuccess, + Action onFailure) + { + if (IsInitialized) + { + Debug.Log("[Dirichlet] Initialize called, but SDK is already initialized."); + DispatchToUnityThread(() => onSuccess?.Invoke(DirichletInitResult.AlreadyInitialized())); + return; + } + + if (config == null) + { + DirichletAdManager.Clear(); + var error = new DirichletError("invalid_config", "DirichletAdConfig cannot be null"); + Debug.LogError(error); + DispatchToUnityThread(() => onFailure?.Invoke(error)); + return; + } + + var options = config.ToPlatformOptions(); + if (options == null) + { + DirichletAdManager.Clear(); + var error = new DirichletError("invalid_config", "Failed to map DirichletAdConfig to native options"); + Debug.LogError(error); + DispatchToUnityThread(() => onFailure?.Invoke(error)); + return; + } + + DirichletAdManager.ApplyConfig(config); + + void SuccessHandler(DirichletInitResult result) + { + DispatchToUnityThread(() => + { + IsInitialized = result?.Success ?? false; + onSuccess?.Invoke(result ?? DirichletInitResult.Ok("bridge_returned_null")); + }); + } + + void FailureHandler(DirichletError error) + { + DispatchToUnityThread(() => + { + IsInitialized = false; + onFailure?.Invoke(error ?? new DirichletError("bridge_error", "Initialization failed")); + }); + } + + Bridge.Initialize(options, SuccessHandler, FailureHandler); + } + + /// + /// Requests runtime permissions if the underlying SDK requires them. + /// + public static void RequestPermissionIfNecessary() + { + Bridge?.RequestPermissionIfNeeded(); + } + + [Obsolete("Use RequestPermissionIfNecessary() instead.")] + public static void RequestPermissionIfNeeded() + { + RequestPermissionIfNecessary(); + } + + /// + /// Returns the native SDK version if available. + /// + public static string GetVersion() => Bridge?.GetSdkVersion() ?? "unknown"; + + [Obsolete("Use GetVersion() instead.")] + public static string GetSdkVersion() => GetVersion(); + + internal static IDirichletPlatformBridge GetBridge() => Bridge; + + private static void EnsureUnityThreadPump() + { + if (pump != null || !Application.isPlaying) + { + return; + } + + if (!IsUnityThread) + { + unityContext?.Post(_ => EnsureUnityThreadPump(), null); + return; + } + + var host = new GameObject("DirichletUnityThreadPump") + { + hideFlags = HideFlags.HideAndDontSave + }; + UnityEngine.Object.DontDestroyOnLoad(host); + pump = host.AddComponent(); + } + + private sealed class UnityThreadPump : MonoBehaviour + { + private readonly List executionBuffer = new List(8); + + private void Awake() + { + UnityEngine.Object.DontDestroyOnLoad(gameObject); + } + + private void Update() + { + executionBuffer.Clear(); + + lock (pendingActionsLock) + { + while (pendingActions.Count > 0) + { + executionBuffer.Add(pendingActions.Dequeue()); + } + } + + for (int i = 0; i < executionBuffer.Count; i++) + { + var action = executionBuffer[i]; + try + { + action?.Invoke(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + } + + private void OnDestroy() + { + if (ReferenceEquals(pump, this)) + { + pump = null; + } + } + } + } + + #region Initialization data + + public enum DirichletAdNetworkType + { + Unknown = 0, + Csj = 1, + Gdt = 2, + Tap = 3 + } + + [Serializable] + /// + /// 配置 Dirichlet Mediation SDK 的初始化参数 + /// + public sealed class DirichletAdConfig + { + public long MediaId { get; } + public string MediaName { get; } + public string MediaKey { get; } + public string GameChannel { get; } + + /// + /// 子渠道标识 + /// + /// + /// Android: 用于区分不同的子渠道来源 + /// iOS: 此属性不使用,传递的值会被忽略 + /// + public string SubChannel { get; } + + public bool DebugEnabled { get; } + public string TapClientId { get; } + public bool ShakeEnabled { get; } + public string CustomConfigJson { get; } + public string DataJson { get; } + + /// + /// 控制是否允许访问广告标识符 + /// + /// + /// iOS: 控制 IDFA 访问。设置为 true 时,iOS 14+ 会检查 ATT 授权状态后读取 IDFA。 + /// Android: 此属性不使用,Android 使用 OAID/AAID 机制。 + /// 默认值: true + /// + public bool AllowIDFAAccess { get; } + + /// + /// 外部配置的 aTags(JSON 格式) + /// + /// + /// 可选配置,用于传递额外的标签信息到广告 SDK。 + /// 格式: JSON 字符串,例如 {"key":"value"} + /// 平台支持: iOS/Android 通用 + /// + public string ATags { get; } + + internal string LegacyAppId { get; } + + private DirichletAdConfig(Builder builder) + { + MediaId = builder.mediaId; + LegacyAppId = builder.legacyAppId; + MediaName = builder.mediaName; + MediaKey = builder.mediaKey; + GameChannel = string.IsNullOrEmpty(builder.gameChannel) ? "default" : builder.gameChannel; + SubChannel = builder.subChannel; + DebugEnabled = builder.enableDebug; + TapClientId = builder.tapClientId; + ShakeEnabled = builder.shakeEnabled; + CustomConfigJson = builder.customConfigJson; + DataJson = builder.dataJson; + AllowIDFAAccess = builder.allowIDFAAccess; + ATags = builder.aTags; + } + + public Builder ToBuilder() + { + return new Builder() + .WithMediaId(MediaId) + .WithMediaName(MediaName) + .WithMediaKey(MediaKey) + .WithGameChannel(GameChannel) + .WithSubChannel(SubChannel) + .EnableDebug(DebugEnabled) + .WithTapClientId(TapClientId) + .ShakeEnabled(ShakeEnabled) + .WithCustomConfigJson(CustomConfigJson) + .WithDataJson(DataJson) + .WithAppId(LegacyAppId) + .AllowIDFAAccess(AllowIDFAAccess) + .WithATags(ATags); + } + + internal DirichletPlatformInitOptions ToPlatformOptions() + { + return new DirichletPlatformInitOptions + { + MediaId = MediaId, + AppId = LegacyAppId, + Channel = GameChannel, + SubChannel = SubChannel, + EnableLog = DebugEnabled, + MediaName = MediaName, + MediaKey = MediaKey, + TapClientId = TapClientId, + ShakeEnabled = ShakeEnabled, + CustomConfigJson = CustomConfigJson, + DataJson = DataJson, + AllowIDFAAccess = AllowIDFAAccess, + ATags = ATags + }; + } + + public override string ToString() + { + return $"DirichletAdConfig(MediaId={MediaId}, MediaName={MediaName}, GameChannel={GameChannel}, SubChannel={SubChannel}, DebugEnabled={DebugEnabled}, TapClientId={(string.IsNullOrEmpty(TapClientId) ? "" : TapClientId)}, ShakeEnabled={ShakeEnabled})"; + } + + public sealed class Builder + { + internal long mediaId; + internal string mediaName = "Unity Dirichlet Demo"; + internal string mediaKey; + internal string gameChannel = "default"; + internal string subChannel; + internal bool enableDebug = true; + internal string tapClientId; + internal bool shakeEnabled = true; + internal string customConfigJson; + internal string dataJson; + internal string legacyAppId; + internal bool allowIDFAAccess = true; + internal string aTags; + + public Builder WithMediaId(long value) + { + mediaId = value; + return this; + } + + public Builder WithAppId(string appId) + { + legacyAppId = appId; + if (!string.IsNullOrEmpty(appId) && long.TryParse(appId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed > 0) + { + mediaId = parsed; + } + return this; + } + + public Builder WithMediaName(string value) + { + mediaName = value; + return this; + } + + public Builder WithMediaKey(string value) + { + mediaKey = value; + return this; + } + + public Builder WithGameChannel(string value) + { + gameChannel = value; + return this; + } + + /// + /// 设置子渠道标识(Android only,iOS 会忽略此值) + /// + public Builder WithSubChannel(string value) + { + subChannel = value; + return this; + } + + public Builder EnableDebug(bool enabled) + { + enableDebug = enabled; + return this; + } + + public Builder WithTapClientId(string value) + { + tapClientId = value; + return this; + } + + public Builder ShakeEnabled(bool enabled) + { + shakeEnabled = enabled; + return this; + } + + public Builder WithCustomConfigJson(string json) + { + customConfigJson = json; + return this; + } + + public Builder WithDataJson(string json) + { + dataJson = json; + return this; + } + + /// + /// 设置是否允许访问广告标识符(iOS: IDFA,Android: 忽略) + /// + /// true 表示允许访问(默认),false 表示禁止访问 + public Builder AllowIDFAAccess(bool enabled) + { + allowIDFAAccess = enabled; + return this; + } + + /// + /// 设置外部 aTags(JSON 格式,两平台通用) + /// + /// JSON 字符串,例如 {"key":"value"} + public Builder WithATags(string value) + { + aTags = value; + return this; + } + + public DirichletAdConfig Build() + { + return new DirichletAdConfig(this); + } + } + } + + public sealed class DirichletInitResult + { + public bool Success { get; } + public string Message { get; } + + private DirichletInitResult(bool success, string message) + { + Success = success; + Message = message; + } + + public static DirichletInitResult Ok(string message = null) => new DirichletInitResult(true, message); + public static DirichletInitResult Failed(string message) => new DirichletInitResult(false, message); + public static DirichletInitResult AlreadyInitialized() => new DirichletInitResult(true, "already_initialized"); + } + + /// + /// 平台桥接层使用的初始化选项(内部类) + /// + internal sealed class DirichletPlatformInitOptions + { + public string AppId { get; set; } + public long MediaId { get; set; } + public string Channel { get; set; } + + /// + /// 子渠道标识(Android only) + /// + public string SubChannel { get; set; } + + public bool EnableLog { get; set; } + public string MediaName { get; set; } + public string MediaKey { get; set; } + public string TapClientId { get; set; } + public bool ShakeEnabled { get; set; } + public string CustomConfigJson { get; set; } + public string DataJson { get; set; } + + /// + /// 控制 IDFA 访问(iOS only) + /// + public bool AllowIDFAAccess { get; set; } + + /// + /// 外部 aTags JSON(通用) + /// + public string ATags { get; set; } + + internal string GetAppIdString() + { + if (MediaId > 0) + { + return MediaId.ToString(CultureInfo.InvariantCulture); + } + + return AppId ?? string.Empty; + } + + public override string ToString() + { + return $"DirichletPlatformInitOptions(MediaId={MediaId}, AppId={AppId}, Channel={Channel}, SubChannel={SubChannel}, EnableLog={EnableLog}, MediaName={MediaName}, MediaKey={(string.IsNullOrEmpty(MediaKey) ? "" : "***")}, TapClientId={(string.IsNullOrEmpty(TapClientId) ? "" : TapClientId)}, ShakeEnabled={ShakeEnabled}, AllowIDFAAccess={AllowIDFAAccess}, ATags={(string.IsNullOrEmpty(ATags) ? "" : ATags)})"; + } + } + + public sealed class DirichletError + { + public string Code { get; } + public string Message { get; } + public string Adapter { get; } + public string Network { get; } + + public DirichletError(string code, string message, string adapter = null, string network = null) + { + Code = string.IsNullOrEmpty(code) ? "unknown" : code; + Message = message ?? string.Empty; + Adapter = adapter; + Network = network; + } + + public override string ToString() + { + return $"DirichletError(Code={Code}, Message={Message}, Adapter={Adapter}, Network={Network})"; + } + } + + #endregion + + #region Platform bridge plumbing + + internal interface IDirichletPlatformBridge + { + void Initialize(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure); + void LoadRewardVideoAd(DirichletAdRequest request, Action onSuccess, Action onFailure); + bool ShowRewardVideoAd(DirichletPlatformAdHandle handle); + + void LoadInterstitialAd(DirichletAdRequest request, Action onSuccess, Action onFailure); + bool ShowInterstitialAd(DirichletPlatformAdHandle handle); + + void LoadBannerAd(DirichletAdRequest request, Action onSuccess, Action onFailure); + bool ShowBannerAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options); + + void LoadSplashAd(DirichletAdRequest request, Action onSuccess, Action onFailure); + bool ShowSplashAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options); + + void DestroyAd(DirichletPlatformAdHandle handle); + bool IsAdValid(DirichletPlatformAdHandle handle); + void RequestPermissionIfNeeded(); + string GetSdkVersion(); + + /// + /// Shows a reward video ad with automatic load-and-show logic. + /// Android only - iOS will call onFailure with not_supported error. + /// + void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener); + + /// + /// Shows an interstitial ad with automatic load-and-show logic. Android only. + /// + void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener); + + /// + /// Shows a banner ad with automatic load + rotation logic. Android only. + /// + void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener); + + /// + /// Shows a splash ad with automatic load logic. Android only. + /// + void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener); + + /// + /// Preloads an ad for later auto-show. Android only. + /// + /// Ad type code; native SDK currently only handles type=3 (reward video). + void PreLoad(DirichletAdRequest request, int type); + } + + internal static class DirichletPlatformBridgeFactory + { + private static IDirichletPlatformBridge instance; + + internal static IDirichletPlatformBridge Create() + { + if (instance != null) + { + return instance; + } + +#if UNITY_ANDROID && !UNITY_EDITOR + instance = new AndroidDirichletBridge(); +#elif UNITY_IOS && !UNITY_EDITOR + instance = new IOSDirichletBridge(); +#else + instance = new NoopDirichletBridge(); +#endif + return instance; + } + + internal static void OverrideForTesting(IDirichletPlatformBridge customBridge) + { + instance = customBridge; + } + } + +#if UNITY_ANDROID && !UNITY_EDITOR + internal sealed class AndroidDirichletBridge : IDirichletPlatformBridge + { + private const string BridgeClassName = "com.dirichlet.unity.DirichletUnityBridge"; + private static AndroidJavaClass cachedBridgeClass; + + private static AndroidJavaClass BridgeClass + { + get + { + if (cachedBridgeClass == null) + { + cachedBridgeClass = new AndroidJavaClass(BridgeClassName); + } + return cachedBridgeClass; + } + } + + private readonly Dictionary loadCallbacks = new Dictionary(); + private readonly object loadCallbacksLock = new object(); + + public void Initialize(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure) + { + if (options == null) + { + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("android_invalid_options", "Initialization options cannot be null"))); + return; + } + + try + { + var dataPayload = !string.IsNullOrEmpty(options.DataJson) + ? options.DataJson + : options.CustomConfigJson; + + // Do not block Unity thread on Android init. The Java bridge enforces a timeout and reports via callback. + var callback = new AndroidInitCallback( + () => onSuccess?.Invoke(DirichletInitResult.Ok("android_bridge")), + onFailure); + + BridgeClass.CallStatic( + "initializeAsync", + options.GetAppIdString(), + options.Channel ?? string.Empty, + options.SubChannel ?? string.Empty, + options.EnableLog, + options.MediaName ?? string.Empty, + options.MediaKey ?? string.Empty, + options.TapClientId ?? string.Empty, + dataPayload ?? string.Empty, + options.ShakeEnabled, + callback); + } + catch (Exception ex) + { + Debug.LogException(ex); + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("android_exception", ex.Message))); + } + } + + private sealed class AndroidInitCallback : AndroidJavaProxy + { + // Reuse the existing Java callback interface to avoid adding more bridge-only types. + private const string ListenerInterface = "com.dirichlet.unity.DirichletUnityBridge$LoadListener"; + + private readonly Action success; + private readonly Action failure; + + public AndroidInitCallback(Action success, Action failure) + : base(ListenerInterface) + { + this.success = success; + this.failure = failure; + } + + // Called from Java (case-sensitive method name). + public void onSuccess() + { + DirichletSdk.DispatchToUnityThread(() => success?.Invoke()); + } + + // Called from Java (case-sensitive method name). + public void onError(string code, string message) + { + var errorCode = string.IsNullOrEmpty(code) ? "android_init_failed" : code; + DirichletSdk.DispatchToUnityThread(() => failure?.Invoke(new DirichletError(errorCode, message ?? string.Empty))); + } + } + + public void RequestPermissionIfNeeded() + { + try + { + BridgeClass.CallStatic("requestPermissionIfNeeded"); + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] requestPermissionIfNeeded failed: {ex.Message}"); + } + } + + public string GetSdkVersion() + { + try + { + return BridgeClass.CallStatic("getSdkVersion") ?? "android-unknown"; + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] getSdkVersion failed: {ex.Message}"); + return "android-error"; + } + } + + public void LoadRewardVideoAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + LoadAdInternal(DirichletAdType.RewardVideo, request, onSuccess, onFailure); + } + + public bool ShowRewardVideoAd(DirichletPlatformAdHandle handle) + { + return ShowAdInternal(handle, null); + } + + public void LoadInterstitialAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + LoadAdInternal(DirichletAdType.Interstitial, request, onSuccess, onFailure); + } + + public bool ShowInterstitialAd(DirichletPlatformAdHandle handle) + { + return ShowAdInternal(handle, null); + } + + public void LoadBannerAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + LoadAdInternal(DirichletAdType.Banner, request, onSuccess, onFailure); + } + + public bool ShowBannerAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options) + { + return ShowAdInternal(handle, options); + } + + public void LoadSplashAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + LoadAdInternal(DirichletAdType.Splash, request, onSuccess, onFailure); + } + + public bool ShowSplashAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options) + { + return ShowAdInternal(handle, options); + } + + public void DestroyAd(DirichletPlatformAdHandle handle) + { + DestroyAdInternal(handle); + } + + public bool IsAdValid(DirichletPlatformAdHandle handle) + { + if (handle == null || string.IsNullOrEmpty(handle.DebugId)) + { + return false; + } + + try + { + return BridgeClass.CallStatic("isAdValid", handle.DebugId); + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][Android] IsAdValid failed: {ex.Message}"); + return false; + } + } + + private void LoadAdInternal(DirichletAdType adType, DirichletAdRequest request, Action onSuccess, Action onFailure) + { + if (request == null) + { + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("invalid_request", "Request cannot be null"))); + return; + } + + try + { + var payload = request.ToBridgePayload(); + var callback = new AndroidLoadCallback(this, null, () => + { + // Handle will be set in callback's onSuccess + }, onFailure); + + string handleId; + using (var extras = BuildJsonObject(payload)) + { + // Use the new direct load methods that match native SDK pattern + // extras already contains space_id from request.ToBridgePayload() + switch (adType) + { + case DirichletAdType.RewardVideo: + handleId = BridgeClass.CallStatic("loadRewardVideoAd", extras, callback); + break; + case DirichletAdType.Interstitial: + handleId = BridgeClass.CallStatic("loadInterstitialAd", extras, callback); + break; + case DirichletAdType.Banner: + handleId = BridgeClass.CallStatic("loadBannerAd", extras, callback); + break; + case DirichletAdType.Splash: + handleId = BridgeClass.CallStatic("loadSplashAd", extras, callback); + break; + default: + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("unsupported_type", $"Unsupported ad type: {adType}"))); + return; + } + } + + if (string.IsNullOrEmpty(handleId)) + { + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("invalid_handle", "Bridge returned null handle"))); + return; + } + + // Create handle - simple wrapper around handle string + var handle = DirichletPlatformAdHandle.FromNative(handleId); + callback.SetHandle(handle); + callback.SetSuccessCallback(() => onSuccess?.Invoke(handle)); + + lock (loadCallbacksLock) + { + loadCallbacks[handleId] = callback; + } + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] LoadAd failed: {ex.Message}"); + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("android_exception", ex.Message))); + } + } + + private bool ShowAdInternal(DirichletPlatformAdHandle handle, DirichletAdShowOptions options) + { + try + { + var payload = options?.ToBridgePayload(); + + using (var extras = BuildJsonObject(payload)) + { + return BridgeClass.CallStatic("showAd", handle.DebugId, extras); + } + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] ShowAd failed: {ex.Message}"); + return false; + } + } + + private void DestroyAdInternal(DirichletPlatformAdHandle handle) + { + try + { + BridgeClass.CallStatic("destroyAd", handle.DebugId); + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] DestroyAd failed: {ex.Message}"); + } + finally + { + RemoveLoadCallback(handle?.DebugId); + } + } + + private AndroidJavaObject BuildJsonObject(Dictionary dictionary) + { + if (dictionary == null || dictionary.Count == 0) + { + return null; + } + + AndroidJavaObject json = null; + + try + { + json = new AndroidJavaObject("org.json.JSONObject"); + + foreach (var kv in dictionary) + { + if (string.IsNullOrEmpty(kv.Key)) + { + continue; + } + + var value = kv.Value; + if (value == null) + { + continue; + } + + try + { + switch (value) + { + case bool boolValue: + json.Call("put", kv.Key, boolValue); + break; + case int intValue: + json.Call("put", kv.Key, intValue); + break; + case long longValue: + json.Call("put", kv.Key, longValue); + break; + case float floatValue: + json.Call("put", kv.Key, (double)floatValue); + break; + case double doubleValue: + json.Call("put", kv.Key, doubleValue); + break; + case Enum enumValue: + json.Call("put", kv.Key, Convert.ToInt32(enumValue, CultureInfo.InvariantCulture)); + break; + default: + json.Call("put", kv.Key, value.ToString()); + break; + } + } + catch (Exception putEx) + { + Debug.LogWarning($"[Dirichlet][Android] Failed to add extra {kv.Key}: {putEx.Message}"); + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] Failed to build json object: {ex.Message}"); + } + + return json; + } + + private void RemoveLoadCallback(string handleId) + { + if (string.IsNullOrEmpty(handleId)) + { + return; + } + + lock (loadCallbacksLock) + { + loadCallbacks.Remove(handleId); + } + } + + private sealed class AndroidLoadCallback : AndroidJavaProxy + { + private readonly AndroidDirichletBridge owner; + private string handleId; + private Action success; + private readonly Action failure; + + public AndroidLoadCallback(AndroidDirichletBridge owner, string handleId, Action success, Action failure) + : base("com.dirichlet.unity.DirichletUnityBridge$LoadListener") + { + this.owner = owner; + this.handleId = handleId; + this.success = success; + this.failure = failure; + } + + public void SetHandle(DirichletPlatformAdHandle handle) + { + if (handle != null) + { + handleId = handle.DebugId; + } + } + + public void SetSuccessCallback(Action callback) + { + success = callback; + } + + public void onSuccess() + { + owner.RemoveLoadCallback(handleId); + DirichletSdk.DispatchToUnityThread(() => success?.Invoke()); + } + + public void onError(string code, string message) + { + owner.RemoveLoadCallback(handleId); + var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code; + DirichletSdk.DispatchToUnityThread(() => failure?.Invoke(new DirichletError(errorCode, message ?? string.Empty))); + } + } + + public void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener) + { + if (request == null) + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("invalid_request", "Request cannot be null"))); + return; + } + + try + { + var payload = request.ToBridgePayload(); + var callback = new AndroidRewardVideoAutoAdCallback(listener); + + using (var extras = BuildJsonObject(payload)) + { + BridgeClass.CallStatic("showRewardVideoAutoAd", extras, callback); + } + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] ShowRewardVideoAutoAd failed: {ex.Message}"); + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("android_exception", ex.Message))); + } + } + + public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener) + { + if (request == null) + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("invalid_request", "Request cannot be null"))); + return; + } + + try + { + var payload = request.ToBridgePayload(); + var callback = new AndroidInterstitialAutoAdCallback(listener); + + using (var extras = BuildJsonObject(payload)) + { + BridgeClass.CallStatic("showInterstitialAutoAd", extras, callback); + } + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] ShowInterstitialAutoAd failed: {ex.Message}"); + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("android_exception", ex.Message))); + } + } + + public void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener) + { + if (request == null) + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("invalid_request", "Request cannot be null"))); + return; + } + + try + { + var payload = request.ToBridgePayload(); + var optionsPayload = (options ?? new DirichletAdShowOptions()).ToBridgePayload(); + var callback = new AndroidBannerAutoAdCallback(listener); + + using (var extras = BuildJsonObject(payload)) + using (var optsJson = BuildJsonObject(optionsPayload)) + { + BridgeClass.CallStatic("showBannerAutoAd", extras, optsJson, callback); + } + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] ShowBannerAutoAd failed: {ex.Message}"); + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("android_exception", ex.Message))); + } + } + + public void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener) + { + if (request == null) + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("invalid_request", "Request cannot be null"))); + return; + } + + try + { + var payload = request.ToBridgePayload(); + var optionsPayload = (options ?? new DirichletAdShowOptions()).ToBridgePayload(); + var callback = new AndroidSplashAutoAdCallback(listener); + + using (var extras = BuildJsonObject(payload)) + using (var optsJson = BuildJsonObject(optionsPayload)) + { + BridgeClass.CallStatic("showSplashAutoAd", extras, optsJson, callback); + } + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] ShowSplashAutoAd failed: {ex.Message}"); + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("android_exception", ex.Message))); + } + } + + public void PreLoad(DirichletAdRequest request, int type) + { + if (request == null) + { + Debug.LogWarning("[Dirichlet][Android] PreLoad called with null request"); + return; + } + + try + { + var payload = request.ToBridgePayload(); + using (var extras = BuildJsonObject(payload)) + { + BridgeClass.CallStatic("preLoad", extras, type); + } + } + catch (Exception ex) + { + Debug.LogWarning($"[Dirichlet][Android] PreLoad failed: {ex.Message}"); + } + } + + private sealed class AndroidInterstitialAutoAdCallback : AndroidJavaProxy + { + private readonly IDirichletInterstitialAutoAdListener listener; + + public AndroidInterstitialAutoAdCallback(IDirichletInterstitialAutoAdListener listener) + : base("com.dirichlet.unity.DirichletUnityBridge$InterstitialAutoAdListener") + { + this.listener = listener; + } + + public void onError(string code, string message) + { + var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code; + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError(errorCode, message ?? string.Empty))); + } + + public void onAdShow() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow()); } + public void onAdClose() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClose()); } + public void onAdClick() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClick()); } + } + + private sealed class AndroidBannerAutoAdCallback : AndroidJavaProxy + { + private readonly IDirichletBannerAutoAdListener listener; + + public AndroidBannerAutoAdCallback(IDirichletBannerAutoAdListener listener) + : base("com.dirichlet.unity.DirichletUnityBridge$BannerAutoAdListener") + { + this.listener = listener; + } + + public void onError(string code, string message) + { + var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code; + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError(errorCode, message ?? string.Empty))); + } + + public void onAdShow() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow()); } + public void onAdClose() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClose()); } + public void onAdClick() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClick()); } + } + + private sealed class AndroidSplashAutoAdCallback : AndroidJavaProxy + { + private readonly IDirichletSplashAutoAdListener listener; + + public AndroidSplashAutoAdCallback(IDirichletSplashAutoAdListener listener) + : base("com.dirichlet.unity.DirichletUnityBridge$SplashAutoAdListener") + { + this.listener = listener; + } + + public void onError(string code, string message) + { + var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code; + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError(errorCode, message ?? string.Empty))); + } + + public void onAdShow() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow()); } + public void onAdClose() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClose()); } + public void onAdClick() { DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClick()); } + } + + private sealed class AndroidRewardVideoAutoAdCallback : AndroidJavaProxy + { + private readonly IDirichletRewardVideoAutoAdListener listener; + + public AndroidRewardVideoAutoAdCallback(IDirichletRewardVideoAutoAdListener listener) + : base("com.dirichlet.unity.DirichletUnityBridge$RewardVideoAutoAdListener") + { + this.listener = listener; + } + + public void onError(string code, string message) + { + var errorCode = string.IsNullOrEmpty(code) ? "android_error" : code; + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError(errorCode, message ?? string.Empty))); + } + + public void onAdShow() + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow()); + } + + public void onAdClose() + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClose()); + } + + public void onRewardVerify(bool rewardVerify, int rewardAmount, string rewardName, int code, string msg) + { + var args = new DirichletRewardVerificationEventArgs(rewardVerify, rewardAmount, rewardName ?? string.Empty, code, msg ?? string.Empty); + DirichletSdk.DispatchToUnityThread(() => listener?.OnRewardVerify(args)); + } + + public void onAdClick() + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnAdClick()); + } + } + } +#elif UNITY_IOS && !UNITY_EDITOR + internal sealed class IOSDirichletBridge : IDirichletPlatformBridge + { + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern bool DirichletMediationUnityBridge_Initialize( + string mediaId, string mediaKey, bool enableLog, string mediaName, + string gameChannel, bool shakeEnabled, bool allowIDFAAccess, string aTags); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern void DirichletMediationUnityBridge_RequestPermissionIfNeeded(); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern string DirichletMediationUnityBridge_GetSdkVersion(); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern string DirichletMediationUnityBridge_LoadRewardVideoAd(long spaceId, string extras); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern string DirichletMediationUnityBridge_LoadInterstitialAd(long spaceId, string extras); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern string DirichletMediationUnityBridge_LoadBannerAd(long spaceId, string extras); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern string DirichletMediationUnityBridge_LoadSplashAd(long spaceId, string extras); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern bool DirichletMediationUnityBridge_ShowAd(string handleId, string extras); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern void DirichletMediationUnityBridge_DestroyAd(string handleId); + + [System.Runtime.InteropServices.DllImport("__Internal")] + private static extern bool DirichletMediationUnityBridge_IsAdValid(string handleId); + + private readonly Dictionary loadCallbacks = new Dictionary(); + private readonly object loadCallbacksLock = new object(); + private static bool loadCallbackReceiverInitialized; + private static bool initCallbackReceiverInitialized; + private static readonly object initCallbackLock = new object(); + private static Action pendingInitSuccess; + private static Action pendingInitFailure; + + public void Initialize(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure) + { + if (options == null) + { + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("ios_invalid_options", "Initialization options cannot be null"))); + return; + } + + // Register callbacks for async result + lock (initCallbackLock) + { + pendingInitSuccess = onSuccess; + pendingInitFailure = onFailure; + } + + EnsureInitCallbackReceiver(); + + try + { + // iOS Mediation SDK uses async callback (aligned with Ad Unity implementation) + var started = DirichletMediationUnityBridge_Initialize( + options.GetAppIdString(), + options.MediaKey ?? string.Empty, + options.EnableLog, + options.MediaName ?? string.Empty, + options.Channel ?? string.Empty, + options.ShakeEnabled, + options.AllowIDFAAccess, + options.ATags ?? string.Empty); + + if (!started) + { + lock (initCallbackLock) + { + pendingInitSuccess = null; + pendingInitFailure = null; + } + + DirichletSdk.DispatchToUnityThread(() => + onFailure?.Invoke(new DirichletError("ios_init_rejected", "Initialization could not be started"))); + } + } + catch (Exception ex) + { + Debug.LogException(ex); + lock (initCallbackLock) + { + pendingInitSuccess = null; + pendingInitFailure = null; + } + + DirichletSdk.DispatchToUnityThread(() => + onFailure?.Invoke(new DirichletError("ios_exception", ex.Message))); + } + } + + public void RequestPermissionIfNeeded() + { + try + { + DirichletMediationUnityBridge_RequestPermissionIfNeeded(); + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] RequestPermissionIfNeeded failed: {ex.Message}"); + } + } + + public string GetSdkVersion() + { + try + { + return DirichletMediationUnityBridge_GetSdkVersion() ?? "ios-unknown"; + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] GetSdkVersion failed: {ex.Message}"); + return "ios-error"; + } + } + + public void LoadRewardVideoAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + LoadAdInternal(DirichletAdType.RewardVideo, request, onSuccess, onFailure); + } + + public bool ShowRewardVideoAd(DirichletPlatformAdHandle handle) + { + return ShowAdInternal(handle, null); + } + + public void LoadInterstitialAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + LoadAdInternal(DirichletAdType.Interstitial, request, onSuccess, onFailure); + } + + public bool ShowInterstitialAd(DirichletPlatformAdHandle handle) + { + return ShowAdInternal(handle, null); + } + + public void LoadBannerAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + LoadAdInternal(DirichletAdType.Banner, request, onSuccess, onFailure); + } + + public bool ShowBannerAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options) + { + return ShowAdInternal(handle, options); + } + + public void LoadSplashAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + LoadAdInternal(DirichletAdType.Splash, request, onSuccess, onFailure); + } + + public bool ShowSplashAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options) + { + return ShowAdInternal(handle, options); + } + + public void DestroyAd(DirichletPlatformAdHandle handle) + { + DestroyAdInternal(handle); + } + + public bool IsAdValid(DirichletPlatformAdHandle handle) + { + if (handle == null || string.IsNullOrEmpty(handle.DebugId)) + { + return false; + } + + try + { + return DirichletMediationUnityBridge_IsAdValid(handle.DebugId); + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] IsAdValid failed: {ex.Message}"); + return false; + } + } + + private void LoadAdInternal(DirichletAdType adType, DirichletAdRequest request, Action onSuccess, Action onFailure) + { + if (request == null) + { + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("invalid_request", "Request cannot be null"))); + return; + } + + try + { + var payload = request.ToBridgePayload(); + var callback = new IOSLoadCallback(this, null, () => { }, onFailure); + + string handleId; + var extrasJson = BuildJsonString(payload); + + switch (adType) + { + case DirichletAdType.RewardVideo: + handleId = DirichletMediationUnityBridge_LoadRewardVideoAd(request.SpaceId, extrasJson); + break; + case DirichletAdType.Interstitial: + handleId = DirichletMediationUnityBridge_LoadInterstitialAd(request.SpaceId, extrasJson); + break; + case DirichletAdType.Banner: + handleId = DirichletMediationUnityBridge_LoadBannerAd(request.SpaceId, extrasJson); + break; + case DirichletAdType.Splash: + handleId = DirichletMediationUnityBridge_LoadSplashAd(request.SpaceId, extrasJson); + break; + default: + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("unsupported_type", $"Unsupported ad type: {adType}"))); + return; + } + + if (string.IsNullOrEmpty(handleId)) + { + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("invalid_handle", "Bridge returned null handle"))); + return; + } + + // Create handle and setup callback + var handle = DirichletPlatformAdHandle.FromNative(handleId); + callback.SetHandle(handle); + callback.SetSuccessCallback(() => onSuccess?.Invoke(handle)); + + lock (loadCallbacksLock) + { + loadCallbacks[handleId] = callback; + } + + // Ensure load callback receiver is initialized + EnsureLoadCallbackReceiver(); + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] LoadAd failed: {ex.Message}"); + DirichletSdk.DispatchToUnityThread(() => onFailure?.Invoke(new DirichletError("ios_exception", ex.Message))); + } + } + + private bool ShowAdInternal(DirichletPlatformAdHandle handle, DirichletAdShowOptions options) + { + try + { + var payload = options?.ToBridgePayload(); + var extrasJson = BuildJsonString(payload); + return DirichletMediationUnityBridge_ShowAd(handle.DebugId, extrasJson); + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] ShowAd failed: {ex.Message}"); + return false; + } + } + + private void DestroyAdInternal(DirichletPlatformAdHandle handle) + { + try + { + DirichletMediationUnityBridge_DestroyAd(handle.DebugId); + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] DestroyAd failed: {ex.Message}"); + } + finally + { + RemoveLoadCallback(handle?.DebugId); + } + } + + private string BuildJsonString(Dictionary dictionary) + { + if (dictionary == null || dictionary.Count == 0) + { + return string.Empty; + } + + try + { + var jsonBuilder = new System.Text.StringBuilder(); + jsonBuilder.Append("{"); + var first = true; + + foreach (var kv in dictionary) + { + if (string.IsNullOrEmpty(kv.Key) || kv.Value == null) + { + continue; + } + + if (!first) + { + jsonBuilder.Append(","); + } + first = false; + + jsonBuilder.Append($"\"{kv.Key}\":"); + + if (kv.Value is string) + { + jsonBuilder.Append($"\"{kv.Value}\""); + } + else if (kv.Value is bool) + { + jsonBuilder.Append(((bool)kv.Value) ? "true" : "false"); + } + else + { + jsonBuilder.Append(kv.Value.ToString()); + } + } + + jsonBuilder.Append("}"); + return jsonBuilder.ToString(); + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] Failed to build json string: {ex.Message}"); + return string.Empty; + } + } + + private void RemoveLoadCallback(string handleId) + { + if (string.IsNullOrEmpty(handleId)) + { + return; + } + + lock (loadCallbacksLock) + { + loadCallbacks.Remove(handleId); + } + } + + private void EnsureLoadCallbackReceiver() + { + if (!DirichletSdk.IsUnityThread) + { + DirichletSdk.DispatchToUnityThread(EnsureLoadCallbackReceiver); + return; + } + + const string receiverName = "DirichletMediationIOSLoadCallbackReceiver"; + + if (loadCallbackReceiverInitialized) + { + var existing = GameObject.Find(receiverName); + if (existing != null) + { + return; + } + Debug.LogWarning("[DirichletMediation][iOS] LoadCallbackReceiver was destroyed, recreating..."); + loadCallbackReceiverInitialized = false; + } + + var host = new GameObject(receiverName) + { + hideFlags = HideFlags.HideAndDontSave + }; + UnityEngine.Object.DontDestroyOnLoad(host); + var receiver = host.AddComponent(); + receiver.bridge = this; + + loadCallbackReceiverInitialized = true; + } + + private void EnsureInitCallbackReceiver() + { + if (!DirichletSdk.IsUnityThread) + { + DirichletSdk.DispatchToUnityThread(EnsureInitCallbackReceiver); + return; + } + + const string receiverName = "DirichletMediationIOSInitCallbackReceiver"; + if (initCallbackReceiverInitialized) + { + var existing = GameObject.Find(receiverName); + if (existing != null) + { + return; + } + + Debug.LogWarning("[DirichletMediation][iOS] InitCallbackReceiver was destroyed, recreating..."); + initCallbackReceiverInitialized = false; + } + + var host = new GameObject(receiverName) + { + hideFlags = HideFlags.HideAndDontSave + }; + UnityEngine.Object.DontDestroyOnLoad(host); + var receiver = host.AddComponent(); + receiver.bridge = this; + + initCallbackReceiverInitialized = true; + } + + internal void HandleLoadCallback(string payload) + { + if (string.IsNullOrEmpty(payload)) + { + return; + } + + try + { + var message = JsonUtility.FromJson(payload); + if (message == null || string.IsNullOrEmpty(message.handle)) + { + return; + } + + IOSLoadCallback callback; + lock (loadCallbacksLock) + { + if (!loadCallbacks.TryGetValue(message.handle, out callback)) + { + Debug.LogWarning($"[DirichletMediation][iOS] No callback found for handle: {message.handle}"); + return; + } + } + + if (message.eventName == "load_success") + { + callback.OnSuccess(); + } + else if (message.eventName == "load_error") + { + var code = message.data?.code.ToString() ?? "unknown"; + var msg = message.data?.message ?? "Unknown error"; + callback.OnError(code, msg); + } + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] Failed to handle load callback: {ex.Message}\n{payload}"); + } + } + + internal void HandleInitCallback(string payload) + { + if (string.IsNullOrEmpty(payload)) + { + return; + } + + InitCallbackPayload message = null; + try + { + message = JsonUtility.FromJson(payload); + } + catch (Exception ex) + { + Debug.LogWarning($"[DirichletMediation][iOS] Failed to parse init callback: {ex.Message}\n{payload}"); + } + + var success = message?.success ?? false; + var data = message?.data; + var code = data?.code ?? -1; + var domain = data?.domain; + var description = data?.message; + + Action successCallback; + Action failureCallback; + + lock (initCallbackLock) + { + successCallback = pendingInitSuccess; + failureCallback = pendingInitFailure; + pendingInitSuccess = null; + pendingInitFailure = null; + } + + if (success) + { + var messageText = string.IsNullOrEmpty(description) ? "ios_mediation_bridge" : description; + var result = DirichletInitResult.Ok(messageText); + DirichletSdk.DispatchToUnityThread(() => successCallback?.Invoke(result)); + return; + } + + var errorCode = code > 0 ? $"ios_init_{code}" : "ios_init_failed"; + var errorMessage = string.IsNullOrEmpty(description) ? "Initialization failed" : description; + if (!string.IsNullOrEmpty(domain)) + { + errorMessage = $"{errorMessage} ({domain})"; + } + + var error = new DirichletError(errorCode, errorMessage); + DirichletSdk.DispatchToUnityThread(() => + { + if (failureCallback != null) + { + failureCallback(error); + } + else + { + Debug.LogWarning($"[DirichletMediation][iOS] Init failure received but no callback registered: {error}"); + } + }); + } + + [Serializable] + private class LoadCallbackPayload + { + public string handle; + public string eventName; + public string adType; + public LoadCallbackPayloadData data; + } + + [Serializable] + private class LoadCallbackPayloadData + { + public int code; + public string message; + } + + [Serializable] + private class InitCallbackPayload + { + public bool success; + public InitCallbackPayloadData data; + } + + [Serializable] + private class InitCallbackPayloadData + { + public int code; + public string message; + public string domain; + } + + private class IOSLoadCallbackReceiver : MonoBehaviour + { + public IOSDirichletBridge bridge; + + public void OnLoadCallback(string payload) + { + bridge?.HandleLoadCallback(payload); + } + } + + private class IOSInitCallbackReceiver : MonoBehaviour + { + public IOSDirichletBridge bridge; + + public void OnInitCallback(string payload) + { + bridge?.HandleInitCallback(payload); + } + } + + private sealed class IOSLoadCallback + { + private readonly IOSDirichletBridge owner; + private string handleId; + private Action success; + private readonly Action failure; + + public IOSLoadCallback(IOSDirichletBridge owner, string handleId, Action success, Action failure) + { + this.owner = owner; + this.handleId = handleId; + this.success = success; + this.failure = failure; + } + + public void SetHandle(DirichletPlatformAdHandle handle) + { + if (handle != null) + { + handleId = handle.DebugId; + } + } + + public void SetSuccessCallback(Action callback) + { + success = callback; + } + + public void OnSuccess() + { + owner.RemoveLoadCallback(handleId); + DirichletSdk.DispatchToUnityThread(() => success?.Invoke()); + } + + public void OnError(string code, string message) + { + owner.RemoveLoadCallback(handleId); + DirichletSdk.DispatchToUnityThread(() => failure?.Invoke(new DirichletError(code, message))); + } + } + + public void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener) + { + // iOS hasn't added this API yet + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("not_supported", "showRewardVideoAutoAd is not supported on iOS yet"))); + } + + public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener) + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("not_supported", "showInterstitialAutoAd is not supported on iOS yet"))); + } + + public void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener) + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("not_supported", "showBannerAutoAd is not supported on iOS yet"))); + } + + public void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener) + { + DirichletSdk.DispatchToUnityThread(() => listener?.OnError(new DirichletError("not_supported", "showSplashAutoAd is not supported on iOS yet"))); + } + + public void PreLoad(DirichletAdRequest request, int type) + { + Debug.LogWarning("[DirichletMediation][iOS] PreLoad is not supported on iOS yet"); + } + } +#else + internal sealed class NoopDirichletBridge : IDirichletPlatformBridge + { + public void Initialize(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure) + { + Debug.LogWarning("[Dirichlet] No platform bridge available (editor/unsupported platform)."); + DirichletSdk.DispatchToUnityThread(() => onSuccess?.Invoke(DirichletInitResult.Ok("noop"))); + } + + public void InitializeWithoutTap(DirichletPlatformInitOptions options, Action onSuccess, Action onFailure) + { + Debug.LogWarning("[Dirichlet] InitializeWithoutTap ignored on noop bridge."); + DirichletSdk.DispatchToUnityThread(() => onSuccess?.Invoke(DirichletInitResult.Ok("noop"))); + } + + public void UpdateConfig(DirichletPlatformInitOptions options) + { + Debug.Log("[Dirichlet] UpdateConfig ignored on noop bridge."); + } + + public void RequestPermissionIfNeeded() + { + Debug.Log("[Dirichlet] RequestPermissionIfNeeded ignored on noop bridge."); + } + + public string GetSdkVersion() => "noop"; + + private static DirichletPlatformAdHandle CreateStubHandle() + { + return DirichletPlatformAdHandle.CreateStub(); + } + + private static void LoadStubAd(DirichletPlatformAdHandle handle, Action onSuccess) + { + Debug.Log($"[Dirichlet] LoadAd noop for {handle.DebugId}"); + DirichletSdk.DispatchToUnityThread(() => onSuccess?.Invoke(handle)); + } + + public void LoadRewardVideoAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + var handle = CreateStubHandle(); + LoadStubAd(handle, onSuccess); + } + + public bool ShowRewardVideoAd(DirichletPlatformAdHandle handle) + { + Debug.Log($"[Dirichlet] ShowRewardAd noop for {handle.DebugId}"); + return true; + } + + public void LoadInterstitialAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + var handle = CreateStubHandle(); + LoadStubAd(handle, onSuccess); + } + + public bool ShowInterstitialAd(DirichletPlatformAdHandle handle) + { + Debug.Log($"[Dirichlet] ShowInterstitialAd noop for {handle.DebugId}"); + return true; + } + + public void LoadBannerAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + var handle = CreateStubHandle(); + LoadStubAd(handle, onSuccess); + } + + public bool ShowBannerAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options) + { + Debug.Log($"[Dirichlet] ShowBannerAd noop for {handle.DebugId}"); + return true; + } + + public void LoadSplashAd(DirichletAdRequest request, Action onSuccess, Action onFailure) + { + var handle = CreateStubHandle(); + LoadStubAd(handle, onSuccess); + } + + public bool ShowSplashAd(DirichletPlatformAdHandle handle, DirichletAdShowOptions options) + { + Debug.Log($"[Dirichlet] ShowSplashAd noop for {handle.DebugId}"); + return true; + } + + public void DestroyAd(DirichletPlatformAdHandle handle) + { + Debug.Log($"[Dirichlet] DestroyAd noop for {handle.DebugId}"); + } + + public bool IsAdValid(DirichletPlatformAdHandle handle) + { + Debug.Log($"[Dirichlet] IsAdValid noop for {handle?.DebugId}"); + return true; + } + + public void ShowRewardVideoAutoAd(DirichletAdRequest request, IDirichletRewardVideoAutoAdListener listener) + { + Debug.Log("[Dirichlet] ShowRewardVideoAutoAd noop"); + DirichletSdk.DispatchToUnityThread(() => + { + listener?.OnAdShow(); + listener?.OnRewardVerify(new DirichletRewardVerificationEventArgs(true, 10, "noop_reward", 0, "noop")); + listener?.OnAdClose(); + }); + } + + public void ShowInterstitialAutoAd(DirichletAdRequest request, IDirichletInterstitialAutoAdListener listener) + { + Debug.Log("[Dirichlet] ShowInterstitialAutoAd noop"); + DirichletSdk.DispatchToUnityThread(() => + { + listener?.OnAdShow(); + listener?.OnAdClose(); + }); + } + + public void ShowBannerAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletBannerAutoAdListener listener) + { + Debug.Log("[Dirichlet] ShowBannerAutoAd noop"); + DirichletSdk.DispatchToUnityThread(() => listener?.OnAdShow()); + } + + public void ShowSplashAutoAd(DirichletAdRequest request, DirichletAdShowOptions options, IDirichletSplashAutoAdListener listener) + { + Debug.Log("[Dirichlet] ShowSplashAutoAd noop"); + DirichletSdk.DispatchToUnityThread(() => + { + listener?.OnAdShow(); + listener?.OnAdClose(); + }); + } + + public void PreLoad(DirichletAdRequest request, int type) + { + Debug.Log($"[Dirichlet] PreLoad noop (type={type})"); + } + } +#endif + + #endregion +} + + diff --git a/Assets/DirichletMediation/Runtime/DirichletMediationSdk.cs.meta b/Assets/DirichletMediation/Runtime/DirichletMediationSdk.cs.meta new file mode 100644 index 0000000..30427e3 --- /dev/null +++ b/Assets/DirichletMediation/Runtime/DirichletMediationSdk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45ede85e8de28482f92f239b9e824766 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins.meta b/Assets/Plugins.meta new file mode 100644 index 0000000..f3d65c9 --- /dev/null +++ b/Assets/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 30b3bfe6f7c94fd2ba05c41c8a8ae1f1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android.meta b/Assets/Plugins/Android.meta new file mode 100644 index 0000000..e8f85df --- /dev/null +++ b/Assets/Plugins/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 652205868ad149ecae26404dc3f16554 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/AndroidManifest.xml b/Assets/Plugins/Android/AndroidManifest.xml new file mode 100644 index 0000000..ce30331 --- /dev/null +++ b/Assets/Plugins/Android/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets/Plugins/Android/AndroidManifest.xml.meta b/Assets/Plugins/Android/AndroidManifest.xml.meta new file mode 100644 index 0000000..cce2dda --- /dev/null +++ b/Assets/Plugins/Android/AndroidManifest.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3c972780d1f434d0cb5b25ce1dd8861b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation.meta b/Assets/Plugins/Android/DirichletMediation.meta new file mode 100644 index 0000000..3717ddb --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 228b690bf488f4593868f1ddf81e5a45 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/libs.meta b/Assets/Plugins/Android/DirichletMediation/libs.meta new file mode 100644 index 0000000..1cae7cc --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/libs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3733efcb61dd489fa8152ca50efff39a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar new file mode 100644 index 0000000..7c277fe Binary files /dev/null and b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar differ diff --git a/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar.meta b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar.meta new file mode 100644 index 0000000..443cf59 --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_CSJ_Adapter_4.2.5.0.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: c7970f095313643b682bb2d27b4ede2f +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar new file mode 100644 index 0000000..f00d661 Binary files /dev/null and b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar differ diff --git a/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar.meta b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar.meta new file mode 100644 index 0000000..d7f63c4 --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_GDT_Adapter_4.2.5.0.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 174b13ee51c7a444fb02d6f2dfdb894f +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar new file mode 100644 index 0000000..4868a04 Binary files /dev/null and b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar differ diff --git a/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar.meta b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar.meta new file mode 100644 index 0000000..685c79a --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_IQY_Adapter_4.2.5.0.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 514287dbd7374309a1b94a565a605659 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar new file mode 100644 index 0000000..6746cea Binary files /dev/null and b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar differ diff --git a/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar.meta b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar.meta new file mode 100644 index 0000000..ef81e3d --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/libs/DirichletAD_Mediation_4.2.5.0.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 4866e4d90c0ff47018ffd97469eace11 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/src.meta b/Assets/Plugins/Android/DirichletMediation/src.meta new file mode 100644 index 0000000..ab68a8e --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/src.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2d971358c2bfb48d0b3f792a83924f42 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/src/main.meta b/Assets/Plugins/Android/DirichletMediation/src/main.meta new file mode 100644 index 0000000..3fded90 --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/src/main.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d720fa5d414114f8cb2794a88c403f42 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/src/main/java.meta b/Assets/Plugins/Android/DirichletMediation/src/main/java.meta new file mode 100644 index 0000000..6b5d529 --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/src/main/java.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 93eb097a3680445f6a2f78d5eae1de63 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/src/main/java/com.meta b/Assets/Plugins/Android/DirichletMediation/src/main/java/com.meta new file mode 100644 index 0000000..fcbf4a9 --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/src/main/java/com.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 32384e1dfd1c1422e92cc2a15056870c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet.meta b/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet.meta new file mode 100644 index 0000000..251c4a9 --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ef8ea0067be0345e292de883a7496086 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity.meta b/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity.meta new file mode 100644 index 0000000..fedee16 --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 20ac9c2dde05f432da7fc53d70736c85 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java b/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java new file mode 100644 index 0000000..b73d162 --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java @@ -0,0 +1,1908 @@ +package com.dirichlet.unity; + +import android.app.Activity; +import android.app.Application; +import android.graphics.Color; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.FrameLayout; + +import com.tapsdk.tapad.group.AdNetworkType; +import com.tapsdk.tapad.group.DirichletAdConfig; +import com.tapsdk.tapad.group.DirichletAdManager; +import com.tapsdk.tapad.group.DirichletAdNative; +import com.tapsdk.tapad.group.DirichletAdRequest; +import com.tapsdk.tapad.group.DirichletSdk; +import com.tapsdk.tapad.group.ads.DirichletBannerAd; +import com.tapsdk.tapad.group.ads.DirichletInterstitialAd; +import com.tapsdk.tapad.group.ads.DirichletRewardVideoAd; +import com.tapsdk.tapad.group.ads.DirichletSplashAd; +import com.tapsdk.tapad.group.DirichletAdCustomController; +import com.unity3d.player.UnityPlayer; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.UUID; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Collections; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Unity bridge for Dirichlet Mediation SDK. + * + * This bridge provides a Unity-compatible interface that closely mirrors the native SDK API. + * The API design follows the pattern: + * - DirichletAdManager.get().createAdNative(context) -> loadXXXAd() methods + * - Ad objects returned from loadXXXAd() -> show() and destroy() methods + * + * Usage pattern: + * 1. Initialize SDK: initialize(...) + * 2. Load ad: loadRewardVideoAd(...) / loadInterstitialAd(...) / etc. + * 3. Show ad: showAd(handle, options) + * 4. Destroy ad: destroyAd(handle) + * + * @see com.tapsdk.tapad.group.DirichletAdNative + * @see com.tapsdk.tapad.group.DirichletAdManager + */ +@SuppressWarnings("unused") +public final class DirichletUnityBridge { + + private static final String TAG = "DirichletUnityBridge"; + private static final long INIT_TIMEOUT_MS = 5_000L; + + private static final String UNITY_CALLBACK_OBJECT = "DirichletMediationEventReceiver"; + private static final String UNITY_CALLBACK_METHOD = "OnNativeEvent"; + private static final String EVENT_SHOW = "show"; + private static final String EVENT_CLOSE = "close"; + private static final String EVENT_CLICK = "click"; + private static final String EVENT_REWARD = "reward"; + + private static final int TYPE_REWARD = 0; + private static final int TYPE_INTERSTITIAL = 1; + private static final int TYPE_BANNER = 2; + private static final int TYPE_SPLASH = 3; + private static final int TYPE_EXPRESS_FEED = 4; + private static final int TYPE_NATIVE_FEED = 5; + + /** + * Cache of ad entries mapping handle IDs to ad instances. + * This allows Unity to reference Java objects via string handles. + */ + private static final Map AD_CACHE = new ConcurrentHashMap<>(); + private static final DirichletAdCustomController UNITY_CUSTOM_CONTROLLER = new UnityCustomController(); + + /** + * Singleton DirichletAdNative instance for auto-ad caching. + * Must be created once and reused to preserve the internal ad cache (rewardVideoAdMap). + * Used specifically by showRewardVideoAutoAd to maintain cache across calls. + */ + private static volatile DirichletAdNative sAutoAdNative = null; + private static final Object sAutoAdNativeLock = new Object(); + + private static final Map REQUEST_BUILDER_METHODS = buildRequestMethodMap(); + + private DirichletUnityBridge() { + // Private constructor to prevent instantiation + } + + /** + * Listener interface for ad load callbacks. + * Mirrors the native SDK listener pattern. + */ + public interface LoadListener { + /** + * Called when ad load succeeds. + */ + void onSuccess(); + + /** + * Called when ad load fails. + * + * @param code Error code + * @param message Error message + */ + void onError(String code, String message); + } + + public static boolean initialize(String appId, + String channel, + String subChannel, + boolean enableLog, + String mediaName, + String mediaKey, + String tapClientId, + String dataJson, + boolean shakeEnabled) { + // Never block the Android main thread (Unity often runs game loop on it). + // The synchronous boolean return is best-effort only. + if (Looper.getMainLooper() == Looper.myLooper()) { + Log.w(TAG, "initialize called on main thread; falling back to async init to avoid ANR"); + initializeAsync(appId, channel, subChannel, enableLog, mediaName, mediaKey, tapClientId, dataJson, shakeEnabled, null); + return true; + } + + return performInitialization(appId, channel, subChannel, enableLog, mediaName, mediaKey, tapClientId, dataJson, shakeEnabled); + } + + /** + * Asynchronous initialization. This is the preferred entry point for Unity C# to avoid blocking the game loop. + * It reports completion via the provided listener. A timeout is enforced so Unity always receives a result. + */ + public static void initializeAsync(String appId, + String channel, + String subChannel, + boolean enableLog, + String mediaName, + String mediaKey, + String tapClientId, + String dataJson, + boolean shakeEnabled, + LoadListener listener) { + final Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + Log.e(TAG, "initializeAsync: Unity activity is null"); + if (listener != null) { + listener.onError("activity_null", "Unity activity is null"); + } + return; + } + + final Application application = activity.getApplication(); + final AtomicBoolean completed = new AtomicBoolean(false); + final Handler handler = new Handler(Looper.getMainLooper()); + + final Runnable timeoutTask = () -> { + if (listener == null) { + return; + } + if (completed.compareAndSet(false, true)) { + Log.w(TAG, "initializeAsync timed out waiting for callback"); + listener.onError("timeout", "initialize timed out"); + } + }; + handler.postDelayed(timeoutTask, INIT_TIMEOUT_MS); + + activity.runOnUiThread(() -> { + try { + long mediaId = safeParseLong(appId, 0L); + String dataPayload = mergeDataPayload(subChannel, dataJson); + DirichletAdConfig config = buildConfig(mediaId, channel, enableLog, mediaName, mediaKey, tapClientId, dataPayload, shakeEnabled); + + DirichletSdk.InitListener sdkListener = new DirichletSdk.InitListener() { + @Override + public void onInitSuccess() { + if (listener == null) { + return; + } + if (completed.compareAndSet(false, true)) { + handler.removeCallbacks(timeoutTask); + listener.onSuccess(); + } + } + + @Override + public void onInitFail(int code, String msg) { + if (listener == null) { + return; + } + if (completed.compareAndSet(false, true)) { + handler.removeCallbacks(timeoutTask); + listener.onError(String.valueOf(code), msg); + } + } + }; + + DirichletSdk.init(application, config, sdkListener); + } catch (Throwable t) { + Log.e(TAG, "initializeAsync error", t); + if (listener != null && completed.compareAndSet(false, true)) { + handler.removeCallbacks(timeoutTask); + listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName()); + } + } + }); + } + + private static boolean performInitialization(String appId, + String channel, + String subChannel, + boolean enableLog, + String mediaName, + String mediaKey, + String tapClientId, + String dataJson, + boolean shakeEnabled) { + final Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + Log.e(TAG, "initialize: Unity activity is null"); + return false; + } + + final Application application = activity.getApplication(); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean success = new AtomicBoolean(false); + final AtomicReference failureMessage = new AtomicReference<>(); + + activity.runOnUiThread(() -> { + try { + long mediaId = safeParseLong(appId, 0L); + String dataPayload = mergeDataPayload(subChannel, dataJson); + DirichletAdConfig config = buildConfig(mediaId, channel, enableLog, mediaName, mediaKey, tapClientId, dataPayload, shakeEnabled); + + DirichletSdk.InitListener listener = new DirichletSdk.InitListener() { + @Override + public void onInitSuccess() { + success.set(true); + latch.countDown(); + } + + @Override + public void onInitFail(int code, String msg) { + failureMessage.set("code=" + code + ", msg=" + msg); + latch.countDown(); + } + }; + + DirichletSdk.init(application, config, listener); + } catch (Throwable t) { + Log.e(TAG, "initialize error", t); + failureMessage.set(t.getMessage()); + latch.countDown(); + } + }); + + try { + if (!latch.await(INIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + Log.w(TAG, "initialize timed out waiting for callback"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + if (!success.get() && failureMessage.get() != null) { + Log.e(TAG, "initialize failed: " + failureMessage.get()); + } + + return success.get(); + } + + private static DirichletAdConfig buildConfig(long mediaId, + String channel, + boolean enableLog, + String mediaName, + String mediaKey, + String tapClientId, + String dataPayload, + boolean shakeEnabled) { + DirichletAdConfig.Builder builder = new DirichletAdConfig.Builder() + .withMediaId(mediaId) + .enableDebug(enableLog) + .shakeEnabled(shakeEnabled) + .withCustomController(UNITY_CUSTOM_CONTROLLER); + + if (!TextUtils.isEmpty(mediaName)) { + builder.withMediaName(mediaName); + } + + if (!TextUtils.isEmpty(mediaKey)) { + builder.withMediaKey(mediaKey); + } + + if (!TextUtils.isEmpty(channel)) { + builder.withGameChannel(channel); + } + + if (!TextUtils.isEmpty(tapClientId)) { + builder.withTapClientId(tapClientId); + } + + if (!TextUtils.isEmpty(dataPayload)) { + builder.withData(dataPayload); + } + + return builder.build(); + } + + private static Integer toInteger(Object value) { + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Long) { + return ((Long) value).intValue(); + } + if (value instanceof Double) { + return ((Double) value).intValue(); + } + if (value instanceof String) { + try { + return Integer.parseInt(((String) value).trim()); + } catch (NumberFormatException ignore) { + return null; + } + } + return null; + } + + private static void applyGenericBuilderValue(DirichletAdRequest.Builder builder, String normalizedKey, Object value) { + if (builder == null || normalizedKey == null || normalizedKey.isEmpty()) { + return; + } + + String methodName = REQUEST_BUILDER_METHODS.get(normalizedKey); + if (methodName == null) { + methodName = "with" + toPascalCase(normalizedKey); + } + + if (methodName == null || methodName.isEmpty()) { + return; + } + + if (!invokeBuilderWithSingleArg(builder, methodName, value)) { + if (!(value instanceof String)) { + invokeBuilderWithSingleArg(builder, methodName, String.valueOf(value)); + } + } + } + + private static boolean invokeBuilderWithSingleArg(Object target, String methodName, Object value) { + if (target == null || value == null || methodName == null || methodName.isEmpty()) { + return false; + } + + if (value instanceof Integer) { + int intValue = (Integer) value; + if (tryInvoke(target, methodName, int.class, intValue)) return true; + if (tryInvoke(target, methodName, Integer.class, intValue)) return true; + if (tryInvoke(target, methodName, long.class, (long) intValue)) return true; + if (tryInvoke(target, methodName, Long.class, (long) intValue)) return true; + } else if (value instanceof Long) { + long longValue = (Long) value; + if (tryInvoke(target, methodName, long.class, longValue)) return true; + if (tryInvoke(target, methodName, Long.class, longValue)) return true; + if (tryInvoke(target, methodName, int.class, (int) longValue)) return true; + if (tryInvoke(target, methodName, Integer.class, (int) longValue)) return true; + } else if (value instanceof Double) { + double doubleValue = (Double) value; + if (tryInvoke(target, methodName, double.class, doubleValue)) return true; + if (tryInvoke(target, methodName, Double.class, doubleValue)) return true; + if (tryInvoke(target, methodName, float.class, (float) doubleValue)) return true; + if (tryInvoke(target, methodName, Float.class, (float) doubleValue)) return true; + } else if (value instanceof Boolean) { + boolean boolValue = (Boolean) value; + if (tryInvoke(target, methodName, boolean.class, boolValue)) return true; + if (tryInvoke(target, methodName, Boolean.class, boolValue)) return true; + } else if (value instanceof String) { + String stringValue = ((String) value).trim(); + if (!stringValue.isEmpty()) { + try { + int parsed = Integer.parseInt(stringValue); + if (tryInvoke(target, methodName, int.class, parsed)) return true; + if (tryInvoke(target, methodName, Integer.class, parsed)) return true; + if (tryInvoke(target, methodName, long.class, (long) parsed)) return true; + if (tryInvoke(target, methodName, Long.class, (long) parsed)) return true; + } catch (NumberFormatException ignore) { + try { + long parsedLong = Long.parseLong(stringValue); + if (tryInvoke(target, methodName, long.class, parsedLong)) return true; + if (tryInvoke(target, methodName, Long.class, parsedLong)) return true; + } catch (NumberFormatException ignoredLong) { + try { + double parsedDouble = Double.parseDouble(stringValue); + if (tryInvoke(target, methodName, double.class, parsedDouble)) return true; + if (tryInvoke(target, methodName, Double.class, parsedDouble)) return true; + } catch (NumberFormatException ignoredDouble) { + // fall through to string invocation + } + } + } + if (tryInvoke(target, methodName, String.class, stringValue)) { + return true; + } + } + if (tryInvoke(target, methodName, String.class, stringValue)) { + return true; + } + return false; + } + + return tryInvoke(target, methodName, String.class, value.toString()); + } + + private static void invokeBuilderTwoInts(Object target, String methodName, int first, int second) { + if (target == null || methodName == null || methodName.isEmpty()) { + return; + } + + if (tryInvokeTwoArgs(target, methodName, int.class, int.class, first, second)) { + return; + } + + tryInvokeTwoArgs(target, methodName, Integer.class, Integer.class, first, second); + } + + private static boolean tryInvoke(Object target, String methodName, Class paramType, Object argument) { + try { + Method method = target.getClass().getMethod(methodName, paramType); + method.setAccessible(true); + method.invoke(target, argument); + return true; + } catch (NoSuchMethodException ignored) { + return false; + } catch (Throwable t) { + Log.w(TAG, "Failed to invoke " + methodName + " with " + paramType + ": " + t.getMessage()); + return false; + } + } + + private static boolean tryInvokeTwoArgs(Object target, String methodName, Class firstType, Class secondType, Object arg1, Object arg2) { + try { + Method method = target.getClass().getMethod(methodName, firstType, secondType); + method.setAccessible(true); + method.invoke(target, arg1, arg2); + return true; + } catch (NoSuchMethodException ignored) { + return false; + } catch (Throwable t) { + Log.w(TAG, "Failed to invoke " + methodName + " with two args: " + t.getMessage()); + return false; + } + } + + private static String toPascalCase(String key) { + if (key == null || key.isEmpty()) { + return ""; + } + + StringBuilder builder = new StringBuilder(key.length()); + boolean capitalizeNext = true; + for (int i = 0; i < key.length(); i++) { + char ch = key.charAt(i); + if (Character.isLetterOrDigit(ch)) { + if (capitalizeNext) { + builder.append(Character.toUpperCase(ch)); + capitalizeNext = false; + } else { + builder.append(ch); + } + } else { + capitalizeNext = true; + } + } + + return builder.toString(); + } + + private static JSONObject cloneJson(JSONObject value) { + if (value == null) { + return null; + } + try { + return new JSONObject(value.toString()); + } catch (Exception ignored) { + return null; + } + } + + private static void applyAdOptionSetters(AdEntry entry, JSONObject options) { + if (entry == null || options == null) { + return; + } + + Object target = resolveAdObject(entry); + if (target == null) { + return; + } + + Iterator keys = options.keys(); + while (keys.hasNext()) { + String key = keys.next(); + if (key == null) { + continue; + } + + String normalized = key.trim().toLowerCase(Locale.US); + if ("banner_baseline".equals(normalized) || "banner_offset".equals(normalized)) { + continue; // handled explicitly in show logic + } + + Object value = options.opt(key); + if (value == null || JSONObject.NULL.equals(value)) { + continue; + } + + String methodName = "set" + toPascalCase(normalized); + invokeBuilderWithSingleArg(target, methodName, value); + } + } + + private static Object resolveAdObject(AdEntry entry) { + return entry != null ? entry.getAdObject() : null; + } + + public static void requestPermissionIfNeeded() { + final Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + Log.w(TAG, "requestPermissionIfNeeded: activity is null"); + return; + } + + activity.runOnUiThread(() -> DirichletAdManager.get().requestPermissionIfNecessary(activity)); + } + + public static String getSdkVersion() { + try { + return String.valueOf(DirichletSdk.getVersion()); + } catch (Throwable t) { + Log.w(TAG, "getSdkVersion failed", t); + return "android-error"; + } + } + + /** + * Loads a reward video ad, matching the native SDK pattern. + * This follows DirichletAdNative.loadRewardVideoAd() from the native SDK. + * + * @param extras Request parameters as JSON (must include space_id) + * @param listener Callback for load success/failure + * @return Handle ID for the ad instance (can be used for show/destroy operations) + */ + public static String loadRewardVideoAd(JSONObject extras, LoadListener listener) { + return loadAdInternal(TYPE_REWARD, extras, listener); + } + + /** + * Loads an interstitial ad, matching the native SDK pattern. + * This follows DirichletAdNative.loadInterstitialAd() from the native SDK. + * + * @param extras Request parameters as JSON (must include space_id) + * @param listener Callback for load success/failure + * @return Handle ID for the ad instance (can be used for show/destroy operations) + */ + public static String loadInterstitialAd(JSONObject extras, LoadListener listener) { + return loadAdInternal(TYPE_INTERSTITIAL, extras, listener); + } + + /** + * Loads a banner ad, matching the native SDK pattern. + * This follows DirichletAdNative.loadBannerAd() from the native SDK. + * + * @param extras Request parameters as JSON (must include space_id) + * @param listener Callback for load success/failure + * @return Handle ID for the ad instance (can be used for show/destroy operations) + */ + public static String loadBannerAd(JSONObject extras, LoadListener listener) { + return loadAdInternal(TYPE_BANNER, extras, listener); + } + + /** + * Loads a splash ad, matching the native SDK pattern. + * This follows DirichletAdNative.loadSplashAd() from the native SDK. + * + * @param extras Request parameters as JSON (must include space_id) + * @param listener Callback for load success/failure + * @return Handle ID for the ad instance (can be used for show/destroy operations) + */ + public static String loadSplashAd(JSONObject extras, LoadListener listener) { + return loadAdInternal(TYPE_SPLASH, extras, listener); + } + + /** + * Listener interface for auto interstitial ad callbacks. + * Mirrors DirichletAdNative.InterstitialAutoAdListener from the native SDK. + */ + public interface InterstitialAutoAdListener { + void onError(String code, String message); + void onAdShow(); + void onAdClose(); + void onAdClick(); + } + + /** + * Listener interface for auto banner ad callbacks. + * Mirrors DirichletAdNative.BannerAutoAdListener from the native SDK. + */ + public interface BannerAutoAdListener { + void onError(String code, String message); + void onAdShow(); + void onAdClose(); + void onAdClick(); + } + + /** + * Listener interface for auto splash ad callbacks. + * Mirrors DirichletAdNative.SplashAutoAdListener from the native SDK. + */ + public interface SplashAutoAdListener { + void onError(String code, String message); + void onAdShow(); + void onAdClose(); + void onAdClick(); + } + + /** + * Listener interface for auto reward video ad callbacks. + * This combines load and show callbacks into a single interface. + * Mirrors DirichletAdNative.RewardVideoAutoAdListener from the native SDK. + */ + public interface RewardVideoAutoAdListener { + /** + * Called when ad fails to load or show. + * + * @param code Error code + * @param message Error message + */ + void onError(String code, String message); + + /** + * Called when ad is shown to the user. + */ + void onAdShow(); + + /** + * Called when ad is closed. + */ + void onAdClose(); + + /** + * Called when reward verification is completed. + * + * @param rewardVerify Whether the reward was verified + * @param rewardAmount Reward amount + * @param rewardName Reward name + * @param code Verification code + * @param msg Verification message + */ + void onRewardVerify(boolean rewardVerify, int rewardAmount, String rewardName, int code, String msg); + + /** + * Called when ad is clicked. + */ + void onAdClick(); + } + + /** + * Shows a reward video ad with automatic load-and-show logic. + * This follows DirichletAdNative.showRewardVideoAutoAd() from the native SDK. + * + * The method will: + * 1. Show cached ad immediately if available and valid + * 2. Load a new ad in the background for next time + * 3. If no cached ad, wait for load and then show + * + * @param extras Request parameters as JSON (must include space_id) + * @param listener Callback for all ad events (load/show/close/reward/click/error) + */ + public static void showRewardVideoAutoAd(JSONObject extras, RewardVideoAutoAdListener listener) { + Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + if (listener != null) { + listener.onError("activity_null", "Unity activity is null"); + } + return; + } + + if (extras == null) { + if (listener != null) { + listener.onError("invalid_request", "Request extras cannot be null, must include space_id"); + } + return; + } + + long spaceId = extras.optLong("space_id", 0L); + if (spaceId <= 0) { + if (listener != null) { + listener.onError("invalid_space_id", "space_id must be provided and greater than zero in extras"); + } + return; + } + + activity.runOnUiThread(() -> { + try { + // Use singleton to preserve cache (rewardVideoAdMap) across calls + DirichletAdNative adNative = getOrCreateAutoAdNative(activity); + DirichletAdRequest.Builder builder = new DirichletAdRequest.Builder().withSpaceId(spaceId); + + // Apply optional request parameters + String userId = extras.optString("user_id", null); + if (userId != null && !userId.isEmpty()) { + builder.withUserId(userId); + } + + String rewardName = extras.optString("reward_name", null); + if (rewardName != null && !rewardName.isEmpty()) { + builder.withRewardName(rewardName); + } + + int rewardAmount = extras.optInt("reward_amount", 0); + if (rewardAmount > 0) { + builder.withRewardAmount(rewardAmount); + } + + String query = extras.optString("query", null); + if (query != null && !query.isEmpty()) { + builder.withQuery(query); + } + + String extra1 = extras.optString("extra1", null); + if (extra1 != null && !extra1.isEmpty()) { + builder.withExtra1(extra1); + } + + DirichletAdRequest request = builder.build(); + + // Create the native listener that bridges to Unity callbacks + DirichletAdNative.RewardVideoAutoAdListener nativeListener = new DirichletAdNative.RewardVideoAutoAdListener() { + @Override + public void onError(int code, String message) { + if (listener != null) { + listener.onError(String.valueOf(code), message); + } + } + + @Override + public void onAdShow() { + if (listener != null) { + listener.onAdShow(); + } + } + + @Override + public void onAdClose() { + if (listener != null) { + listener.onAdClose(); + } + } + + @Override + public void onRewardVerify(boolean rewardVerify, int rewardAmount, String rewardName, int code, String msg) { + if (listener != null) { + listener.onRewardVerify(rewardVerify, rewardAmount, rewardName, code, msg); + } + } + + @Override + public void onAdClick() { + if (listener != null) { + listener.onAdClick(); + } + } + }; + + adNative.showRewardVideoAutoAd(request, activity, nativeListener); + } catch (Throwable t) { + Log.e(TAG, "showRewardVideoAutoAd exception", t); + if (listener != null) { + listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName()); + } + } + }); + } + + /** + * Shows an interstitial ad with automatic load-and-show logic. + * Mirrors DirichletAdNative.showInterstitialAutoAd() from the native SDK. + */ + public static void showInterstitialAutoAd(JSONObject extras, InterstitialAutoAdListener listener) { + Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + if (listener != null) { + listener.onError("activity_null", "Unity activity is null"); + } + return; + } + + if (!validateAutoAdSpaceId(extras, listener == null ? null : (code, message) -> listener.onError(code, message))) { + return; + } + + activity.runOnUiThread(() -> { + try { + DirichletAdNative adNative = getOrCreateAutoAdNative(activity); + DirichletAdRequest request = buildAutoAdRequest(extras); + + DirichletAdNative.InterstitialAutoAdListener nativeListener = new DirichletAdNative.InterstitialAutoAdListener() { + @Override + public void onError(int code, String message) { + if (listener != null) listener.onError(String.valueOf(code), message); + } + @Override public void onAdShow() { if (listener != null) listener.onAdShow(); } + @Override public void onAdClose() { if (listener != null) listener.onAdClose(); } + @Override public void onAdClick() { if (listener != null) listener.onAdClick(); } + }; + + adNative.showInterstitialAutoAd(request, activity, nativeListener); + } catch (Throwable t) { + Log.e(TAG, "showInterstitialAutoAd exception", t); + if (listener != null) { + listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName()); + } + } + }); + } + + /** + * Shows a banner ad with automatic load + rotation logic. + * Mirrors DirichletAdNative.showBannerAutoAd() from the native SDK. + * The container is created internally based on options (banner_baseline, banner_offset). + */ + public static void showBannerAutoAd(JSONObject extras, JSONObject options, BannerAutoAdListener listener) { + Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + if (listener != null) { + listener.onError("activity_null", "Unity activity is null"); + } + return; + } + + if (!validateAutoAdSpaceId(extras, listener == null ? null : (code, message) -> listener.onError(code, message))) { + return; + } + + activity.runOnUiThread(() -> { + try { + DirichletAdNative adNative = getOrCreateAutoAdNative(activity); + DirichletAdRequest request = buildAutoAdRequest(extras); + + int baseline = options != null ? options.optInt("banner_baseline", 1) : 1; + int offset = options != null ? options.optInt("banner_offset", 0) : 0; + FrameLayout container = attachBannerAutoContainer(activity, baseline, offset); + if (container == null) { + if (listener != null) listener.onError("attach_failed", "Failed to attach banner auto container"); + return; + } + + DirichletAdNative.BannerAutoAdListener nativeListener = new DirichletAdNative.BannerAutoAdListener() { + @Override + public void onError(int code, String message) { + if (listener != null) listener.onError(String.valueOf(code), message); + } + @Override public void onAdShow() { if (listener != null) listener.onAdShow(); } + @Override + public void onAdClose() { + detachBannerAutoContainer(); + if (listener != null) listener.onAdClose(); + } + @Override public void onAdClick() { if (listener != null) listener.onAdClick(); } + }; + + adNative.showBannerAutoAd(request, container, nativeListener); + } catch (Throwable t) { + Log.e(TAG, "showBannerAutoAd exception", t); + detachBannerAutoContainer(); + if (listener != null) { + listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName()); + } + } + }); + } + + /** + * Shows a splash ad with automatic load logic. + * Mirrors DirichletAdNative.showSplashAutoAd() from the native SDK. + * The container is a fullscreen overlay created internally. + */ + public static void showSplashAutoAd(JSONObject extras, JSONObject options, SplashAutoAdListener listener) { + Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + if (listener != null) { + listener.onError("activity_null", "Unity activity is null"); + } + return; + } + + if (!validateAutoAdSpaceId(extras, listener == null ? null : (code, message) -> listener.onError(code, message))) { + return; + } + + activity.runOnUiThread(() -> { + try { + DirichletAdNative adNative = getOrCreateAutoAdNative(activity); + DirichletAdRequest request = buildAutoAdRequest(extras); + + FrameLayout container = attachSplashAutoContainer(activity); + if (container == null) { + if (listener != null) listener.onError("attach_failed", "Failed to attach splash auto container"); + return; + } + + DirichletAdNative.SplashAutoAdListener nativeListener = new DirichletAdNative.SplashAutoAdListener() { + @Override + public void onError(int code, String message) { + detachSplashAutoContainer(); + if (listener != null) listener.onError(String.valueOf(code), message); + } + @Override public void onAdShow() { if (listener != null) listener.onAdShow(); } + @Override + public void onAdClose() { + detachSplashAutoContainer(); + if (listener != null) listener.onAdClose(); + } + @Override public void onAdClick() { if (listener != null) listener.onAdClick(); } + }; + + adNative.showSplashAutoAd(request, container, nativeListener); + } catch (Throwable t) { + Log.e(TAG, "showSplashAutoAd exception", t); + detachSplashAutoContainer(); + if (listener != null) { + listener.onError("exception", t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName()); + } + } + }); + } + + /** + * Preloads an ad for later auto-show. Mirrors DirichletAdNative.preLoad(). + * + * @param extras Request parameters as JSON (must include space_id) + * @param type Ad type code; native SDK currently only handles type=3 (reward video) + */ + public static void preLoad(JSONObject extras, int type) { + Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + Log.w(TAG, "preLoad: activity null"); + return; + } + + if (extras == null) { + Log.w(TAG, "preLoad: extras null"); + return; + } + + long spaceId = extras.optLong("space_id", 0L); + if (spaceId <= 0) { + Log.w(TAG, "preLoad: invalid space_id"); + return; + } + + activity.runOnUiThread(() -> { + try { + DirichletAdNative adNative = getOrCreateAutoAdNative(activity); + DirichletAdRequest request = buildAutoAdRequest(extras); + adNative.preLoad(request, type); + } catch (Throwable t) { + Log.e(TAG, "preLoad exception", t); + } + }); + } + + /** + * Builds a DirichletAdRequest from JSON extras for auto-ad calls. + * Supports the same keys as buildRequest (banner uses slide_internal, splash uses express size). + */ + private static DirichletAdRequest buildAutoAdRequest(JSONObject extras) { + long spaceId = extras.optLong("space_id", 0L); + DirichletAdRequest.Builder builder = new DirichletAdRequest.Builder().withSpaceId(spaceId); + + Integer expressWidth = null; + Integer expressHeight = null; + Integer expressImageWidth = null; + Integer expressImageHeight = null; + + Iterator keys = extras.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object value = extras.opt(key); + if (value == null || JSONObject.NULL.equals(value)) { + continue; + } + + String normalizedKey = key == null ? "" : key.trim().toLowerCase(Locale.US); + switch (normalizedKey) { + case "space_id": + continue; + case "express_width": + case "express_view_width": + expressWidth = toInteger(value); + break; + case "express_height": + case "express_view_height": + expressHeight = toInteger(value); + break; + case "express_image_width": + expressImageWidth = toInteger(value); + break; + case "express_image_height": + expressImageHeight = toInteger(value); + break; + default: + applyGenericBuilderValue(builder, normalizedKey, value); + break; + } + } + + if (expressWidth != null || expressHeight != null) { + int width = expressWidth != null ? expressWidth : -1; + int height = expressHeight != null ? expressHeight : -1; + invokeBuilderTwoInts(builder, "withExpressViewAcceptedSize", width, height); + } + + if (expressImageWidth != null || expressImageHeight != null) { + int width = expressImageWidth != null ? expressImageWidth : -1; + int height = expressImageHeight != null ? expressImageHeight : -1; + invokeBuilderTwoInts(builder, "withExpressImageAcceptedSize", width, height); + } + + return builder.build(); + } + + private interface AutoErrorReporter { + void report(String code, String message); + } + + private static boolean validateAutoAdSpaceId(JSONObject extras, AutoErrorReporter reporter) { + if (extras == null) { + if (reporter != null) reporter.report("invalid_request", "Request extras cannot be null, must include space_id"); + return false; + } + long spaceId = extras.optLong("space_id", 0L); + if (spaceId <= 0) { + if (reporter != null) reporter.report("invalid_space_id", "space_id must be provided and greater than zero in extras"); + return false; + } + return true; + } + + private static volatile FrameLayout sBannerAutoContainer = null; + private static volatile FrameLayout sSplashAutoContainer = null; + + private static FrameLayout attachBannerAutoContainer(Activity activity, int baseline, int offset) { + if (activity == null) return null; + + ViewGroup root = activity.findViewById(android.R.id.content); + if (root == null) return null; + + detachBannerAutoContainer(); + + FrameLayout container = new FrameLayout(activity); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT); + params.gravity = baseline == 0 ? (Gravity.TOP | Gravity.CENTER_HORIZONTAL) : (Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + if (baseline == 0) { + params.topMargin = Math.max(0, offset); + } else { + params.bottomMargin = Math.max(0, offset); + } + container.setLayoutParams(params); + container.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + root.addView(container); + sBannerAutoContainer = container; + return container; + } + + private static void detachBannerAutoContainer() { + FrameLayout container = sBannerAutoContainer; + if (container == null) return; + sBannerAutoContainer = null; + ViewParent parent = container.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(container); + } + } + + private static FrameLayout attachSplashAutoContainer(Activity activity) { + if (activity == null) return null; + + ViewGroup root = activity.findViewById(android.R.id.content); + if (root == null) return null; + + detachSplashAutoContainer(); + + FrameLayout overlay = new FrameLayout(activity); + overlay.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + overlay.setClickable(true); + overlay.setFocusable(true); + overlay.setBackgroundColor(Color.BLACK); + overlay.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + root.addView(overlay); + sSplashAutoContainer = overlay; + return overlay; + } + + private static void detachSplashAutoContainer() { + FrameLayout container = sSplashAutoContainer; + if (container == null) return; + sSplashAutoContainer = null; + ViewParent parent = container.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(container); + } + } + + /** + * Gets or creates a singleton DirichletAdNative instance for auto-ad caching. + * The cache (rewardVideoAdMap) is stored as an instance variable in DirichletAdNativeImpl, + * so we must reuse the same instance to preserve cached ads across showRewardVideoAutoAd calls. + * + * @param activity The current activity context + * @return The singleton DirichletAdNative instance + */ + private static DirichletAdNative getOrCreateAutoAdNative(Activity activity) { + if (sAutoAdNative == null) { + synchronized (sAutoAdNativeLock) { + if (sAutoAdNative == null) { + sAutoAdNative = DirichletAdManager.get().createAdNative(activity); + Log.d(TAG, "Created singleton DirichletAdNative for auto-ad caching"); + } + } + } + return sAutoAdNative; + } + + /** + * Internal method to load an ad of the specified type. + * Creates an AdEntry, stores it in cache, and initiates the load process. + */ + private static String loadAdInternal(int adType, JSONObject extras, LoadListener listener) { + String handle = UUID.randomUUID().toString(); + AdEntry entry = new AdEntry(handle, adType); + // Only add to cache after validation passes + // If loadAdWithEntry fails early, entry won't be in cache + loadAdWithEntry(entry, extras, listener); + return handle; + } + + /** + * Internal method to perform the actual ad loading using DirichletAdNative. + * This matches the native SDK pattern: DirichletAdManager.get().createAdNative(context) + */ + private static void loadAdWithEntry(AdEntry entry, JSONObject extras, LoadListener listener) { + Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + if (listener != null) { + listener.onError("activity_null", "Unity activity is null"); + } + return; + } + + // Validate extras before adding to cache + try { + if (extras == null) { + if (listener != null) { + listener.onError("invalid_request", "Request extras cannot be null, must include space_id"); + } + return; + } + + long spaceId = extras.optLong("space_id", 0L); + if (spaceId <= 0) { + if (listener != null) { + listener.onError("invalid_space_id", "space_id must be provided and greater than zero in extras"); + } + return; + } + } catch (Exception e) { + Log.e(TAG, "loadAd validation error", e); + if (listener != null) { + listener.onError("invalid_request", "Failed to validate request: " + e.getMessage()); + } + return; + } + + // Only add to cache after validation passes + AD_CACHE.put(entry.handle, entry); + + activity.runOnUiThread(() -> { + try { + // Create AdNative instance, matching DirichletAdManager.get().createAdNative(context) + DirichletAdNative adNative = DirichletAdManager.get().createAdNative(activity); + DirichletAdRequest request = buildRequest(entry, extras); + + switch (entry.type) { + case TYPE_REWARD: + adNative.loadRewardVideoAd(request, new DirichletAdNative.RewardVideoAdListener() { + @Override + public void onRewardVideoAdLoad(DirichletRewardVideoAd rewardVideoAd) { + entry.rewardAd = rewardVideoAd; + attachRewardInteraction(entry); + if (listener != null) { + listener.onSuccess(); + } + } + + @Override + public void onError(int code, String message) { + if (listener != null) { + listener.onError(String.valueOf(code), message); + } + } + }); + break; + case TYPE_INTERSTITIAL: + adNative.loadInterstitialAd(request, new DirichletAdNative.InterstitialAdListener() { + @Override + public void onInterstitialAdLoad(DirichletInterstitialAd interstitialAd) { + entry.interstitialAd = interstitialAd; + attachInterstitialInteraction(entry); + if (listener != null) { + listener.onSuccess(); + } + } + + @Override + public void onError(int code, String message) { + if (listener != null) { + listener.onError(String.valueOf(code), message); + } + } + }); + break; + case TYPE_BANNER: + adNative.loadBannerAd(request, new DirichletAdNative.BannerAdListener() { + @Override + public void onBannerAdLoad(DirichletBannerAd bannerAd) { + entry.bannerAd = bannerAd; + attachBannerInteraction(entry); + if (listener != null) { + listener.onSuccess(); + } + } + + @Override + public void onError(int code, String message) { + if (listener != null) { + listener.onError(String.valueOf(code), message); + } + } + }); + break; + case TYPE_SPLASH: + adNative.loadSplashAd(request, new DirichletAdNative.SplashAdListener() { + @Override + public void onSplashAdLoad(DirichletSplashAd splashAd) { + entry.splashAd = splashAd; + attachSplashInteraction(entry); + if (listener != null) { + listener.onSuccess(); + } + } + + @Override + public void onError(int code, String message) { + if (listener != null) { + listener.onError(String.valueOf(code), message); + } + } + }); + break; + default: + if (listener != null) { + listener.onError("unsupported_type", "Unsupported ad type: " + entry.type); + } + break; + } + } catch (IllegalArgumentException e) { + // Remove entry from cache on validation failure + AD_CACHE.remove(entry.handle); + Log.e(TAG, "loadAd validation failed", e); + if (listener != null) { + listener.onError("invalid_request", e.getMessage()); + } + } catch (Throwable t) { + // Remove entry from cache on other exceptions + AD_CACHE.remove(entry.handle); + Log.e(TAG, "loadAd exception", t); + if (listener != null) { + String errorCode = t instanceof IllegalArgumentException ? "invalid_request" : "exception"; + listener.onError(errorCode, t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName()); + } + } + }); + } + + /** + * Shows an ad using the handle returned from loadRewardVideoAd/loadInterstitialAd/etc. + * This matches the native SDK pattern where ad objects have show() methods. + * + * @param handle The handle ID returned from the load method + * @param options Show options (e.g., banner alignment, offset) + * @return true if the ad was shown successfully, false otherwise + */ + public static boolean showAd(String handle, JSONObject options) { + AdEntry entry = AD_CACHE.get(handle); + if (entry == null) { + Log.w(TAG, "showAd: handle not found " + handle); + return false; + } + + Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + Log.w(TAG, "showAd: activity null"); + return false; + } + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean result = new AtomicBoolean(false); + + activity.runOnUiThread(() -> { + try { + JSONObject localOptions = cloneJson(options); + applyAdOptionSetters(entry, localOptions); + + switch (entry.type) { + case TYPE_REWARD: + if (entry.rewardAd != null) { + entry.rewardAd.showRewardVideoAd(activity); + result.set(true); + } + break; + case TYPE_INTERSTITIAL: + if (entry.interstitialAd != null) { + entry.interstitialAd.show(activity); + result.set(true); + } + break; + case TYPE_BANNER: + if (entry.bannerAd != null) { + int baseline = localOptions != null ? localOptions.optInt("banner_baseline", 1) : 1; + int offset = localOptions != null ? localOptions.optInt("banner_offset", 0) : 0; + entry.bannerAd.show(activity, baseline, offset); + // Hardware acceleration will be enabled in onAdShow callback + // when the view is actually added to the hierarchy + result.set(true); + } + break; + case TYPE_SPLASH: + if (entry.splashAd != null) { + FrameLayout container = attachSplashContainer(activity, entry); + if (container != null) { + attachSplashListener(activity, entry); + entry.splashAd.show(container); + result.set(true); + } else { + Log.w(TAG, "showAd: failed to attach splash container"); + } + } + break; + default: + Log.w(TAG, "showAd: unsupported type " + entry.type); + break; + } + } catch (Throwable t) { + Log.e(TAG, "showAd exception", t); + detachSplashContainer(entry); + } finally { + latch.countDown(); + } + }); + + try { + latch.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return result.get(); + } + + private static FrameLayout attachSplashContainer(Activity activity, AdEntry entry) { + if (activity == null || entry == null) { + return null; + } + + ViewGroup root = activity.findViewById(android.R.id.content); + if (root == null) { + Log.w(TAG, "attachSplashContainer: root content view is null"); + return null; + } + + detachSplashContainer(entry); + + FrameLayout overlay = new FrameLayout(activity); + FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT); + overlay.setLayoutParams(overlayParams); + overlay.setClickable(true); + overlay.setFocusable(true); + overlay.setBackgroundColor(Color.BLACK); + // Enable hardware acceleration layer for better video playback and rounded corners rendering + overlay.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + int width = entry.splashWidth > 0 ? entry.splashWidth : FrameLayout.LayoutParams.MATCH_PARENT; + int height = entry.splashHeight > 0 ? entry.splashHeight : FrameLayout.LayoutParams.MATCH_PARENT; + FrameLayout.LayoutParams slotParams = new FrameLayout.LayoutParams(width, height); + slotParams.gravity = Gravity.CENTER; + + FrameLayout slot = new FrameLayout(activity); + slot.setLayoutParams(slotParams); + // Enable hardware acceleration layer for ad content container + slot.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + overlay.addView(slot); + root.addView(overlay); + entry.splashContainer = overlay; + return slot; + } + + private static void attachSplashListener(Activity activity, AdEntry entry) { + if (entry == null || entry.splashAd == null) { + return; + } + + entry.splashAd.setSplashInteractionListener(new DirichletSplashAd.AdInteractionListener() { + @Override + public void onAdClick() { + // no-op for Unity bridge; host app can listen via native SDK logs if needed + } + + @Override + public void onAdShow() { + // no-op + } + + @Override + public void onAdClose() { + if (activity != null) { + activity.runOnUiThread(() -> detachSplashContainer(entry)); + } else { + detachSplashContainer(entry); + } + } + }); + } + + private static void detachSplashContainer(AdEntry entry) { + if (entry == null) { + return; + } + + FrameLayout container = entry.splashContainer; + if (container == null) { + return; + } + + entry.splashContainer = null; + ViewParent parent = container.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(container); + } + } + + /** + * Checks if an ad is still valid and can be shown. + * This should be called before show() to ensure the ad hasn't expired. + * + * @param handle The handle ID returned from the load method + * @return true if the ad is valid and can be shown, false otherwise + */ + public static boolean isAdValid(String handle) { + AdEntry entry = AD_CACHE.get(handle); + if (entry == null) { + Log.w(TAG, "isAdValid: handle not found " + handle); + return false; + } + + try { + switch (entry.type) { + case TYPE_REWARD: + return entry.rewardAd != null && entry.rewardAd.isValid(); + case TYPE_INTERSTITIAL: + return entry.interstitialAd != null && entry.interstitialAd.isValid(); + case TYPE_BANNER: + return entry.bannerAd != null; + case TYPE_SPLASH: + return entry.splashAd != null; + default: + return false; + } + } catch (Throwable t) { + Log.w(TAG, "isAdValid exception", t); + return false; + } + } + + /** + * Destroys an ad instance and releases resources. + * This matches the native SDK pattern where ad objects have destroy() methods. + * + * @param handle The handle ID returned from the load method + */ + public static void destroyAd(String handle) { + AdEntry entry = AD_CACHE.remove(handle); + if (entry == null) { + return; + } + + Activity activity = UnityPlayer.currentActivity; + if (activity == null) { + entry.destroy(); + return; + } + + activity.runOnUiThread(entry::destroy); + } + + /** + * Builds a DirichletAdRequest from JSON extras. + * Note: Validation should be done before calling this method. + * This method assumes extras is not null and contains valid space_id. + */ + private static DirichletAdRequest buildRequest(AdEntry entry, JSONObject extras) { + // Validation already done in loadAdWithEntry, but add defensive check + if (extras == null) { + throw new IllegalArgumentException("extras cannot be null, must include space_id"); + } + + long spaceId = extras.optLong("space_id", 0L); + if (spaceId <= 0) { + throw new IllegalArgumentException("space_id must be provided and greater than zero in extras"); + } + + DirichletAdRequest.Builder builder = new DirichletAdRequest.Builder() + .withSpaceId(spaceId); + + Integer expressWidth = null; + Integer expressHeight = null; + Integer expressImageWidth = null; + Integer expressImageHeight = null; + + Iterator keys = extras.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object value = extras.opt(key); + if (value == null || JSONObject.NULL.equals(value)) { + continue; + } + + String normalizedKey = key == null ? "" : key.trim().toLowerCase(Locale.US); + switch (normalizedKey) { + case "space_id": + // Already set above, skip to avoid duplicate + continue; + case "express_width": + case "express_view_width": + expressWidth = toInteger(value); + break; + case "express_height": + case "express_view_height": + expressHeight = toInteger(value); + break; + case "express_image_width": + expressImageWidth = toInteger(value); + break; + case "express_image_height": + expressImageHeight = toInteger(value); + break; + default: + applyGenericBuilderValue(builder, normalizedKey, value); + break; + } + } + + if (expressWidth != null || expressHeight != null) { + int width = expressWidth != null ? expressWidth : -1; + int height = expressHeight != null ? expressHeight : -1; + invokeBuilderTwoInts(builder, "withExpressViewAcceptedSize", width, height); + if (entry != null && entry.type == TYPE_SPLASH) { + entry.splashWidth = width; + entry.splashHeight = height; + } + } + + if (expressImageWidth != null || expressImageHeight != null) { + int width = expressImageWidth != null ? expressImageWidth : -1; + int height = expressImageHeight != null ? expressImageHeight : -1; + invokeBuilderTwoInts(builder, "withExpressImageAcceptedSize", width, height); + } + + return builder.build(); + } + + private static void attachRewardInteraction(final AdEntry entry) { + if (entry == null || entry.rewardAd == null) { + return; + } + + entry.rewardAd.setRewardAdInteractionListener(new DirichletRewardVideoAd.RewardAdInteractionListener() { + @Override + public void onAdShow() { + emitEvent(entry.handle, EVENT_SHOW, resolveAdType(entry.type)); + } + + @Override + public void onAdClose() { + emitEvent(entry.handle, EVENT_CLOSE, resolveAdType(entry.type)); + } + + @Override + public void onRewardVerify(boolean rewardVerify, int rewardAmount, String rewardName, int code, String msg) { + JSONObject data = new JSONObject(); + try { + data.put("rewardVerify", rewardVerify); + data.put("rewardAmount", rewardAmount); + data.put("rewardName", rewardName == null ? "" : rewardName); + data.put("code", code); + data.put("message", msg == null ? "" : msg); + } catch (JSONException e) { + Log.w(TAG, "Failed to build reward payload", e); + } + emitEvent(entry.handle, EVENT_REWARD, resolveAdType(entry.type), data); + } + + @Override + public void onAdClick() { + emitEvent(entry.handle, EVENT_CLICK, resolveAdType(entry.type)); + } + }); + } + + private static void attachInterstitialInteraction(final AdEntry entry) { + if (entry == null || entry.interstitialAd == null) { + return; + } + + entry.interstitialAd.setInteractionListener(new DirichletInterstitialAd.InterstitialAdInteractionListener() { + @Override + public void onAdShow() { + emitEvent(entry.handle, EVENT_SHOW, resolveAdType(entry.type)); + } + + @Override + public void onAdClose() { + emitEvent(entry.handle, EVENT_CLOSE, resolveAdType(entry.type)); + } + + @Override + public void onAdClick() { + emitEvent(entry.handle, EVENT_CLICK, resolveAdType(entry.type)); + } + }); + } + + private static void attachBannerInteraction(final AdEntry entry) { + if (entry == null || entry.bannerAd == null) { + return; + } + + entry.bannerAd.setBannerInteractionListener(new DirichletBannerAd.BannerInteractionListener() { + @Override + public void onAdShow() { + emitEvent(entry.handle, EVENT_SHOW, resolveAdType(entry.type)); + } + + @Override + public void onAdClose() { + emitEvent(entry.handle, EVENT_CLOSE, resolveAdType(entry.type)); + } + + @Override + public void onAdClick() { + emitEvent(entry.handle, EVENT_CLICK, resolveAdType(entry.type)); + } + }); + } + + + private static void attachSplashInteraction(final AdEntry entry) { + if (entry == null || entry.splashAd == null) { + return; + } + + entry.splashAd.setSplashInteractionListener(new DirichletSplashAd.AdInteractionListener() { + @Override + public void onAdClick() { + emitEvent(entry.handle, EVENT_CLICK, resolveAdType(entry.type)); + } + + @Override + public void onAdShow() { + emitEvent(entry.handle, EVENT_SHOW, resolveAdType(entry.type)); + } + + @Override + public void onAdClose() { + emitEvent(entry.handle, EVENT_CLOSE, resolveAdType(entry.type)); + } + }); + } + + private static void emitEvent(String handle, String eventName, String adType) { + emitEvent(handle, eventName, adType, null); + } + + private static void emitEvent(String handle, String eventName, String adType, JSONObject data) { + if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(eventName)) { + return; + } + + try { + JSONObject message = new JSONObject(); + message.put("handle", handle); + message.put("eventName", eventName); + if (!TextUtils.isEmpty(adType)) { + message.put("adType", adType); + } + if (data != null && data.length() > 0) { + message.put("data", data); + } + UnityPlayer.UnitySendMessage(UNITY_CALLBACK_OBJECT, UNITY_CALLBACK_METHOD, message.toString()); + } catch (Exception ex) { + Log.w(TAG, "Failed to emit unity event", ex); + } + } + + private static String resolveAdType(int type) { + switch (type) { + case TYPE_REWARD: + return "reward"; + case TYPE_INTERSTITIAL: + return "interstitial"; + case TYPE_BANNER: + return "banner"; + case TYPE_SPLASH: + return "splash"; + case TYPE_EXPRESS_FEED: + return "express_feed"; + case TYPE_NATIVE_FEED: + return "native_feed"; + default: + return "unknown"; + } + } + + private static Map buildRequestMethodMap() { + Map map = new HashMap<>(16); + map.put("space_id", "withSpaceId"); + map.put("extra1", "withExtra1"); + map.put("user_id", "withUserId"); + map.put("reward_name", "withRewardName"); + map.put("reward_amount", "withRewardAmount"); + map.put("query", "withQuery"); + map.put("express_width", "withExpressViewAcceptedSize"); + map.put("express_height", "withExpressViewAcceptedSize"); + map.put("express_view_width", "withExpressViewAcceptedSize"); + map.put("express_view_height", "withExpressViewAcceptedSize"); + map.put("express_image_width", "withExpressImageAcceptedSize"); + map.put("express_image_height", "withExpressImageAcceptedSize"); + map.put("mina_id", "withMinaId"); + map.put("slide_internal", "withSlideInternal"); + return Collections.unmodifiableMap(map); + } + + private static String mergeDataPayload(String subChannel, String dataJson) { + if (!TextUtils.isEmpty(dataJson)) { + return dataJson; + } + + if (TextUtils.isEmpty(subChannel)) { + return null; + } + + try { + JSONObject json = new JSONObject(); + json.put("sub_channel", subChannel); + return json.toString(); + } catch (JSONException e) { + Log.w(TAG, "mergeDataPayload error", e); + return null; + } + } + + private static long safeParseLong(String value, long fallback) { + if (TextUtils.isEmpty(value)) { + return fallback; + } + + try { + return Long.parseLong(value); + } catch (NumberFormatException ignore) { + return fallback; + } + } + + /** + * Internal data structure to hold ad instances and associated state. + * + * Since Unity cannot directly hold Java objects, this class serves as a bridge + * to store ad instances returned from DirichletAdNative.loadXXXAd() methods. + * The handle ID is used as a key in AD_CACHE to map Unity's string reference + * to the actual Java ad object. + * + * This design is necessary because: + * 1. Unity can only pass strings between C# and Java + * 2. Different ad types have different interfaces (rewardAd.showRewardVideoAd() vs interstitialAd.show()) + * 3. Splash ads require additional UI state (container, dimensions) + * 4. Event callbacks need handle and type information + */ + private static final class AdEntry { + final String handle; + final int type; + + // Ad object - only one will be set based on type + DirichletRewardVideoAd rewardAd; + DirichletInterstitialAd interstitialAd; + DirichletBannerAd bannerAd; + DirichletSplashAd splashAd; + + // Splash-specific state + FrameLayout splashContainer; + int splashWidth; + int splashHeight; + + AdEntry(String handle, int type) { + this.handle = handle; + this.type = type; + } + + /** + * Gets the ad object based on type. Used for generic operations. + */ + Object getAdObject() { + switch (type) { + case TYPE_REWARD: + return rewardAd; + case TYPE_INTERSTITIAL: + return interstitialAd; + case TYPE_BANNER: + return bannerAd; + case TYPE_SPLASH: + return splashAd; + default: + return null; + } + } + + /** + * Destroys the ad instance and releases resources. + * This matches the native SDK pattern where ad objects have destroy() methods. + */ + void destroy() { + try { + switch (type) { + case TYPE_REWARD: + if (rewardAd != null) { + rewardAd.destroy(); + rewardAd = null; + } + break; + case TYPE_INTERSTITIAL: + if (interstitialAd != null) { + interstitialAd.destroy(); + interstitialAd = null; + } + break; + case TYPE_BANNER: + if (bannerAd != null) { + bannerAd.destroy(); + bannerAd = null; + } + break; + case TYPE_SPLASH: + if (splashAd != null) { + splashAd.setSplashInteractionListener(null); + splashAd.destroy(); + splashAd = null; + } + detachSplashContainer(this); + break; + } + } catch (Throwable t) { + Log.w(TAG, "destroyAd exception", t); + } + } + } + + private static final class UnityCustomController extends DirichletAdCustomController { + } +} diff --git a/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java.meta b/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java.meta new file mode 100644 index 0000000..80f94bf --- /dev/null +++ b/Assets/Plugins/Android/DirichletMediation/src/main/java/com/dirichlet/unity/DirichletUnityBridge.java.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 14cb359fe1a3c478ca0969cf714aff16 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar b/Assets/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar new file mode 100644 index 0000000..cb42934 Binary files /dev/null and b/Assets/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar differ diff --git a/Assets/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar.meta b/Assets/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar.meta new file mode 100644 index 0000000..f39d9f8 --- /dev/null +++ b/Assets/Plugins/Android/com.tencent.mm.opensdk.wechat-sdk-android-6.8.34.aar.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: 8745d7dbdbaeb7948ab964d52eafe76c +labels: +- gpsr +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/libs.meta b/Assets/Plugins/Android/libs.meta new file mode 100644 index 0000000..2ee9baa --- /dev/null +++ b/Assets/Plugins/Android/libs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e7e8438cf7c294b92988c3830e288191 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar b/Assets/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar new file mode 100644 index 0000000..68322db Binary files /dev/null and b/Assets/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar differ diff --git a/Assets/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar.meta b/Assets/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar.meta new file mode 100644 index 0000000..c0095bf --- /dev/null +++ b/Assets/Plugins/Android/libs/GDTSDK.unionNormal.4.671.1541.aar.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 90b3b17e0788494398662e729c344ee2 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: + diff --git a/Assets/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar b/Assets/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar new file mode 100644 index 0000000..12d7b79 Binary files /dev/null and b/Assets/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar differ diff --git a/Assets/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar.meta b/Assets/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar.meta new file mode 100644 index 0000000..06ccc36 --- /dev/null +++ b/Assets/Plugins/Android/libs/iadsdk-release-2.3.102.110.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 92c7680ef2e44d279c23926510865f93 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar b/Assets/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar new file mode 100644 index 0000000..a98d09d Binary files /dev/null and b/Assets/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar differ diff --git a/Assets/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar.meta b/Assets/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar.meta new file mode 100644 index 0000000..d3f502a --- /dev/null +++ b/Assets/Plugins/Android/libs/open_ad_sdk_7.4.2.2.aar.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 930136242e574a2b89110e6a25e49065 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: + diff --git a/Assets/Plugins/Android/proguard-user.txt b/Assets/Plugins/Android/proguard-user.txt new file mode 100644 index 0000000..e69de29 diff --git a/Assets/Plugins/Android/proguard-user.txt.meta b/Assets/Plugins/Android/proguard-user.txt.meta new file mode 100644 index 0000000..6be6b78 --- /dev/null +++ b/Assets/Plugins/Android/proguard-user.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c51429155b4914ffe99f152a28d7d6d4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS.meta b/Assets/Plugins/iOS.meta new file mode 100644 index 0000000..f4183df --- /dev/null +++ b/Assets/Plugins/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 215b74292b87e41809a9e2c72050a616 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/DirichletMediationUnityBridge.h b/Assets/Plugins/iOS/DirichletMediationUnityBridge.h new file mode 100644 index 0000000..436f5b5 --- /dev/null +++ b/Assets/Plugins/iOS/DirichletMediationUnityBridge.h @@ -0,0 +1,80 @@ +// +// DirichletMediationUnityBridge.h +// Dirichlet Mediation Unity Bridge for iOS +// +// Created by Dirichlet Unity SDK +// Copyright © 2025 Dirichlet Inc. All rights reserved. +// + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +/// Initialize the Dirichlet Mediation SDK +/// @param mediaId Media ID string +/// @param mediaKey Media key string +/// @param enableLog Enable debug logging +/// @param mediaName Media name string (optional) +/// @param gameChannel Game channel string (optional) +/// @param shakeEnabled Enable shake interaction (for GDT adapter) +/// @param allowIDFAAccess Allow IDFA access (for TapADN and GDT adapters) +/// @param aTags External aTags JSON string (optional) +/// @return YES if initialization started successfully, NO otherwise +bool DirichletMediationUnityBridge_Initialize( + const char* mediaId, + const char* mediaKey, + bool enableLog, + const char* mediaName, + const char* gameChannel, + bool shakeEnabled, + bool allowIDFAAccess, + const char* aTags +); + +/// Request permissions if necessary (ATT for iOS 14+) +void DirichletMediationUnityBridge_RequestPermissionIfNeeded(void); + +/// Get SDK version +/// @return SDK version string (caller should NOT free this pointer) +const char* DirichletMediationUnityBridge_GetSdkVersion(void); + +/// Load reward video ad +/// @param spaceId Space/slot ID +/// @param extras JSON string with additional parameters +/// @return Handle ID for the ad instance (caller should copy/free) +const char* DirichletMediationUnityBridge_LoadRewardVideoAd(long long spaceId, const char* extras); + +/// Load interstitial ad +/// @param spaceId Space/slot ID +/// @param extras JSON string with additional parameters +/// @return Handle ID for the ad instance +const char* DirichletMediationUnityBridge_LoadInterstitialAd(long long spaceId, const char* extras); + +/// Load banner ad +/// @param spaceId Space/slot ID +/// @param extras JSON string with additional parameters +/// @return Handle ID for the ad instance +const char* DirichletMediationUnityBridge_LoadBannerAd(long long spaceId, const char* extras); + +/// Load splash ad +/// @param spaceId Space/slot ID +/// @param extras JSON string with additional parameters +/// @return Handle ID for the ad instance +const char* DirichletMediationUnityBridge_LoadSplashAd(long long spaceId, const char* extras); + +/// Show ad by handle +/// @param handleId Handle ID returned from load methods +/// @param extras JSON string with show options (e.g., banner alignment) +/// @return YES if show started successfully, NO otherwise +bool DirichletMediationUnityBridge_ShowAd(const char* handleId, const char* extras); + +/// Destroy ad by handle +/// @param handleId Handle ID returned from load methods +void DirichletMediationUnityBridge_DestroyAd(const char* handleId); + +#ifdef __cplusplus +} +#endif + diff --git a/Assets/Plugins/iOS/DirichletMediationUnityBridge.h.meta b/Assets/Plugins/iOS/DirichletMediationUnityBridge.h.meta new file mode 100644 index 0000000..c27c09d --- /dev/null +++ b/Assets/Plugins/iOS/DirichletMediationUnityBridge.h.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 07aa84d4e2aa748719c5e30912f2cf53 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/DirichletMediationUnityBridge.mm b/Assets/Plugins/iOS/DirichletMediationUnityBridge.mm new file mode 100644 index 0000000..381171e --- /dev/null +++ b/Assets/Plugins/iOS/DirichletMediationUnityBridge.mm @@ -0,0 +1,682 @@ +// +// DirichletMediationUnityBridge.mm +// Dirichlet Mediation Unity Bridge for iOS +// +// +// Created by Dirichlet Unity SDK +// Copyright © 2025 Dirichlet Inc. All rights reserved. +// + +#import "DirichletMediationUnityBridge.h" +#import +#import + +// Unity callback interface +extern "C" void UnitySendMessage(const char* obj, const char* method, const char* msg); + +// Constants +static NSString * const kUnityCallbackObject = @"DirichletMediationEventReceiver"; +static NSString * const kUnityCallbackMethod = @"OnNativeEvent"; +static NSString * const kUnityLoadCallbackObject = @"DirichletMediationIOSLoadCallbackReceiver"; +static NSString * const kUnityLoadCallbackMethod = @"OnLoadCallback"; +static NSString * const kUnityInitCallbackObject = @"DirichletMediationIOSInitCallbackReceiver"; +static NSString * const kUnityInitCallbackMethod = @"OnInitCallback"; + +// Helper to convert C string to NSString +static NSString* CreateNSString(const char* cString) { + return cString ? [NSString stringWithUTF8String:cString] : @""; +} + +// Helper to convert NSString to C string (caller must free) +static char* MakeCString(NSString* nsString) { + if (nsString == nil) { + return NULL; + } + const char* utf8String = [nsString UTF8String]; + char* cString = (char*)malloc(strlen(utf8String) + 1); + strcpy(cString, utf8String); + return cString; +} + +// Helper to send event to Unity +static void SendEventToUnity(NSString* handleId, NSString* eventName, NSString* adType, NSDictionary* data) { + NSMutableDictionary* payload = [NSMutableDictionary dictionary]; + payload[@"handle"] = handleId ?: @""; + payload[@"eventName"] = eventName ?: @""; + payload[@"adType"] = adType ?: @""; + if (data) { + payload[@"data"] = data; + } + + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error]; + if (jsonData && !error) { + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + UnitySendMessage([kUnityCallbackObject UTF8String], [kUnityCallbackMethod UTF8String], [jsonString UTF8String]); + } +} + +// Helper to send load callback to Unity (separate from ad events) +static void SendLoadCallbackToUnity(NSString* handleId, NSString* eventName, NSString* adType, NSDictionary* data) { + NSMutableDictionary* payload = [NSMutableDictionary dictionary]; + payload[@"handle"] = handleId ?: @""; + payload[@"eventName"] = eventName ?: @""; + payload[@"adType"] = adType ?: @""; + if (data) { + payload[@"data"] = data; + } + + NSError* error = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error]; + if (jsonData && !error) { + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + UnitySendMessage([kUnityLoadCallbackObject UTF8String], [kUnityLoadCallbackMethod UTF8String], [jsonString UTF8String]); + } +} + +// Helper to send init callback to Unity (async initialization result) +static void SendInitCallbackToUnity(BOOL success, NSError* error, NSString* extraMessage) { + NSMutableDictionary* payload = [NSMutableDictionary dictionary]; + payload[@"success"] = @(success); + + NSMutableDictionary* data = [NSMutableDictionary dictionary]; + if (error) { + data[@"code"] = @(error.code); + data[@"message"] = error.localizedDescription ?: @""; + if (error.domain) { + data[@"domain"] = error.domain; + } + } else if (extraMessage.length > 0) { + data[@"message"] = extraMessage; + } + + if (data.count > 0) { + payload[@"data"] = data; + } + + NSError* jsonError = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&jsonError]; + if (jsonData && !jsonError) { + NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + UnitySendMessage([kUnityInitCallbackObject UTF8String], [kUnityInitCallbackMethod UTF8String], [jsonString UTF8String]); + } +} + +// Parse JSON extras to dictionary +static NSDictionary* ParseExtras(const char* extrasJson) { + if (!extrasJson || strlen(extrasJson) == 0) { + return nil; + } + + NSString* jsonString = CreateNSString(extrasJson); + NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + if (!jsonData) { + return nil; + } + + NSError* error = nil; + NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + return error ? nil : dict; +} + +#pragma mark - Ad Instance Manager + +@interface DirichletMediationInstanceManager : NSObject + +@property (nonatomic, strong) NSMutableDictionary* adInstances; +@property (nonatomic, strong) dispatch_queue_t syncQueue; + ++ (instancetype)shared; +- (void)storeAd:(id)ad forHandle:(NSString*)handleId; +- (id)adForHandle:(NSString*)handleId; +- (void)removeAdForHandle:(NSString*)handleId; +- (NSString*)generateHandle; + +@end + +@implementation DirichletMediationInstanceManager + ++ (instancetype)shared { + static DirichletMediationInstanceManager* instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[DirichletMediationInstanceManager alloc] init]; + }); + return instance; +} + +- (instancetype)init { + if (self = [super init]) { + _adInstances = [NSMutableDictionary dictionary]; + _syncQueue = dispatch_queue_create("com.dirichlet.mediation.unity.admanager", DISPATCH_QUEUE_SERIAL); + } + return self; +} + +- (void)storeAd:(id)ad forHandle:(NSString*)handleId { + dispatch_sync(self.syncQueue, ^{ + self.adInstances[handleId] = ad; + }); +} + +- (id)adForHandle:(NSString*)handleId { + __block id ad = nil; + dispatch_sync(self.syncQueue, ^{ + ad = self.adInstances[handleId]; + }); + return ad; +} + +- (void)removeAdForHandle:(NSString*)handleId { + dispatch_sync(self.syncQueue, ^{ + [self.adInstances removeObjectForKey:handleId]; + }); +} + +- (NSString*)generateHandle { + return [[NSUUID UUID] UUIDString]; +} + +@end + +#pragma mark - Ad Delegates + +// Reward Video Ad Delegate +@interface DirichletMediationUnityRewardVideoAdDelegate : NSObject +@property (nonatomic, strong) NSString* handleId; +@end + +@implementation DirichletMediationUnityRewardVideoAdDelegate + +- (void)rewardVideoAdDidShow:(DRMRewardVideoAd *)rewardVideoAd { + SendEventToUnity(self.handleId, @"show", @"reward_video", nil); +} + +- (void)rewardVideoAdDidFailToShow:(DRMRewardVideoAd *)rewardVideoAd withError:(NSError *)error { + NSDictionary* data = @{ + @"code": @(error.code), + @"message": error.localizedDescription ?: @"Unknown error" + }; + SendEventToUnity(self.handleId, @"show_error", @"reward_video", data); +} + +- (void)rewardVideoAdDidClick:(DRMRewardVideoAd *)rewardVideoAd { + SendEventToUnity(self.handleId, @"click", @"reward_video", nil); +} + +- (void)rewardVideoAdDidClose:(DRMRewardVideoAd *)rewardVideoAd { + SendEventToUnity(self.handleId, @"close", @"reward_video", nil); +} + +- (void)rewardVideoAdDidRewardUser:(DRMRewardVideoAd *)rewardVideoAd { + NSDictionary* data = @{ + @"rewardVerify": @(YES), + @"rewardAmount": @(0), + @"rewardName": @"", + @"code": @(0), + @"message": @"" + }; + SendEventToUnity(self.handleId, @"reward", @"reward_video", data); +} + +@end + +// Interstitial Ad Delegate +@interface DirichletMediationUnityInterstitialAdDelegate : NSObject +@property (nonatomic, strong) NSString* handleId; +@end + +@implementation DirichletMediationUnityInterstitialAdDelegate + +- (void)interstitialAdDidShow:(DRMInterstitialAd *)interstitialAd { + SendEventToUnity(self.handleId, @"show", @"interstitial", nil); +} + +- (void)interstitialAdDidFailToShow:(DRMInterstitialAd *)interstitialAd withError:(NSError *)error { + NSDictionary* data = @{ + @"code": @(error.code), + @"message": error.localizedDescription ?: @"Unknown error" + }; + SendEventToUnity(self.handleId, @"show_error", @"interstitial", data); +} + +- (void)interstitialAdDidClick:(DRMInterstitialAd *)interstitialAd { + SendEventToUnity(self.handleId, @"click", @"interstitial", nil); +} + +- (void)interstitialAdDidClose:(DRMInterstitialAd *)interstitialAd { + SendEventToUnity(self.handleId, @"close", @"interstitial", nil); +} + +@end + +// Banner Ad Delegate +@interface DirichletMediationUnityBannerAdDelegate : NSObject +@property (nonatomic, strong) NSString* handleId; +@end + +@implementation DirichletMediationUnityBannerAdDelegate + +- (void)bannerAdDidShow:(DRMBannerAd *)bannerAd { + SendEventToUnity(self.handleId, @"show", @"banner", nil); +} + +- (void)bannerAdDidFailToShow:(DRMBannerAd *)bannerAd withError:(NSError *)error { + NSDictionary* data = @{ + @"code": @(error.code), + @"message": error.localizedDescription ?: @"Unknown error" + }; + SendEventToUnity(self.handleId, @"show_error", @"banner", data); +} + +- (void)bannerAdDidClick:(DRMBannerAd *)bannerAd { + SendEventToUnity(self.handleId, @"click", @"banner", nil); +} + +- (void)bannerAdDidClose:(DRMBannerAd *)bannerAd { + SendEventToUnity(self.handleId, @"close", @"banner", nil); +} + +@end + +// Splash Ad Delegate +@interface DirichletMediationUnitySplashAdDelegate : NSObject +@property (nonatomic, strong) NSString* handleId; +@end + +@implementation DirichletMediationUnitySplashAdDelegate + +- (void)splashAdDidShow:(DRMSplashAd *)splashAd { + SendEventToUnity(self.handleId, @"show", @"splash", nil); +} + +- (void)splashAdDidFailToShow:(DRMSplashAd *)splashAd withError:(NSError *)error { + NSDictionary* data = @{ + @"code": @(error.code), + @"message": error.localizedDescription ?: @"Unknown error" + }; + SendEventToUnity(self.handleId, @"show_error", @"splash", data); +} + +- (void)splashAdDidClick:(DRMSplashAd *)splashAd { + SendEventToUnity(self.handleId, @"click", @"splash", nil); +} + +- (void)splashAdDidClose:(DRMSplashAd *)splashAd { + SendEventToUnity(self.handleId, @"close", @"splash", nil); +} + +@end + +#pragma mark - Bridge Implementation + +extern "C" { + +bool DirichletMediationUnityBridge_Initialize( + const char* mediaId, + const char* mediaKey, + bool enableLog, + const char* mediaName, + const char* gameChannel, + bool shakeEnabled, + bool allowIDFAAccess, + const char* aTags +) { + NSString* nsMediaId = CreateNSString(mediaId); + NSString* nsMediaKey = CreateNSString(mediaKey); + + NSLog(@"[DirichletMediationUnityBridge] Initialize called with mediaId=%@, mediaKey=%@, enableLog=%d", + nsMediaId, nsMediaKey, enableLog); + + if (nsMediaId.length == 0 || nsMediaKey.length == 0) { + NSLog(@"[DirichletMediationUnityBridge] Initialize failed: mediaId and mediaKey are required"); + return false; + } + + // Check if SDK is already initialized + if ([DirichletMediation isInitialized]) { + NSLog(@"[DirichletMediationUnityBridge] SDK already initialized"); + return true; + } + + DRMSDKConfig* config = [DRMSDKConfig configWithMediaId:nsMediaId mediaKey:nsMediaKey]; + if (!config) { + NSLog(@"[DirichletMediationUnityBridge] Failed to create SDK config"); + return false; + } + + config.isDebug = enableLog; + config.shakeEnabled = shakeEnabled; + config.allowIDFAAccess = allowIDFAAccess; + + if (mediaName && strlen(mediaName) > 0) { + config.mediaName = CreateNSString(mediaName); + } + + if (gameChannel && strlen(gameChannel) > 0) { + config.gameChannel = CreateNSString(gameChannel); + } + + if (aTags && strlen(aTags) > 0) { + config.aTags = CreateNSString(aTags); + } + + NSLog(@"[DirichletMediationUnityBridge] Starting SDK initialization (async callback)..."); + NSLog(@"[DirichletMediationUnityBridge] Config details - mediaId:%@, mediaKey:%@, mediaName:%@, gameChannel:%@", + config.mediaId, config.mediaKey, config.mediaName, config.gameChannel); + + // Use async callback pattern (aligned with Ad Unity implementation) + void (^startBlock)(void) = ^{ + [DirichletMediation startWithConfig:config completion:^(BOOL success, NSError * _Nullable error) { + if (error) { + NSLog(@"[DirichletMediationUnityBridge] SDK init callback - success:%d, error:%@ (code:%ld, domain:%@)", + success, error.localizedDescription, (long)error.code, error.domain); + } else { + NSLog(@"[DirichletMediationUnityBridge] SDK init callback - success:%d", success); + } + SendInitCallbackToUnity(success, error, success ? @"ios_mediation_bridge" : nil); + }]; + }; + + if ([NSThread isMainThread]) { + startBlock(); + } else { + dispatch_async(dispatch_get_main_queue(), startBlock); + } + + return true; +} + +void DirichletMediationUnityBridge_RequestPermissionIfNeeded(void) { + // iOS 14+ ATT permission is handled internally by the SDK + NSLog(@"[DirichletMediationUnityBridge] RequestPermissionIfNeeded called"); +} + +const char* DirichletMediationUnityBridge_GetSdkVersion(void) { + static char* versionCString = NULL; + if (versionCString == NULL) { + versionCString = MakeCString([DirichletMediation sdkVersion]); + } + return versionCString; +} + +const char* DirichletMediationUnityBridge_LoadRewardVideoAd(long long spaceId, const char* extras) { + NSString* handleId = [[DirichletMediationInstanceManager shared] generateHandle]; + NSDictionary* extrasDict = ParseExtras(extras); + + // Create load request + DRMAdLoadRequest* request = [[DRMAdLoadRequest alloc] initWithSpaceId:[NSString stringWithFormat:@"%lld", spaceId]]; + + // Apply extras if provided (matching Unity C# ToBridgePayload keys) + if (extrasDict[@"user_id"]) { + request.rewardUserId = extrasDict[@"user_id"]; + } + if (extrasDict[@"extra1"]) { + request.rewardExtra = extrasDict[@"extra1"]; + } + if (extrasDict[@"reward_name"]) { + request.rewardName = extrasDict[@"reward_name"]; + } + if (extrasDict[@"reward_amount"]) { + request.rewardAmount = [extrasDict[@"reward_amount"] integerValue]; + } + if (extrasDict[@"mina_id"]) { + request.minaId = [NSString stringWithFormat:@"%@", extrasDict[@"mina_id"]]; + } + + [DRMRewardVideoAd loadWithRequest:request completion:^(NSArray * _Nullable ads, NSError * _Nullable error) { + if (ads && ads.count > 0) { + DRMRewardVideoAd* ad = ads.firstObject; + DirichletMediationUnityRewardVideoAdDelegate* delegate = [[DirichletMediationUnityRewardVideoAdDelegate alloc] init]; + delegate.handleId = handleId; + ad.delegate = delegate; + + [[DirichletMediationInstanceManager shared] storeAd:ad forHandle:handleId]; + [[DirichletMediationInstanceManager shared] storeAd:delegate forHandle:[handleId stringByAppendingString:@"_delegate"]]; + + NSLog(@"[DirichletMediationUnityBridge] RewardVideoAd loaded: %@", handleId); + SendLoadCallbackToUnity(handleId, @"load_success", @"reward_video", nil); + } else { + NSLog(@"[DirichletMediationUnityBridge] RewardVideoAd load failed: %@", error.localizedDescription); + NSDictionary* errorData = @{ + @"code": @(error.code), + @"message": error.localizedDescription ?: @"Unknown error" + }; + SendLoadCallbackToUnity(handleId, @"load_error", @"reward_video", errorData); + } + }]; + + return MakeCString(handleId); +} + +const char* DirichletMediationUnityBridge_LoadInterstitialAd(long long spaceId, const char* extras) { + NSString* handleId = [[DirichletMediationInstanceManager shared] generateHandle]; + NSDictionary* extrasDict = ParseExtras(extras); + + // Create load request + DRMAdLoadRequest* request = [[DRMAdLoadRequest alloc] initWithSpaceId:[NSString stringWithFormat:@"%lld", spaceId]]; + + // Apply extras if provided (matching Unity C# ToBridgePayload keys) + if (extrasDict[@"mina_id"]) { + request.minaId = [NSString stringWithFormat:@"%@", extrasDict[@"mina_id"]]; + } + + [DRMInterstitialAd loadWithRequest:request completion:^(NSArray * _Nullable ads, NSError * _Nullable error) { + if (ads && ads.count > 0) { + DRMInterstitialAd* ad = ads.firstObject; + DirichletMediationUnityInterstitialAdDelegate* delegate = [[DirichletMediationUnityInterstitialAdDelegate alloc] init]; + delegate.handleId = handleId; + ad.delegate = delegate; + + [[DirichletMediationInstanceManager shared] storeAd:ad forHandle:handleId]; + [[DirichletMediationInstanceManager shared] storeAd:delegate forHandle:[handleId stringByAppendingString:@"_delegate"]]; + + NSLog(@"[DirichletMediationUnityBridge] InterstitialAd loaded: %@", handleId); + SendLoadCallbackToUnity(handleId, @"load_success", @"interstitial", nil); + } else { + NSLog(@"[DirichletMediationUnityBridge] InterstitialAd load failed: %@", error.localizedDescription); + NSDictionary* errorData = @{ + @"code": @(error.code), + @"message": error.localizedDescription ?: @"Unknown error" + }; + SendLoadCallbackToUnity(handleId, @"load_error", @"interstitial", errorData); + } + }]; + + return MakeCString(handleId); +} + +const char* DirichletMediationUnityBridge_LoadBannerAd(long long spaceId, const char* extras) { + NSString* handleId = [[DirichletMediationInstanceManager shared] generateHandle]; + NSDictionary* extrasDict = ParseExtras(extras); + + // Create load request + DRMAdLoadRequest* request = [[DRMAdLoadRequest alloc] initWithSpaceId:[NSString stringWithFormat:@"%lld", spaceId]]; + + // Apply extras if provided (matching Unity C# ToBridgePayload keys) + if (extrasDict[@"mina_id"]) { + request.minaId = [NSString stringWithFormat:@"%@", extrasDict[@"mina_id"]]; + } + // Set ad size for Banner (CSJ/GDT adapters need this) + NSNumber* width = extrasDict[@"express_width"]; + NSNumber* height = extrasDict[@"express_height"]; + if (width || height) { + CGFloat w = width ? [width floatValue] : 0; + CGFloat h = height ? [height floatValue] : 0; + request.adSize = CGSizeMake(w, h); + } + + [DRMBannerAd loadWithRequest:request completion:^(NSArray * _Nullable ads, NSError * _Nullable error) { + if (ads && ads.count > 0) { + DRMBannerAd* ad = ads.firstObject; + DirichletMediationUnityBannerAdDelegate* delegate = [[DirichletMediationUnityBannerAdDelegate alloc] init]; + delegate.handleId = handleId; + ad.delegate = delegate; + + [[DirichletMediationInstanceManager shared] storeAd:ad forHandle:handleId]; + [[DirichletMediationInstanceManager shared] storeAd:delegate forHandle:[handleId stringByAppendingString:@"_delegate"]]; + + NSLog(@"[DirichletMediationUnityBridge] BannerAd loaded: %@", handleId); + SendLoadCallbackToUnity(handleId, @"load_success", @"banner", nil); + } else { + NSLog(@"[DirichletMediationUnityBridge] BannerAd load failed: %@", error.localizedDescription); + NSDictionary* errorData = @{ + @"code": @(error.code), + @"message": error.localizedDescription ?: @"Unknown error" + }; + SendLoadCallbackToUnity(handleId, @"load_error", @"banner", errorData); + } + }]; + + return MakeCString(handleId); +} + +const char* DirichletMediationUnityBridge_LoadSplashAd(long long spaceId, const char* extras) { + NSString* handleId = [[DirichletMediationInstanceManager shared] generateHandle]; + NSDictionary* extrasDict = ParseExtras(extras); + + // Create load request + DRMAdLoadRequest* request = [[DRMAdLoadRequest alloc] initWithSpaceId:[NSString stringWithFormat:@"%lld", spaceId]]; + + // Apply extras if provided (matching Unity C# ToBridgePayload keys) + if (extrasDict[@"mina_id"]) { + request.minaId = [NSString stringWithFormat:@"%@", extrasDict[@"mina_id"]]; + } + // Set ad size for Splash (CSJ adapter needs this) + NSNumber* width = extrasDict[@"express_width"]; + NSNumber* height = extrasDict[@"express_height"]; + if (width || height) { + CGFloat w = width ? [width floatValue] : 0; + CGFloat h = height ? [height floatValue] : 0; + request.adSize = CGSizeMake(w, h); + } + + [DRMSplashAd loadWithRequest:request completion:^(NSArray * _Nullable ads, NSError * _Nullable error) { + if (ads && ads.count > 0) { + DRMSplashAd* ad = ads.firstObject; + DirichletMediationUnitySplashAdDelegate* delegate = [[DirichletMediationUnitySplashAdDelegate alloc] init]; + delegate.handleId = handleId; + ad.delegate = delegate; + + [[DirichletMediationInstanceManager shared] storeAd:ad forHandle:handleId]; + [[DirichletMediationInstanceManager shared] storeAd:delegate forHandle:[handleId stringByAppendingString:@"_delegate"]]; + + NSLog(@"[DirichletMediationUnityBridge] SplashAd loaded: %@", handleId); + SendLoadCallbackToUnity(handleId, @"load_success", @"splash", nil); + } else { + NSLog(@"[DirichletMediationUnityBridge] SplashAd load failed: %@", error.localizedDescription); + NSDictionary* errorData = @{ + @"code": @(error.code), + @"message": error.localizedDescription ?: @"Unknown error" + }; + SendLoadCallbackToUnity(handleId, @"load_error", @"splash", errorData); + } + }]; + + return MakeCString(handleId); +} + +bool DirichletMediationUnityBridge_ShowAd(const char* handleId, const char* extras) { + NSString* nsHandleId = CreateNSString(handleId); + id ad = [[DirichletMediationInstanceManager shared] adForHandle:nsHandleId]; + + if (!ad) { + NSLog(@"[DirichletMediationUnityBridge] ShowAd failed: Ad not found for handle %@", nsHandleId); + return false; + } + + // Ensure show is always called on main thread (aligned with Ad Unity implementation) + dispatch_async(dispatch_get_main_queue(), ^{ + UIViewController* rootVC = [[[UIApplication sharedApplication] keyWindow] rootViewController]; + if (!rootVC) { + NSLog(@"[DirichletMediationUnityBridge] ShowAd failed: Root view controller not found"); + return; + } + + if ([ad isKindOfClass:[DRMRewardVideoAd class]]) { + DRMRewardVideoAd* rewardAd = (DRMRewardVideoAd*)ad; + if ([rewardAd isReady]) { + [rewardAd showFromViewController:rootVC]; + NSLog(@"[DirichletMediationUnityBridge] Showing reward video ad: %@", nsHandleId); + } + } else if ([ad isKindOfClass:[DRMInterstitialAd class]]) { + DRMInterstitialAd* interstitialAd = (DRMInterstitialAd*)ad; + if ([interstitialAd isReady]) { + [interstitialAd showFromViewController:rootVC]; + NSLog(@"[DirichletMediationUnityBridge] Showing interstitial ad: %@", nsHandleId); + } + } else if ([ad isKindOfClass:[DRMBannerAd class]]) { + DRMBannerAd* bannerAd = (DRMBannerAd*)ad; + UIView* bannerView = bannerAd.view; + if (bannerView) { + // Banner 广告需要将 view 添加到视图控制器上 + // 注意:Unity 侧需要通过 Unity UI 系统来处理 Banner 视图 + // 这里我们发送一个事件通知 Unity 侧,让 Unity 侧来处理视图的展示 + // 或者直接将视图添加到根视图控制器上(临时方案) + [rootVC.view addSubview:bannerView]; + bannerView.translatesAutoresizingMaskIntoConstraints = NO; + // 设置约束,让 Banner 显示在底部 + [NSLayoutConstraint activateConstraints:@[ + [bannerView.leadingAnchor constraintEqualToAnchor:rootVC.view.leadingAnchor], + [bannerView.trailingAnchor constraintEqualToAnchor:rootVC.view.trailingAnchor], + [bannerView.bottomAnchor constraintEqualToAnchor:rootVC.view.safeAreaLayoutGuide.bottomAnchor], + [bannerView.heightAnchor constraintEqualToConstant:bannerAd.size.height > 0 ? bannerAd.size.height : 50] + ]]; + NSLog(@"[DirichletMediationUnityBridge] Showing banner ad: %@", nsHandleId); + } else { + NSLog(@"[DirichletMediationUnityBridge] Banner ad view not available: %@", nsHandleId); + } + } else if ([ad isKindOfClass:[DRMSplashAd class]]) { + DRMSplashAd* splashAd = (DRMSplashAd*)ad; + if ([splashAd isReady]) { + [splashAd showFromViewController:rootVC]; + NSLog(@"[DirichletMediationUnityBridge] Showing splash ad: %@", nsHandleId); + } + } else { + NSLog(@"[DirichletMediationUnityBridge] ShowAd failed: Ad not ready or unknown type"); + } + }); + + return true; +} + +void DirichletMediationUnityBridge_DestroyAd(const char* handleId) { + NSString* nsHandleId = CreateNSString(handleId); + NSLog(@"[DirichletMediationUnityBridge] Destroying ad: %@", nsHandleId); + + // Remove ad instance + [[DirichletMediationInstanceManager shared] removeAdForHandle:nsHandleId]; + + // Remove delegate + NSString* delegateKey = [nsHandleId stringByAppendingString:@"_delegate"]; + [[DirichletMediationInstanceManager shared] removeAdForHandle:delegateKey]; +} + +bool DirichletMediationUnityBridge_IsAdValid(const char* handleId) { + NSString* nsHandleId = CreateNSString(handleId); + id ad = [[DirichletMediationInstanceManager shared] adForHandle:nsHandleId]; + + if (!ad) { + NSLog(@"[DirichletMediationUnityBridge] IsAdValid: Ad not found for handle %@", nsHandleId); + return false; + } + + if ([ad isKindOfClass:[DRMRewardVideoAd class]]) { + DRMRewardVideoAd* rewardAd = (DRMRewardVideoAd*)ad; + return [rewardAd isReady]; + } else if ([ad isKindOfClass:[DRMInterstitialAd class]]) { + DRMInterstitialAd* interstitialAd = (DRMInterstitialAd*)ad; + return [interstitialAd isReady]; + } else if ([ad isKindOfClass:[DRMBannerAd class]]) { + // Banner ads don't have isReady, assume valid if loaded + return true; + } else if ([ad isKindOfClass:[DRMSplashAd class]]) { + DRMSplashAd* splashAd = (DRMSplashAd*)ad; + return [splashAd isReady]; + } + + return false; +} + +} // extern "C" + diff --git a/Assets/Plugins/iOS/DirichletMediationUnityBridge.mm.meta b/Assets/Plugins/iOS/DirichletMediationUnityBridge.mm.meta new file mode 100644 index 0000000..80912d8 --- /dev/null +++ b/Assets/Plugins/iOS/DirichletMediationUnityBridge.mm.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 9f8326f62fa7a44aebc39371a5fcddd0 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples~.meta b/Assets/Samples~.meta new file mode 100644 index 0000000..4cecf1c --- /dev/null +++ b/Assets/Samples~.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1008dc07e5044140babef9a59886d01b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Samples~/IAAAdDebugSample.meta b/Assets/Samples~/IAAAdDebugSample.meta new file mode 100644 index 0000000..24bdffa --- /dev/null +++ b/Assets/Samples~/IAAAdDebugSample.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5aae7ed566c84b00b0c42455e7c704b3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Samples~/IAAAdDebugSample/Configs.meta b/Assets/Samples~/IAAAdDebugSample/Configs.meta new file mode 100644 index 0000000..2326693 --- /dev/null +++ b/Assets/Samples~/IAAAdDebugSample/Configs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0c80b68eb57e4755b9385e066e926f26 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Samples~/IAAAdDebugSample/README.md b/Assets/Samples~/IAAAdDebugSample/README.md new file mode 100644 index 0000000..34262aa --- /dev/null +++ b/Assets/Samples~/IAAAdDebugSample/README.md @@ -0,0 +1,5 @@ +# TapADN IAA Ad Debug Sample + +This optional sample is intentionally lightweight. Import it in a host project, create an `ADConfig`, and initialize with `TapadnCommercialization.InitADManager(...)` to validate rewarded, interstitial, and splash slots. + +The main package does not import debug scenes or editor panels by default. diff --git a/Assets/Samples~/IAAAdDebugSample/README.md.meta b/Assets/Samples~/IAAAdDebugSample/README.md.meta new file mode 100644 index 0000000..cd201b0 --- /dev/null +++ b/Assets/Samples~/IAAAdDebugSample/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f23c6cf6cac240bbaa767a723ae8f313 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Samples~/IAAAdDebugSample/Runtime.meta b/Assets/Samples~/IAAAdDebugSample/Runtime.meta new file mode 100644 index 0000000..f197b62 --- /dev/null +++ b/Assets/Samples~/IAAAdDebugSample/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5b2644e7dd034f42bc57d2033fd01cda +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter.asmdef b/Assets/Tapadn_Adapter.asmdef new file mode 100644 index 0000000..9a4ddcf --- /dev/null +++ b/Assets/Tapadn_Adapter.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Tapadn_Adapter", + "references": [ + "Runtime.Adaggregator", + "Dirichlet.Mediation.Runtime" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Tapadn_Adapter.asmdef.meta b/Assets/Tapadn_Adapter.asmdef.meta new file mode 100644 index 0000000..5b8b61f --- /dev/null +++ b/Assets/Tapadn_Adapter.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5346bec840144a6f8b806ea11c6235ba +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter.meta b/Assets/Tapadn_Adapter.meta new file mode 100644 index 0000000..86f6264 --- /dev/null +++ b/Assets/Tapadn_Adapter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4af77eef1d5f4156b2b96eeb89671fb1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Editor.meta b/Assets/Tapadn_Adapter/Editor.meta new file mode 100644 index 0000000..15557b0 --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 95121de0d8184f92a4184be55ea40e5b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs b/Assets/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs new file mode 100644 index 0000000..c18f47c --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs @@ -0,0 +1,285 @@ +#if UNITY_ANDROID +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using UnityEditor; +using UnityEditor.Android; +using UnityEngine; + +namespace Tapadn_Adapter.Editor +{ + public sealed class TapadnBuildAndroidProcess : IPostGenerateGradleAndroidProject + { + private static readonly XNamespace AndroidNamespace = "http://schemas.android.com/apk/res/android"; + private static readonly XNamespace ToolsNamespace = "http://schemas.android.com/tools"; + + public int callbackOrder => 180; + + public void OnPostGenerateGradleAndroidProject(string path) + { + ProcessGradleProperties(path); + ProcessAndroidManifest(path); + } + + public static void ProcessAndroidManifest(string path) + { + var unityManifestPath = Path.Combine(path, "src/main/AndroidManifest.xml"); + var launcherManifestPath = Path.Combine(path, "../launcher/src/main/AndroidManifest.xml"); + var providerTargetManifestPath = File.Exists(launcherManifestPath) ? launcherManifestPath : unityManifestPath; + + if (!File.Exists(unityManifestPath)) + { + Debug.LogWarning($"[TapADN] AndroidManifest.xml is missing: {unityManifestPath}"); + return; + } + + var unityManifest = XDocument.Load(unityManifestPath); + EnsureManifestPatch(unityManifest, true, false); + unityManifest.Save(unityManifestPath); + + if (File.Exists(providerTargetManifestPath)) + { + var providerManifest = XDocument.Load(providerTargetManifestPath); + EnsureManifestPatch(providerManifest, false, true); + providerManifest.Save(providerTargetManifestPath); + } + + var resXmlPath = Path.Combine(path, "src/main/res/xml"); + Directory.CreateDirectory(resXmlPath); + var sourceXmlPath = GetEditorAssetPath("tapad_ad_file_path.xml"); + if (!string.IsNullOrEmpty(sourceXmlPath) && File.Exists(sourceXmlPath)) + { + File.Copy(sourceXmlPath, Path.Combine(resXmlPath, "tapad_ad_file_path.xml"), true); + } + } + + private static void EnsureManifestPatch(XDocument manifest, bool includeWechatActivity, bool includeTapadProvider) + { + var manifestElement = manifest.Element("manifest"); + if (manifestElement == null) + { + return; + } + + EnsureNamespace(manifestElement, "tools", ToolsNamespace.NamespaceName); + + EnsurePermission(manifestElement, "android.permission.INTERNET"); + EnsurePermission(manifestElement, "android.permission.ACCESS_NETWORK_STATE"); + EnsurePermission(manifestElement, "android.permission.READ_PHONE_STATE"); + EnsurePermission(manifestElement, "android.permission.QUERY_ALL_PACKAGES"); + EnsurePermission(manifestElement, "android.permission.REQUEST_INSTALL_PACKAGES"); + EnsurePermission(manifestElement, "android.permission.BLUETOOTH"); + EnsurePermission(manifestElement, "android.permission.BLUETOOTH_CONNECT"); + EnsurePermission(manifestElement, "android.permission.ACCESS_FINE_LOCATION"); + EnsurePermission(manifestElement, "android.permission.ACCESS_COARSE_LOCATION"); + EnsurePermission(manifestElement, "android.permission.POST_NOTIFICATIONS"); + EnsureWechatQueries(manifestElement); + + var applicationElement = manifestElement.Element("application"); + if (applicationElement == null) + { + applicationElement = new XElement("application"); + manifestElement.Add(applicationElement); + } + + SetUnityActivitySingleTop(applicationElement); + + if (includeWechatActivity) + { + EnsureWechatEntryActivity(applicationElement); + } + + if (includeTapadProvider) + { + EnsureTapadFileProvider(applicationElement); + SetOrAddAttribute(applicationElement, AndroidNamespace + "allowBackup", "false"); + MergeToolsReplaceAttribute(applicationElement, "android:allowBackup"); + } + } + + private static void EnsurePermission(XElement manifestElement, string permissionName) + { + var exists = manifestElement.Elements("uses-permission") + .Any(element => element.Attribute(AndroidNamespace + "name")?.Value == permissionName); + if (exists) + { + return; + } + + var permission = new XElement("uses-permission"); + permission.Add(new XAttribute(AndroidNamespace + "name", permissionName)); + if (permissionName == "android.permission.BLUETOOTH_CONNECT" || + permissionName == "android.permission.POST_NOTIFICATIONS") + { + permission.Add(new XAttribute(ToolsNamespace + "targetApi", permissionName.EndsWith("CONNECT") ? "s" : "33")); + } + + manifestElement.Add(permission); + } + + private static void EnsureWechatQueries(XElement manifestElement) + { + var queries = manifestElement.Elements("queries").FirstOrDefault(); + if (queries == null) + { + queries = new XElement("queries"); + manifestElement.Add(queries); + } + + var exists = queries.Elements("package") + .Any(element => element.Attribute(AndroidNamespace + "name")?.Value == "com.tencent.mm"); + if (!exists) + { + var packageElement = new XElement("package"); + packageElement.Add(new XAttribute(AndroidNamespace + "name", "com.tencent.mm")); + queries.Add(packageElement); + } + } + + private static void EnsureWechatEntryActivity(XElement applicationElement) + { + RemoveElementsByAndroidName(applicationElement, "activity", ".wxapi.WXEntryActivity"); + + var activity = new XElement("activity"); + activity.Add(new XAttribute(AndroidNamespace + "name", ".wxapi.WXEntryActivity")); + activity.Add(new XAttribute(AndroidNamespace + "label", "@string/app_name")); + activity.Add(new XAttribute(AndroidNamespace + "theme", "@android:style/Theme.Translucent.NoTitleBar")); + activity.Add(new XAttribute(AndroidNamespace + "exported", "true")); + activity.Add(new XAttribute(AndroidNamespace + "taskAffinity", Application.identifier)); + activity.Add(new XAttribute(AndroidNamespace + "launchMode", "singleTop")); + applicationElement.Add(activity); + } + + private static void EnsureTapadFileProvider(XElement applicationElement) + { + RemoveElementsByAndroidName(applicationElement, "provider", "com.tapsdk.tapad.internal.TapADFileProvider"); + + var provider = new XElement("provider"); + provider.Add(new XAttribute(AndroidNamespace + "name", "com.tapsdk.tapad.internal.TapADFileProvider")); + provider.Add(new XAttribute(AndroidNamespace + "authorities", "${applicationId}.com.tds.ad.fileprovider")); + provider.Add(new XAttribute(AndroidNamespace + "exported", "false")); + provider.Add(new XAttribute(AndroidNamespace + "grantUriPermissions", "true")); + provider.Add(new XAttribute(ToolsNamespace + "replace", "android:authorities")); + + var metadata = new XElement("meta-data"); + metadata.Add(new XAttribute(AndroidNamespace + "name", "android.support.FILE_PROVIDER_PATHS")); + metadata.Add(new XAttribute(AndroidNamespace + "resource", "@xml/tapad_ad_file_path")); + provider.Add(metadata); + + applicationElement.Add(provider); + } + + private static void SetUnityActivitySingleTop(XElement applicationElement) + { + foreach (var activity in applicationElement.Elements("activity")) + { + var name = activity.Attribute(AndroidNamespace + "name")?.Value; + if (name == "com.unity3d.player.UnityPlayerActivity") + { + SetOrAddAttribute(activity, AndroidNamespace + "launchMode", "singleTop"); + } + } + } + + private static void RemoveElementsByAndroidName(XElement parent, string localName, string androidName) + { + parent.Elements(localName) + .Where(element => element.Attribute(AndroidNamespace + "name")?.Value == androidName) + .ToList() + .ForEach(element => element.Remove()); + } + + private static void ProcessGradleProperties(string path) + { + var root = Directory.GetParent(path)?.FullName; + if (string.IsNullOrEmpty(root)) + { + return; + } + + var gradlePropertiesPath = Path.Combine(root, "gradle.properties"); + var lines = File.Exists(gradlePropertiesPath) + ? File.ReadAllLines(gradlePropertiesPath).ToList() + : new List(); + + UpsertProperty(lines, "android.useAndroidX", "true"); + UpsertProperty(lines, "android.enableJetifier", "true"); + + File.WriteAllText(gradlePropertiesPath, string.Join("\n", lines) + "\n"); + } + + private static void UpsertProperty(List lines, string key, string value) + { + var prefix = key + "="; + var index = lines.FindIndex(line => line.TrimStart().StartsWith(prefix)); + if (index >= 0) + { + lines[index] = prefix + value; + return; + } + + lines.Add(prefix + value); + } + + private static void SetOrAddAttribute(XElement element, XName attributeName, string value) + { + var attribute = element.Attribute(attributeName); + if (attribute == null) + { + element.Add(new XAttribute(attributeName, value)); + return; + } + + attribute.SetValue(value); + } + + private static void MergeToolsReplaceAttribute(XElement element, params string[] replaceValues) + { + var replaceAttributeName = ToolsNamespace + "replace"; + var mergedValues = new List(); + var currentValue = element.Attribute(replaceAttributeName)?.Value; + if (!string.IsNullOrWhiteSpace(currentValue)) + { + mergedValues.AddRange(currentValue.Split(',') + .Select(value => value.Trim()) + .Where(value => !string.IsNullOrWhiteSpace(value))); + } + + foreach (var replaceValue in replaceValues) + { + if (!mergedValues.Contains(replaceValue)) + { + mergedValues.Add(replaceValue); + } + } + + element.SetAttributeValue(replaceAttributeName, string.Join(",", mergedValues)); + } + + private static void EnsureNamespace(XElement element, string prefix, string namespaceName) + { + var namespaceAttributeName = XNamespace.Xmlns + prefix; + if (element.Attribute(namespaceAttributeName) == null) + { + element.Add(new XAttribute(namespaceAttributeName, namespaceName)); + } + } + + private static string GetEditorAssetPath(string fileName) + { + var guids = AssetDatabase.FindAssets(Path.GetFileNameWithoutExtension(fileName)); + foreach (var guid in guids) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + if (Path.GetFileName(path) == fileName) + { + return path; + } + } + + return null; + } + } +} +#endif diff --git a/Assets/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs.meta b/Assets/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs.meta new file mode 100644 index 0000000..0bc3446 --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor/TapadnBuildAndroidProcess.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ab0342739d64cabbf5360a6f49250a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef b/Assets/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef new file mode 100644 index 0000000..876b75e --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef @@ -0,0 +1,15 @@ +{ + "name": "Tapadn_Adapter.Editor", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef.meta b/Assets/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef.meta new file mode 100644 index 0000000..69582dd --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor/Tapadn_Adapter.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0d92d8c493204ca298403fceb36f5a71 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Editor/WXDependencies.xml b/Assets/Tapadn_Adapter/Editor/WXDependencies.xml new file mode 100644 index 0000000..ece3676 --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor/WXDependencies.xml @@ -0,0 +1,11 @@ + + + + + + https://maven.aliyun.com/repository/public + https://repo.maven.apache.org/maven2 + + + + diff --git a/Assets/Tapadn_Adapter/Editor/WXDependencies.xml.meta b/Assets/Tapadn_Adapter/Editor/WXDependencies.xml.meta new file mode 100644 index 0000000..a65ec31 --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor/WXDependencies.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ea7cb8d4cc2e4dce8859500b44a86028 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Editor/tapad_ad_file_path.xml b/Assets/Tapadn_Adapter/Editor/tapad_ad_file_path.xml new file mode 100644 index 0000000..304aad0 --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor/tapad_ad_file_path.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Assets/Tapadn_Adapter/Editor/tapad_ad_file_path.xml.meta b/Assets/Tapadn_Adapter/Editor/tapad_ad_file_path.xml.meta new file mode 100644 index 0000000..324e679 --- /dev/null +++ b/Assets/Tapadn_Adapter/Editor/tapad_ad_file_path.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dcb4be7a15dd43aaa4862ab35f8b309b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime.meta b/Assets/Tapadn_Adapter/Runtime.meta new file mode 100644 index 0000000..c257b52 --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9c09f845d424a529010aa7f15b05e9c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts.meta b/Assets/Tapadn_Adapter/Runtime/Scripts.meta new file mode 100644 index 0000000..1a88e9e --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a07b2d839f4748b0b52bd5bcb950f111 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs new file mode 100644 index 0000000..2ff8d1c --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs @@ -0,0 +1,75 @@ +using System; +using Dirichlet.Mediation; +using Runtime.ADAggregator; +using UnityEngine; + +public sealed class TapadnAdController : IAdController +{ + public static TapadnControllerOptions CurrentOptions { get; private set; } + public static string LastSdkVersion { get; private set; } + public static string LastInitError { get; private set; } + + private Action _maskAction; + private Action _logEventAction; + private ADConfig _adConfig; + private TapadnControllerOptions _options; + + public void Init(ADConfig adConfig, object[] args) + { + _adConfig = adConfig; + _options = TapadnControllerOptions.Resolve(adConfig, args); + CurrentOptions = _options; + + var sdkConfig = _options.BuildSdkConfig(); + DirichletSdk.Init( + sdkConfig, + result => + { + LastInitError = null; + LastSdkVersion = DirichletSdk.GetVersion(); + EventLog("tapadn_init", "success", result?.Message); + Debug.Log($"[TapADN] Init success. version={LastSdkVersion}, message={result?.Message}"); + if (_options.RequestPermissionOnInit) + { + DirichletSdk.RequestPermissionIfNecessary(); + } + }, + error => + { + LastInitError = $"{error?.Code}:{error?.Message}"; + EventLog("tapadn_init_error", error?.Code ?? "unknown", error?.Message); + Debug.LogError($"[TapADN] Init failed. code={error?.Code}, message={error?.Message}"); + }); + } + + public void SetListener(Action adMaskAction, Action logEventAction) + { + _maskAction = adMaskAction; + _logEventAction = logEventAction; + } + + public ADPlayer CreateAdPlayer(AD_Type type) + { + switch (type) + { + case AD_Type.AwardVideo: + return new TapadnAwardVideoPlayer().Init(_adConfig?.BaseAwardAdKeyValue?.value); + case AD_Type.Interaction: + return new TapadnInteractionPlayer().Init(_adConfig?.BaseInteractionAdKeyValue?.value); + case AD_Type.Splash: + return new TapadnSplashPlayer().Init(_adConfig?.BaseSplashAdKeyValue?.value); + default: + return null; + } + } + + public void EventLog(string eventTable, string eventValue, string eventMessage = null) + { + _logEventAction?.Invoke(eventTable, eventValue); + } + + public void SetMask(bool isOpen) + { + _maskAction?.Invoke(isOpen); + } +} diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs.meta b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs.meta new file mode 100644 index 0000000..dfd8a5c --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a08780bf18a349b58a35e2dfb79d0743 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs new file mode 100644 index 0000000..4dab42a --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs @@ -0,0 +1,55 @@ +using System; +using Dirichlet.Mediation; +using Runtime.ADAggregator; +using UnityEngine; + +internal static class TapadnAdRequestFactory +{ + public static bool TryParseSlotId(string slotId, out long parsed) + { + return long.TryParse(slotId, out parsed) && parsed > 0; + } + + public static DirichletAdRequest BuildRewarded(string slotId, TapadnControllerOptions options) + { + var builder = CreateBaseBuilder(slotId) + .WithUserId(ADManager.Instance.UserId) + .WithRewardName(options?.RewardName ?? "reward") + .WithRewardAmount(options?.RewardAmount ?? 1); + + return builder.Build(); + } + + public static DirichletAdRequest BuildInterstitial(string slotId, TapadnControllerOptions options) + { + return ApplyExpressSize(CreateBaseBuilder(slotId), options).Build(); + } + + public static DirichletAdRequest BuildSplash(string slotId, TapadnControllerOptions options) + { + var builder = CreateBaseBuilder(slotId); + var width = options?.ExpressWidth ?? Screen.width; + var height = options?.ExpressHeight ?? Screen.height; + return builder.WithExpressViewSize(width, height).Build(); + } + + private static DirichletAdRequest.Builder CreateBaseBuilder(string slotId) + { + if (!TryParseSlotId(slotId, out var parsed)) + { + throw new ArgumentException($"TapADN slot id must be a positive integer. Current value: {slotId}", nameof(slotId)); + } + + return new DirichletAdRequest.Builder().WithSpaceId(parsed); + } + + private static DirichletAdRequest.Builder ApplyExpressSize(DirichletAdRequest.Builder builder, TapadnControllerOptions options) + { + if (options?.ExpressWidth != null || options?.ExpressHeight != null) + { + builder.WithExpressViewSize(options?.ExpressWidth ?? Screen.width, options?.ExpressHeight ?? Screen.height); + } + + return builder; + } +} diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs.meta b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs.meta new file mode 100644 index 0000000..a0e1cd0 --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAdRequestFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 01ac342b5e1c49cf87b649eddcd34f7c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs new file mode 100644 index 0000000..1bc9a83 --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs @@ -0,0 +1,159 @@ +using System; +using Dirichlet.Mediation; +using Runtime.ADAggregator; +using UnityEngine; + +public sealed class TapadnAwardVideoPlayer : ADPlayer, IDirichletRewardVideoAutoAdListener +{ + private DirichletAdNative _adNative; + private DirichletRewardVideoAd _loadedAd; + private bool _rewardVerified; + + public override int MaxLoadAttempts => TapadnAdController.CurrentOptions?.RewardedMaxLoadAttempts ?? base.MaxLoadAttempts; + public override float LoadRetryDelaySeconds => Math.Max(0f, (TapadnAdController.CurrentOptions?.RewardedLoadRetryDelayMs ?? 500) / 1000f); + public override float ShowPendingTimeoutSeconds => Math.Max(1f, (TapadnAdController.CurrentOptions?.RewardedShowTimeoutMs ?? 20000) / 1000f); + public override bool AutoPreloadOnInit => TapadnAdController.CurrentOptions?.RewardedPrewarmOnInit ?? false; + + public override void OnInit() + { + _adNative = DirichletAdManager.CreateAdNative(); + } + + public override bool IsReadly() + { + if (UseAutoLoad()) + { + return TapadnAdRequestFactory.TryParseSlotId(Key, out _); + } + + if (_loadedAd != null && _loadedAd.IsLoaded && _loadedAd.IsValid) + { + curState = 2; + return true; + } + + return false; + } + + public override void LoadAD() + { + if (!TapadnAdRequestFactory.TryParseSlotId(Key, out _)) + { + Debug.LogError($"[TapADN] Invalid rewarded slot id: {Key}"); + curState = 0; + return; + } + + if (UseAutoLoad()) + { + curState = 2; + try + { + _adNative.PreLoad(TapadnAdRequestFactory.BuildRewarded(Key, TapadnAdController.CurrentOptions), 3); + } + catch (Exception exception) + { + Debug.LogWarning($"[TapADN] Rewarded preload skipped: {exception.Message}"); + } + return; + } + + if (curState == 1 || IsReadly()) + { + return; + } + + curState = 1; + _adNative.LoadRewardVideoAd( + TapadnAdRequestFactory.BuildRewarded(Key, TapadnAdController.CurrentOptions), + ad => + { + _loadedAd?.Destroy(); + _loadedAd = ad; + _loadedAd.Shown += OnManualShown; + _loadedAd.Clicked += OnManualClicked; + _loadedAd.RewardVerified += OnManualRewardVerify; + _loadedAd.Closed += OnManualClosed; + curState = 2; + Debug.Log($"[TapADN] Rewarded loaded. slot={Key}"); + }, + error => + { + curState = 0; + Debug.LogError($"[TapADN] Rewarded load failed. code={error.Code}, message={error.Message}"); + }); + } + + public override void ShowAD(Action onClose, Action onVideoComplete) + { + adListener.onClose = onClose; + adListener.onVideoComplete = onVideoComplete; + _rewardVerified = false; + curState = 0; + + if (UseAutoLoad()) + { + _adNative.ShowRewardVideoAutoAd(TapadnAdRequestFactory.BuildRewarded(Key, TapadnAdController.CurrentOptions), this); + return; + } + + if (_loadedAd == null || !_loadedAd.Show()) + { + adListener.OnShowError(); + return; + } + } + + public void OnError(DirichletError error) + { + curState = 0; + Debug.LogError($"[TapADN] Rewarded show failed. code={error?.Code}, message={error?.Message}"); + adListener.OnShowError(); + } + + public void OnAdShow() + { + NotifyShowStarted(); + } + + public void OnAdClose() + { + adListener.OnRewardVerify(_rewardVerified, TapadnAdController.CurrentOptions?.RewardAmount ?? 1, TapadnAdController.CurrentOptions?.RewardName ?? string.Empty); + adListener.OnAdClose(); + } + + public void OnRewardVerify(DirichletRewardVerificationEventArgs args) + { + _rewardVerified = args != null && args.IsVerified; + } + + public void OnAdClick() + { + } + + private bool UseAutoLoad() + { + return TapadnAdController.CurrentOptions?.RewardedAutoLoad ?? true; + } + + private void OnManualShown() + { + NotifyShowStarted(); + } + + private void OnManualClicked() + { + } + + private void OnManualRewardVerify(DirichletRewardVerificationEventArgs args) + { + _rewardVerified = args != null && args.IsVerified; + } + + private void OnManualClosed() + { + _loadedAd?.Destroy(); + _loadedAd = null; + OnAdClose(); + } +} diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs.meta b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs.meta new file mode 100644 index 0000000..10f606c --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnAwardVideoPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb7643101da94ca69dc8f99128d4e759 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs new file mode 100644 index 0000000..bfc1a0c --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs @@ -0,0 +1,34 @@ +using System; +using Runtime.ADAggregator; + +public static class TapadnCommercialization +{ + public static TapadnAdController CreateController() + { + return new TapadnAdController(); + } + + public static void InitADManager(Action onCallback, string userId, ADConfig adConfig, params object[] args) + { + ADManager.Instance.Init(onCallback, userId, adConfig, CreateController(), args); + } + + public static ADConfig CreateConfig( + string mediaId, + string mediaKey, + string mediaName, + string rewardSlotId, + string interstitialSlotId = null, + string splashSlotId = null) + { + var config = UnityEngine.ScriptableObject.CreateInstance(); + config.ConfigName = "TapADN"; + config.Id = mediaId; + config.Key = mediaKey; + config.Key2 = mediaName; + config.BaseAwardAdKeyValue = new AdKeyValue { key = "reward", value = rewardSlotId }; + config.BaseInteractionAdKeyValue = new AdKeyValue { key = "interaction", value = interstitialSlotId }; + config.BaseSplashAdKeyValue = new AdKeyValue { key = "splash", value = splashSlotId }; + return config; + } +} diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs.meta b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs.meta new file mode 100644 index 0000000..323c287 --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnCommercialization.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 295d10d222a8496e872b02eff3556e86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs new file mode 100644 index 0000000..49915a4 --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Dirichlet.Mediation; +using Runtime.ADAggregator; +using UnityEngine; + +public sealed class TapadnControllerOptions +{ + public const string MediaNameKey = "tapadn.media_name"; + public const string MediaIdKey = "tapadn.media_id"; + public const string MediaKeyKey = "tapadn.media_key"; + public const string ChannelKey = "tapadn.channel"; + public const string SubChannelKey = "tapadn.sub_channel"; + public const string DebugKey = "tapadn.debug"; + public const string TapClientIdKey = "tapadn.tap_client_id"; + public const string ShakeEnabledKey = "tapadn.shake_enabled"; + public const string CustomConfigJsonKey = "tapadn.custom_config_json"; + public const string DataJsonKey = "tapadn.data_json"; + public const string ATagsKey = "tapadn.atags"; + public const string AllowIdfaAccessKey = "tapadn.allow_idfa_access"; + public const string RequestPermissionOnInitKey = "tapadn.request_permission_on_init"; + public const string RewardedAutoLoadKey = "tapadn.rewarded_auto_load"; + public const string RewardedPrewarmOnInitKey = "tapadn.rewarded_prewarm_on_init"; + public const string RewardedMaxLoadAttemptsKey = "tapadn.rewarded_max_load_attempts"; + public const string RewardedLoadRetryDelayMsKey = "tapadn.rewarded_load_retry_delay_ms"; + public const string RewardedShowTimeoutMsKey = "tapadn.rewarded_show_timeout_ms"; + public const string RewardNameKey = "tapadn.reward_name"; + public const string RewardAmountKey = "tapadn.reward_amount"; + public const string InterstitialAutoLoadKey = "tapadn.interstitial_auto_load"; + public const string InterstitialPrewarmOnInitKey = "tapadn.interstitial_prewarm_on_init"; + public const string InterstitialMaxLoadAttemptsKey = "tapadn.interstitial_max_load_attempts"; + public const string InterstitialLoadRetryDelayMsKey = "tapadn.interstitial_load_retry_delay_ms"; + public const string InterstitialShowTimeoutMsKey = "tapadn.interstitial_show_timeout_ms"; + public const string SplashAutoLoadKey = "tapadn.splash_auto_load"; + public const string SplashPrewarmOnInitKey = "tapadn.splash_prewarm_on_init"; + public const string SplashMaxLoadAttemptsKey = "tapadn.splash_max_load_attempts"; + public const string SplashLoadRetryDelayMsKey = "tapadn.splash_load_retry_delay_ms"; + public const string SplashShowTimeoutMsKey = "tapadn.splash_show_timeout_ms"; + public const string ExpressWidthKey = "tapadn.express_width"; + public const string ExpressHeightKey = "tapadn.express_height"; + + public long MediaId { get; set; } + public string MediaName { get; set; } + public string MediaKey { get; set; } + public string Channel { get; set; } = "default"; + public string SubChannel { get; set; } + public bool Debug { get; set; } + public string TapClientId { get; set; } + public bool ShakeEnabled { get; set; } = true; + public string CustomConfigJson { get; set; } + public string DataJson { get; set; } + public string ATags { get; set; } + public bool AllowIDFAAccess { get; set; } = true; + public bool RequestPermissionOnInit { get; set; } + public bool RewardedAutoLoad { get; set; } = true; + public bool RewardedPrewarmOnInit { get; set; } + public int RewardedMaxLoadAttempts { get; set; } = 1; + public int RewardedLoadRetryDelayMs { get; set; } = 500; + public int RewardedShowTimeoutMs { get; set; } = 20000; + public string RewardName { get; set; } = "reward"; + public int RewardAmount { get; set; } = 1; + public bool InterstitialAutoLoad { get; set; } = true; + public bool InterstitialPrewarmOnInit { get; set; } + public int InterstitialMaxLoadAttempts { get; set; } = 1; + public int InterstitialLoadRetryDelayMs { get; set; } = 500; + public int InterstitialShowTimeoutMs { get; set; } = 20000; + public bool SplashAutoLoad { get; set; } = true; + public bool SplashPrewarmOnInit { get; set; } + public int SplashMaxLoadAttempts { get; set; } = 1; + public int SplashLoadRetryDelayMs { get; set; } = 500; + public int SplashShowTimeoutMs { get; set; } = 20000; + public int? ExpressWidth { get; set; } + public int? ExpressHeight { get; set; } + + public static TapadnControllerOptions Resolve(ADConfig adConfig, object[] args) + { + var options = new TapadnControllerOptions(); + options.ApplyAdConfig(adConfig); + options.ApplyLegacyArgs(args); + + if (args == null) + { + return options; + } + + foreach (var arg in args) + { + switch (arg) + { + case TapadnControllerOptions explicitOptions: + options.ApplyExplicitOptions(explicitOptions); + break; + case IDictionary dictionary: + options.ApplyDictionary(dictionary); + break; + } + } + + return options; + } + + public DirichletAdConfig BuildSdkConfig() + { + return new DirichletAdConfig.Builder() + .WithMediaId(MediaId) + .WithMediaName(string.IsNullOrWhiteSpace(MediaName) ? Application.productName : MediaName) + .WithMediaKey(MediaKey) + .WithGameChannel(string.IsNullOrWhiteSpace(Channel) ? "default" : Channel) + .WithSubChannel(SubChannel) + .EnableDebug(Debug) + .WithTapClientId(TapClientId) + .ShakeEnabled(ShakeEnabled) + .WithCustomConfigJson(CustomConfigJson) + .WithDataJson(DataJson) + .AllowIDFAAccess(AllowIDFAAccess) + .WithATags(ATags) + .Build(); + } + + private void ApplyAdConfig(ADConfig adConfig) + { + if (adConfig == null) + { + return; + } + + MediaId = ParseLong(adConfig.Id) ?? MediaId; + MediaKey = string.IsNullOrWhiteSpace(adConfig.Key) ? MediaKey : adConfig.Key.Trim(); + MediaName = string.IsNullOrWhiteSpace(adConfig.Key2) ? MediaName : adConfig.Key2.Trim(); + ApplyCommonKeyValues(adConfig.CommonKeyValues); + } + + private void ApplyCommonKeyValues(List keyValues) + { + if (keyValues == null || keyValues.Count == 0) + { + return; + } + + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var keyValue in keyValues) + { + if (keyValue == null || string.IsNullOrWhiteSpace(keyValue.key)) + { + continue; + } + + map[keyValue.key.Trim()] = keyValue.value; + } + + ApplyMap(map); + } + + private void ApplyLegacyArgs(object[] args) + { + if (args == null || args.Length == 0) + { + return; + } + + if (args.Length > 0 && args[0] is string channel) + { + Channel = channel; + } + + if (args.Length > 1 && TryConvertBool(args[1], out var debug)) + { + Debug = debug; + } + } + + private void ApplyExplicitOptions(TapadnControllerOptions incoming) + { + if (incoming == null) + { + return; + } + + MediaId = incoming.MediaId > 0 ? incoming.MediaId : MediaId; + MediaName = incoming.MediaName ?? MediaName; + MediaKey = incoming.MediaKey ?? MediaKey; + Channel = incoming.Channel ?? Channel; + SubChannel = incoming.SubChannel ?? SubChannel; + Debug = incoming.Debug; + TapClientId = incoming.TapClientId ?? TapClientId; + ShakeEnabled = incoming.ShakeEnabled; + CustomConfigJson = incoming.CustomConfigJson ?? CustomConfigJson; + DataJson = incoming.DataJson ?? DataJson; + ATags = incoming.ATags ?? ATags; + AllowIDFAAccess = incoming.AllowIDFAAccess; + RequestPermissionOnInit = incoming.RequestPermissionOnInit; + RewardedAutoLoad = incoming.RewardedAutoLoad; + RewardedPrewarmOnInit = incoming.RewardedPrewarmOnInit; + RewardedMaxLoadAttempts = incoming.RewardedMaxLoadAttempts; + RewardedLoadRetryDelayMs = incoming.RewardedLoadRetryDelayMs; + RewardedShowTimeoutMs = incoming.RewardedShowTimeoutMs; + RewardName = incoming.RewardName ?? RewardName; + RewardAmount = incoming.RewardAmount; + InterstitialAutoLoad = incoming.InterstitialAutoLoad; + InterstitialPrewarmOnInit = incoming.InterstitialPrewarmOnInit; + InterstitialMaxLoadAttempts = incoming.InterstitialMaxLoadAttempts; + InterstitialLoadRetryDelayMs = incoming.InterstitialLoadRetryDelayMs; + InterstitialShowTimeoutMs = incoming.InterstitialShowTimeoutMs; + SplashAutoLoad = incoming.SplashAutoLoad; + SplashPrewarmOnInit = incoming.SplashPrewarmOnInit; + SplashMaxLoadAttempts = incoming.SplashMaxLoadAttempts; + SplashLoadRetryDelayMs = incoming.SplashLoadRetryDelayMs; + SplashShowTimeoutMs = incoming.SplashShowTimeoutMs; + ExpressWidth = incoming.ExpressWidth ?? ExpressWidth; + ExpressHeight = incoming.ExpressHeight ?? ExpressHeight; + } + + private void ApplyDictionary(IDictionary dictionary) + { + if (dictionary == null || dictionary.Count == 0) + { + return; + } + + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (DictionaryEntry entry in dictionary) + { + if (entry.Key == null) + { + continue; + } + + map[entry.Key.ToString()] = ConvertDictionaryValue(entry.Value); + } + + ApplyMap(map); + } + + private void ApplyMap(IDictionary map) + { + MediaId = GetLong(map, MediaIdKey, "media_id") ?? MediaId; + MediaName = GetString(map, MediaNameKey, "media_name") ?? MediaName; + MediaKey = GetString(map, MediaKeyKey, "media_key") ?? MediaKey; + Channel = GetString(map, ChannelKey, "channel") ?? Channel; + SubChannel = GetString(map, SubChannelKey, "sub_channel") ?? SubChannel; + Debug = GetBool(map, DebugKey, "debug") ?? Debug; + TapClientId = GetString(map, TapClientIdKey, "tap_client_id") ?? TapClientId; + ShakeEnabled = GetBool(map, ShakeEnabledKey) ?? ShakeEnabled; + CustomConfigJson = GetString(map, CustomConfigJsonKey) ?? CustomConfigJson; + DataJson = GetString(map, DataJsonKey) ?? DataJson; + ATags = GetString(map, ATagsKey) ?? ATags; + AllowIDFAAccess = GetBool(map, AllowIdfaAccessKey) ?? AllowIDFAAccess; + RequestPermissionOnInit = GetBool(map, RequestPermissionOnInitKey) ?? RequestPermissionOnInit; + RewardedAutoLoad = GetBool(map, RewardedAutoLoadKey) ?? RewardedAutoLoad; + RewardedPrewarmOnInit = GetBool(map, RewardedPrewarmOnInitKey) ?? RewardedPrewarmOnInit; + RewardedMaxLoadAttempts = GetInt(map, RewardedMaxLoadAttemptsKey) ?? RewardedMaxLoadAttempts; + RewardedLoadRetryDelayMs = GetInt(map, RewardedLoadRetryDelayMsKey) ?? RewardedLoadRetryDelayMs; + RewardedShowTimeoutMs = GetInt(map, RewardedShowTimeoutMsKey) ?? RewardedShowTimeoutMs; + RewardName = GetString(map, RewardNameKey) ?? RewardName; + RewardAmount = GetInt(map, RewardAmountKey) ?? RewardAmount; + InterstitialAutoLoad = GetBool(map, InterstitialAutoLoadKey) ?? InterstitialAutoLoad; + InterstitialPrewarmOnInit = GetBool(map, InterstitialPrewarmOnInitKey) ?? InterstitialPrewarmOnInit; + InterstitialMaxLoadAttempts = GetInt(map, InterstitialMaxLoadAttemptsKey) ?? InterstitialMaxLoadAttempts; + InterstitialLoadRetryDelayMs = GetInt(map, InterstitialLoadRetryDelayMsKey) ?? InterstitialLoadRetryDelayMs; + InterstitialShowTimeoutMs = GetInt(map, InterstitialShowTimeoutMsKey) ?? InterstitialShowTimeoutMs; + SplashAutoLoad = GetBool(map, SplashAutoLoadKey) ?? SplashAutoLoad; + SplashPrewarmOnInit = GetBool(map, SplashPrewarmOnInitKey) ?? SplashPrewarmOnInit; + SplashMaxLoadAttempts = GetInt(map, SplashMaxLoadAttemptsKey) ?? SplashMaxLoadAttempts; + SplashLoadRetryDelayMs = GetInt(map, SplashLoadRetryDelayMsKey) ?? SplashLoadRetryDelayMs; + SplashShowTimeoutMs = GetInt(map, SplashShowTimeoutMsKey) ?? SplashShowTimeoutMs; + ExpressWidth = GetInt(map, ExpressWidthKey) ?? ExpressWidth; + ExpressHeight = GetInt(map, ExpressHeightKey) ?? ExpressHeight; + } + + private static string GetString(IDictionary map, params string[] keys) + { + foreach (var key in keys) + { + if (!string.IsNullOrWhiteSpace(key) && + map.TryGetValue(key, out var value) && + !string.IsNullOrWhiteSpace(value)) + { + return value.Trim(); + } + } + + return null; + } + + private static bool? GetBool(IDictionary map, params string[] keys) + { + var value = GetString(map, keys); + return TryConvertBool(value, out var result) ? result : null; + } + + private static int? GetInt(IDictionary map, params string[] keys) + { + var value = GetString(map, keys); + return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? result : (int?)null; + } + + private static long? GetLong(IDictionary map, params string[] keys) + { + return ParseLong(GetString(map, keys)); + } + + private static long? ParseLong(string value) + { + return long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? result : (long?)null; + } + + private static bool TryConvertBool(object value, out bool result) + { + switch (value) + { + case bool boolValue: + result = boolValue; + return true; + case string stringValue: + if (bool.TryParse(stringValue, out result)) + { + return true; + } + + if (string.Equals(stringValue, "1", StringComparison.OrdinalIgnoreCase)) + { + result = true; + return true; + } + + if (string.Equals(stringValue, "0", StringComparison.OrdinalIgnoreCase)) + { + result = false; + return true; + } + + break; + } + + result = false; + return false; + } + + private static string ConvertDictionaryValue(object value) + { + if (value == null) + { + return null; + } + + if (value is string stringValue) + { + return stringValue; + } + + if (value is IEnumerable enumerable) + { + var items = new List(); + foreach (var item in enumerable) + { + if (item != null) + { + items.Add(item.ToString()); + } + } + + return string.Join(",", items.ToArray()); + } + + return value.ToString(); + } +} diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs.meta b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs.meta new file mode 100644 index 0000000..2670b5e --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnControllerOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb9de9cc991b4649826dc9683c8ec78e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs new file mode 100644 index 0000000..f276348 --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs @@ -0,0 +1,137 @@ +using System; +using Dirichlet.Mediation; +using Runtime.ADAggregator; +using UnityEngine; + +public sealed class TapadnInteractionPlayer : ADPlayer, IDirichletInterstitialAutoAdListener +{ + private DirichletAdNative _adNative; + private DirichletInterstitialAd _loadedAd; + + public override int MaxLoadAttempts => TapadnAdController.CurrentOptions?.InterstitialMaxLoadAttempts ?? base.MaxLoadAttempts; + public override float LoadRetryDelaySeconds => Math.Max(0f, (TapadnAdController.CurrentOptions?.InterstitialLoadRetryDelayMs ?? 500) / 1000f); + public override float ShowPendingTimeoutSeconds => Math.Max(1f, (TapadnAdController.CurrentOptions?.InterstitialShowTimeoutMs ?? 20000) / 1000f); + public override bool AutoPreloadOnInit => TapadnAdController.CurrentOptions?.InterstitialPrewarmOnInit ?? false; + + public override void OnInit() + { + _adNative = DirichletAdManager.CreateAdNative(); + } + + public override bool IsReadly() + { + if (UseAutoLoad()) + { + return TapadnAdRequestFactory.TryParseSlotId(Key, out _); + } + + if (_loadedAd != null && _loadedAd.IsLoaded && _loadedAd.IsValid) + { + curState = 2; + return true; + } + + return false; + } + + public override void LoadAD() + { + if (!TapadnAdRequestFactory.TryParseSlotId(Key, out _)) + { + Debug.LogError($"[TapADN] Invalid interstitial slot id: {Key}"); + curState = 0; + return; + } + + if (UseAutoLoad()) + { + curState = 2; + return; + } + + if (curState == 1 || IsReadly()) + { + return; + } + + curState = 1; + _adNative.LoadInterstitialAd( + TapadnAdRequestFactory.BuildInterstitial(Key, TapadnAdController.CurrentOptions), + ad => + { + _loadedAd?.Destroy(); + _loadedAd = ad; + _loadedAd.Shown += OnManualShown; + _loadedAd.Clicked += OnManualClicked; + _loadedAd.Closed += OnManualClosed; + curState = 2; + Debug.Log($"[TapADN] Interstitial loaded. slot={Key}"); + }, + error => + { + curState = 0; + Debug.LogError($"[TapADN] Interstitial load failed. code={error.Code}, message={error.Message}"); + }); + } + + public override void ShowAD(Action onClose, Action onVideoComplete) + { + adListener.onClose = onClose; + adListener.onVideoComplete = onVideoComplete; + curState = 0; + + if (UseAutoLoad()) + { + _adNative.ShowInterstitialAutoAd(TapadnAdRequestFactory.BuildInterstitial(Key, TapadnAdController.CurrentOptions), this); + return; + } + + if (_loadedAd == null || !_loadedAd.Show()) + { + adListener.OnShowError(); + } + } + + public void OnError(DirichletError error) + { + curState = 0; + Debug.LogError($"[TapADN] Interstitial show failed. code={error?.Code}, message={error?.Message}"); + adListener.OnShowError(); + } + + public void OnAdShow() + { + NotifyShowStarted(); + } + + public void OnAdClose() + { + adListener.OnAdClose(); + adListener.OnShowComplete(); + } + + public void OnAdClick() + { + } + + private bool UseAutoLoad() + { + return TapadnAdController.CurrentOptions?.InterstitialAutoLoad ?? true; + } + + private void OnManualShown() + { + NotifyShowStarted(); + } + + private void OnManualClicked() + { + } + + private void OnManualClosed() + { + _loadedAd?.Destroy(); + _loadedAd = null; + OnAdClose(); + } +} diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs.meta b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs.meta new file mode 100644 index 0000000..02e391f --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnInteractionPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b422dcac50a0451a92815d54329fce8b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs new file mode 100644 index 0000000..4dd9f24 --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs @@ -0,0 +1,137 @@ +using System; +using Dirichlet.Mediation; +using Runtime.ADAggregator; +using UnityEngine; + +public sealed class TapadnSplashPlayer : ADPlayer, IDirichletSplashAutoAdListener +{ + private DirichletAdNative _adNative; + private DirichletSplashAd _loadedAd; + + public override int MaxLoadAttempts => TapadnAdController.CurrentOptions?.SplashMaxLoadAttempts ?? base.MaxLoadAttempts; + public override float LoadRetryDelaySeconds => Math.Max(0f, (TapadnAdController.CurrentOptions?.SplashLoadRetryDelayMs ?? 500) / 1000f); + public override float ShowPendingTimeoutSeconds => Math.Max(1f, (TapadnAdController.CurrentOptions?.SplashShowTimeoutMs ?? 20000) / 1000f); + public override bool AutoPreloadOnInit => TapadnAdController.CurrentOptions?.SplashPrewarmOnInit ?? false; + + public override void OnInit() + { + _adNative = DirichletAdManager.CreateAdNative(); + } + + public override bool IsReadly() + { + if (UseAutoLoad()) + { + return TapadnAdRequestFactory.TryParseSlotId(Key, out _); + } + + if (_loadedAd != null && _loadedAd.IsLoaded && _loadedAd.IsValid) + { + curState = 2; + return true; + } + + return false; + } + + public override void LoadAD() + { + if (!TapadnAdRequestFactory.TryParseSlotId(Key, out _)) + { + Debug.LogError($"[TapADN] Invalid splash slot id: {Key}"); + curState = 0; + return; + } + + if (UseAutoLoad()) + { + curState = 2; + return; + } + + if (curState == 1 || IsReadly()) + { + return; + } + + curState = 1; + _adNative.LoadSplashAd( + TapadnAdRequestFactory.BuildSplash(Key, TapadnAdController.CurrentOptions), + ad => + { + _loadedAd?.Destroy(); + _loadedAd = ad; + _loadedAd.Shown += OnManualShown; + _loadedAd.Clicked += OnManualClicked; + _loadedAd.Closed += OnManualClosed; + curState = 2; + Debug.Log($"[TapADN] Splash loaded. slot={Key}"); + }, + error => + { + curState = 0; + Debug.LogError($"[TapADN] Splash load failed. code={error.Code}, message={error.Message}"); + }); + } + + public override void ShowAD(Action onClose, Action onVideoComplete) + { + adListener.onClose = onClose; + adListener.onVideoComplete = onVideoComplete; + curState = 0; + + if (UseAutoLoad()) + { + _adNative.ShowSplashAutoAd(TapadnAdRequestFactory.BuildSplash(Key, TapadnAdController.CurrentOptions), this); + return; + } + + if (_loadedAd == null || !_loadedAd.Show()) + { + adListener.OnShowError(); + } + } + + public void OnError(DirichletError error) + { + curState = 0; + Debug.LogError($"[TapADN] Splash show failed. code={error?.Code}, message={error?.Message}"); + adListener.OnShowError(); + } + + public void OnAdShow() + { + NotifyShowStarted(); + } + + public void OnAdClose() + { + adListener.OnAdClose(); + adListener.OnShowComplete(); + } + + public void OnAdClick() + { + } + + private bool UseAutoLoad() + { + return TapadnAdController.CurrentOptions?.SplashAutoLoad ?? true; + } + + private void OnManualShown() + { + NotifyShowStarted(); + } + + private void OnManualClicked() + { + } + + private void OnManualClosed() + { + _loadedAd?.Destroy(); + _loadedAd = null; + OnAdClose(); + } +} diff --git a/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs.meta b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs.meta new file mode 100644 index 0000000..c272c6d --- /dev/null +++ b/Assets/Tapadn_Adapter/Runtime/Scripts/TapadnSplashPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 372f8c753ddd48d9a9e08012999a05f4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Assets/package.json b/Assets/package.json new file mode 100644 index 0000000..f2abc3e --- /dev/null +++ b/Assets/package.json @@ -0,0 +1,33 @@ +{ + "name": "com.commercialization.tapadn", + "displayName": "Commercialization.tapadn", + "description": "TapADN / Dirichlet mediation implementation for CC-Framework.Commercialization.", + "version": "1.0.0", + "unity": "2021.1", + "license": "MIT", + "repository": { + "type": "git", + "url": "http://private.lightyears.ltd:18650/foldcc/Commercialization.tapadn" + }, + "author": { + "name": "foldcc", + "email": "lhyuau@qq.com", + "url": "https://gitee.com/foldcc" + }, + "dependencies": { + "com.foldcc.cc-framework.commercialization": "http://private.lightyears.ltd:18650/foldcc/CC-Framework.Commercialization.git#1.0.14" + }, + "samples": [ + { + "displayName": "IAA Ad Debug Sample", + "description": "Optional debug scene/config helper for validating TapADN rewarded, interstitial, and splash flows.", + "path": "Samples~/IAAAdDebugSample" + } + ], + "keywords": [ + "Framework", + "Commercialization", + "TapADN", + "Dirichlet" + ] +} diff --git a/Assets/package.json.meta b/Assets/package.json.meta new file mode 100644 index 0000000..1558e02 --- /dev/null +++ b/Assets/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c3f50282dea34d8cbc525c5becdd3b91 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/GLOBAL_DESIGN.md b/GLOBAL_DESIGN.md new file mode 100644 index 0000000..d2a4cf7 --- /dev/null +++ b/GLOBAL_DESIGN.md @@ -0,0 +1,107 @@ +# Commercialization.tapadn 全局设计 + +## 目标 + +本模块把 TapADN / Dirichlet 聚合广告 SDK 接到 `CC-Framework.Commercialization` 抽象层后面,让业务项目继续只关心 `ADManager` 和 `ADConfig`。 + +当前实现对齐 `Commercialization.topon` 的分层: + +* `CC-Framework.Commercialization`: `ADManager`、`IAdController`、`ADPlayer` 抽象。 +* `Commercialization.tapadn`: 平台 controller、广告播放器、构建自动化。 +* `DirichletMediation`: 官方 SDK 和平台桥。 + +## 目录 + +* `Assets/DirichletMediation`: 官方聚合 Unity SDK `4.2.5.0`,已删除官方 Sample。 +* `Assets/Plugins/Android`: 官方 Android AAR、Manifest、ProGuard、本地微信 OpenSDK AAR。 +* `Assets/Plugins/iOS`: 官方 iOS bridge。 +* `Assets/Tapadn_Adapter/Runtime/Scripts`: 商业化抽象层适配。 +* `Assets/Tapadn_Adapter/Editor`: Android 构建后处理和依赖声明。 +* `Assets/Samples~`: 可选调试样例预留,不随主包自动进入业务项目。 + +## Runtime 设计 + +`TapadnAdController` 实现 `IAdController`: + +* 从 `ADConfig` 和可选 `args` 解析 `TapadnControllerOptions`。 +* 构造 `DirichletAdConfig` 并调用 `DirichletSdk.Init`。 +* 根据 `AD_Type` 创建 `TapadnAwardVideoPlayer`、`TapadnInteractionPlayer`、`TapadnSplashPlayer`。 + +`TapadnCommercialization` 是便捷入口: + +* `CreateController()` 隐藏 controller 创建细节。 +* `InitADManager(...)` 由模块负责创建 controller 并交给 `ADManager`。 +* `CreateConfig(...)` 用代码生成临时 `ADConfig`,适合游戏项目不想维护 TapADN SDK 细节时使用。 + +## 广告播放器 + +默认方案:使用 TapADN auto-ad 接口。 + +原因: + +* 官方 Unity 文档推荐 Android auto-ad,接口把加载和展示合并,SDK 自己管理缓存。 +* `ADManager.AsyncAdPlayer` 已有遮罩、超时和回调收口;auto-ad 可以让业务侧首次播放少一个 preload 时序。 + +备选方案:手动 load/show。 + +* 已通过 `tapadn.rewarded_auto_load=false` 等 key 保留。 +* 手动模式保存 SDK 返回的 ad handle,并在 `Closed` 时销毁。 + +不确定点: + +* 官方文档说明 auto-ad 目前主要是 Android 能力,iOS auto-ad 会返回 `not_supported`。本模块保留手动 fallback,但真正 iOS 出包前需要用 TapADN iOS 账号和广告位做真机验证。 +* `AD_Type` 抽象层没有 Banner 类型,所以本轮没有将 TapADN Banner 暴露到 `ADManager`。如果抽象层后续新增 Banner,可以直接复用官方 `ShowBannerAutoAd`。 + +## 配置设计 + +基础字段复用 `ADConfig`: + +* `Id`: MediaId。 +* `Key`: MediaKey。 +* `Key2`: MediaName。 +* `BaseAwardAdKeyValue.value`: 激励视频 SpaceId。 +* `BaseInteractionAdKeyValue.value`: 插屏 SpaceId。 +* `BaseSplashAdKeyValue.value`: 开屏 SpaceId。 + +高级配置通过 `CommonKeyValues` 或字典传入,使用 `tapadn.*` 前缀,避免和 TopOn 现有 key 冲突。 + +## Android 构建设计 + +保留官方 `DirichletGradlePostProcessor`,它负责 Dirichlet AAR 和 Maven 依赖注入。 + +新增 `TapadnBuildAndroidProcess`,只负责本模块集成层额外需求: + +* TapADN Manifest 权限补齐。 +* `com.tapsdk.tapad.internal.TapADFileProvider` 和 `@xml/tapad_ad_file_path`。 +* 微信 OpenSDK 的 `com.tencent.mm` queries 与 `.wxapi.WXEntryActivity`。 +* AndroidX / Jetifier 开关。 + +方案选择记录: + +* 方案 A:只放 `WXDependencies.xml`,依赖宿主 EDM4U 下载微信 SDK。风险是业务项目没有 EDM4U 或未 resolve 时构建失败。 +* 方案 B:只放本地微信 AAR。风险是宿主 EDM4U 依赖图不可见。 +* 当前选择:本地 AAR + `WXDependencies.xml` 同时保留。这样最接近 TopOn 当前工程,也能覆盖无 EDM4U 的构建场景。 + +## 编辑器可见性 + +本模块不提供默认可见面板。构建自动化通过 `IPostGenerateGradleAndroidProject` 静默执行;调试能力放入 `Samples~`,由业务项目显式导入。 + +若后续需要可视化配置面板,应使用宏包裹菜单入口,例如 `COMMERCIALIZATION_TAPADN_DEBUG_MENU`,默认不在业务项目菜单里出现。 + +## UPM 与本机验证约定 + +发布包入口是 `Assets/package.json`,其中 `com.foldcc.cc-framework.commercialization` 依赖保持为远程 Git URL: + +```json +"com.foldcc.cc-framework.commercialization": "http://private.lightyears.ltd:18650/foldcc/CC-Framework.Commercialization.git#1.0.14" +``` + +当前仓库自身作为 Unity 验证工程时,可以在 `Packages/manifest.json` 使用本地 `file:` 引用: + +```json +"com.foldcc.cc-framework.commercialization": "file:CC-Framework.Commercialization/Assets" +``` + +该路径以 `Packages/manifest.json` 所在目录为基准,符合 Unity 本地 UPM package 规则;`Packages/CC-Framework.Commercialization` 只是本机验证副本,已被 `.gitignore` 排除,不进入发布包。 + +如果 batchmode 出现 `Failed to resolve packages: The "path" argument must be of type string. Received undefined`,不把它直接归因为 manifest 路径错误,也不使用 `-noUpm` 绕过。处理顺序是保存 Unity Editor log 和 `%LOCALAPPDATA%\Unity\Editor\upm.log`,确认没有并发 Unity/UPM 进程,再用官方 `Unity.exe -batchmode -quit -projectPath -logFile ` 做一次只解析 package 的验证;若仍复现,则用 Unity Hub/已打开 Editor 作为 GUI 对照入口继续看 Console 编译错误。 diff --git a/Packages/manifest.json b/Packages/manifest.json new file mode 100644 index 0000000..42efa07 --- /dev/null +++ b/Packages/manifest.json @@ -0,0 +1,45 @@ +{ + "dependencies": { + "com.foldcc.cc-framework.commercialization": "file:CC-Framework.Commercialization/Assets", + "com.unity.collab-proxy": "2.12.4", + "com.unity.feature.2d": "2.0.1", + "com.unity.ide.rider": "3.0.36", + "com.unity.ide.visualstudio": "2.0.22", + "com.unity.test-framework": "1.1.33", + "com.unity.textmeshpro": "3.0.7", + "com.unity.timeline": "1.7.7", + "com.unity.ugui": "1.0.0", + "com.unity.visualscripting": "1.9.4", + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.cloth": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.screencapture": "1.0.0", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.terrainphysics": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.umbra": "1.0.0", + "com.unity.modules.unityanalytics": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vehicles": "1.0.0", + "com.unity.modules.video": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.wind": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } +} diff --git a/ProjectSettings/AudioManager.asset b/ProjectSettings/AudioManager.asset new file mode 100644 index 0000000..27287fe --- /dev/null +++ b/ProjectSettings/AudioManager.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!11 &1 +AudioManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Volume: 1 + Rolloff Scale: 1 + Doppler Factor: 1 + Default Speaker Mode: 2 + m_SampleRate: 0 + m_DSPBufferSize: 1024 + m_VirtualVoiceCount: 512 + m_RealVoiceCount: 32 + m_SpatializerPlugin: + m_AmbisonicDecoderPlugin: + m_DisableAudio: 0 + m_VirtualizeEffects: 1 + m_RequestedDSPBufferSize: 0 diff --git a/ProjectSettings/ClusterInputManager.asset b/ProjectSettings/ClusterInputManager.asset new file mode 100644 index 0000000..e7886b2 --- /dev/null +++ b/ProjectSettings/ClusterInputManager.asset @@ -0,0 +1,6 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!236 &1 +ClusterInputManager: + m_ObjectHideFlags: 0 + m_Inputs: [] diff --git a/ProjectSettings/DynamicsManager.asset b/ProjectSettings/DynamicsManager.asset new file mode 100644 index 0000000..72d1430 --- /dev/null +++ b/ProjectSettings/DynamicsManager.asset @@ -0,0 +1,37 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!55 &1 +PhysicsManager: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_Gravity: {x: 0, y: -9.81, z: 0} + m_DefaultMaterial: {fileID: 0} + m_BounceThreshold: 2 + m_DefaultMaxDepenetrationVelocity: 10 + m_SleepThreshold: 0.005 + m_DefaultContactOffset: 0.01 + m_DefaultSolverIterations: 6 + m_DefaultSolverVelocityIterations: 1 + m_QueriesHitBackfaces: 0 + m_QueriesHitTriggers: 1 + m_EnableAdaptiveForce: 0 + m_ClothInterCollisionDistance: 0.1 + m_ClothInterCollisionStiffness: 0.2 + m_ContactsGeneration: 1 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 0 + m_ReuseCollisionCallbacks: 1 + m_ClothInterCollisionSettingsToggle: 0 + m_ClothGravity: {x: 0, y: -9.81, z: 0} + m_ContactPairsMode: 0 + m_BroadphaseType: 0 + m_WorldBounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 250, y: 250, z: 250} + m_WorldSubdivisions: 8 + m_FrictionType: 0 + m_EnableEnhancedDeterminism: 0 + m_EnableUnifiedHeightmaps: 1 + m_SolverType: 0 + m_DefaultMaxAngularSpeed: 50 diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset new file mode 100644 index 0000000..82ab0f5 --- /dev/null +++ b/ProjectSettings/EditorBuildSettings.asset @@ -0,0 +1,11 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1045 &1 +EditorBuildSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Scenes: + - enabled: 1 + path: Assets/Scenes/SampleScene.unity + guid: 2cda990e2423bbf4892e6590ba056729 + m_configObjects: {} diff --git a/ProjectSettings/EditorSettings.asset b/ProjectSettings/EditorSettings.asset new file mode 100644 index 0000000..e6f57fd --- /dev/null +++ b/ProjectSettings/EditorSettings.asset @@ -0,0 +1,40 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!159 &1 +EditorSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_SerializationMode: 2 + m_LineEndingsForNewScripts: 0 + m_DefaultBehaviorMode: 1 + m_PrefabRegularEnvironment: {fileID: 0} + m_PrefabUIEnvironment: {fileID: 0} + m_SpritePackerMode: 5 + m_SpritePackerPaddingPower: 1 + m_EtcTextureCompressorBehavior: 1 + m_EtcTextureFastCompressor: 1 + m_EtcTextureNormalCompressor: 2 + m_EtcTextureBestCompressor: 4 + m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;asmref;rsp + m_ProjectGenerationRootNamespace: + m_EnableTextureStreamingInEditMode: 1 + m_EnableTextureStreamingInPlayMode: 1 + m_AsyncShaderCompilation: 1 + m_CachingShaderPreprocessor: 1 + m_PrefabModeAllowAutoSave: 1 + m_EnterPlayModeOptionsEnabled: 0 + m_EnterPlayModeOptions: 3 + m_GameObjectNamingDigits: 1 + m_GameObjectNamingScheme: 0 + m_AssetNamingUsesSpace: 1 + m_UseLegacyProbeSampleCount: 0 + m_SerializeInlineMappingsOnOneLine: 1 + m_DisableCookiesInLightmapper: 1 + m_AssetPipelineMode: 1 + m_CacheServerMode: 0 + m_CacheServerEndpoint: + m_CacheServerNamespacePrefix: default + m_CacheServerEnableDownload: 1 + m_CacheServerEnableUpload: 1 + m_CacheServerEnableAuth: 0 + m_CacheServerEnableTls: 0 diff --git a/ProjectSettings/GraphicsSettings.asset b/ProjectSettings/GraphicsSettings.asset new file mode 100644 index 0000000..c165afb --- /dev/null +++ b/ProjectSettings/GraphicsSettings.asset @@ -0,0 +1,64 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!30 &1 +GraphicsSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_Deferred: + m_Mode: 1 + m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} + m_DeferredReflections: + m_Mode: 1 + m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} + m_ScreenSpaceShadows: + m_Mode: 1 + m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} + m_LegacyDeferred: + m_Mode: 1 + m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0} + m_DepthNormals: + m_Mode: 1 + m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} + m_MotionVectors: + m_Mode: 1 + m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} + m_LightHalo: + m_Mode: 1 + m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} + m_LensFlare: + m_Mode: 1 + m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} + m_VideoShadersIncludeMode: 2 + m_AlwaysIncludedShaders: + - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10783, guid: 0000000000000000f000000000000000, type: 0} + m_PreloadedShaders: [] + m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} + m_CustomRenderPipeline: {fileID: 0} + m_TransparencySortMode: 0 + m_TransparencySortAxis: {x: 0, y: 0, z: 1} + m_DefaultRenderingPath: 1 + m_DefaultMobileRenderingPath: 1 + m_TierSettings: [] + m_LightmapStripping: 0 + m_FogStripping: 0 + m_InstancingStripping: 0 + m_LightmapKeepPlain: 1 + m_LightmapKeepDirCombined: 1 + m_LightmapKeepDynamicPlain: 1 + m_LightmapKeepDynamicDirCombined: 1 + m_LightmapKeepShadowMask: 1 + m_LightmapKeepSubtractive: 1 + m_FogKeepLinear: 1 + m_FogKeepExp: 1 + m_FogKeepExp2: 1 + m_AlbedoSwatchInfos: [] + m_LightsUseLinearIntensity: 0 + m_LightsUseColorTemperature: 0 + m_DefaultRenderingLayerMask: 1 + m_LogWhenShaderIsCompiled: 0 diff --git a/ProjectSettings/InputManager.asset b/ProjectSettings/InputManager.asset new file mode 100644 index 0000000..b16147e --- /dev/null +++ b/ProjectSettings/InputManager.asset @@ -0,0 +1,487 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!13 &1 +InputManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Axes: + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: a + altPositiveButton: d + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: s + altPositiveButton: w + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: mouse 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: mouse 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: mouse 2 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: space + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse X + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse Y + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse ScrollWheel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 2 + joyNum: 0 + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 0 + type: 2 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 1 + type: 2 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 0 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 1 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 2 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 3 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: enter + altNegativeButton: + altPositiveButton: space + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Cancel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: escape + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Enable Debug Button 1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: joystick button 8 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Enable Debug Button 2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: backspace + altNegativeButton: + altPositiveButton: joystick button 9 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Reset + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Next + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: page down + altNegativeButton: + altPositiveButton: joystick button 5 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Previous + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: page up + altNegativeButton: + altPositiveButton: joystick button 4 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Validate + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Persistent + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: right shift + altNegativeButton: + altPositiveButton: joystick button 2 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Multiplier + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: joystick button 3 + gravity: 0 + dead: 0 + sensitivity: 0 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 2 + axis: 6 + joyNum: 0 + - serializedVersion: 3 + m_Name: Debug Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 2 + axis: 5 + joyNum: 0 diff --git a/ProjectSettings/MemorySettings.asset b/ProjectSettings/MemorySettings.asset new file mode 100644 index 0000000..5b5face --- /dev/null +++ b/ProjectSettings/MemorySettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!387306366 &1 +MemorySettings: + m_ObjectHideFlags: 0 + m_EditorMemorySettings: + m_MainAllocatorBlockSize: -1 + m_ThreadAllocatorBlockSize: -1 + m_MainGfxBlockSize: -1 + m_ThreadGfxBlockSize: -1 + m_CacheBlockSize: -1 + m_TypetreeBlockSize: -1 + m_ProfilerBlockSize: -1 + m_ProfilerEditorBlockSize: -1 + m_BucketAllocatorGranularity: -1 + m_BucketAllocatorBucketsCount: -1 + m_BucketAllocatorBlockSize: -1 + m_BucketAllocatorBlockCount: -1 + m_ProfilerBucketAllocatorGranularity: -1 + m_ProfilerBucketAllocatorBucketsCount: -1 + m_ProfilerBucketAllocatorBlockSize: -1 + m_ProfilerBucketAllocatorBlockCount: -1 + m_TempAllocatorSizeMain: -1 + m_JobTempAllocatorBlockSize: -1 + m_BackgroundJobTempAllocatorBlockSize: -1 + m_JobTempAllocatorReducedBlockSize: -1 + m_TempAllocatorSizeGIBakingWorker: -1 + m_TempAllocatorSizeNavMeshWorker: -1 + m_TempAllocatorSizeAudioWorker: -1 + m_TempAllocatorSizeCloudWorker: -1 + m_TempAllocatorSizeGfx: -1 + m_TempAllocatorSizeJobWorker: -1 + m_TempAllocatorSizeBackgroundWorker: -1 + m_TempAllocatorSizePreloadManager: -1 + m_PlatformMemorySettings: {} diff --git a/ProjectSettings/NavMeshAreas.asset b/ProjectSettings/NavMeshAreas.asset new file mode 100644 index 0000000..ad2654e --- /dev/null +++ b/ProjectSettings/NavMeshAreas.asset @@ -0,0 +1,93 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!126 &1 +NavMeshProjectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + areas: + - name: Walkable + cost: 1 + - name: Not Walkable + cost: 1 + - name: Jump + cost: 2 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + m_LastAgentTypeID: -887442657 + m_Settings: + - serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.75 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_SettingNames: + - Humanoid diff --git a/ProjectSettings/NetworkManager.asset b/ProjectSettings/NetworkManager.asset new file mode 100644 index 0000000..5dc6a83 --- /dev/null +++ b/ProjectSettings/NetworkManager.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!149 &1 +NetworkManager: + m_ObjectHideFlags: 0 + m_DebugLevel: 0 + m_Sendrate: 15 + m_AssetToPrefab: {} diff --git a/ProjectSettings/PackageManagerSettings.asset b/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 0000000..5f48647 --- /dev/null +++ b/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,36 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_EnablePreReleasePackages: 0 + m_AdvancedSettingsExpanded: 1 + m_ScopedRegistriesSettingsExpanded: 1 + m_SeeAllPackageVersions: 0 + m_DismissPreviewPackagesInUse: 0 + oneTimeWarningShown: 0 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.cn + m_Scopes: [] + m_IsDefault: 1 + m_Capabilities: 7 + m_ConfigSource: 0 + m_UserSelectedRegistryName: + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_Modified: 0 + m_ErrorMessage: + m_UserModificationsInstanceId: -836 + m_OriginalInstanceId: -838 + m_LoadAssets: 0 diff --git a/ProjectSettings/Physics2DSettings.asset b/ProjectSettings/Physics2DSettings.asset new file mode 100644 index 0000000..6cfcdda --- /dev/null +++ b/ProjectSettings/Physics2DSettings.asset @@ -0,0 +1,56 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!19 &1 +Physics2DSettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_Gravity: {x: 0, y: -9.81} + m_DefaultMaterial: {fileID: 0} + m_VelocityIterations: 8 + m_PositionIterations: 3 + m_VelocityThreshold: 1 + m_MaxLinearCorrection: 0.2 + m_MaxAngularCorrection: 8 + m_MaxTranslationSpeed: 100 + m_MaxRotationSpeed: 360 + m_BaumgarteScale: 0.2 + m_BaumgarteTimeOfImpactScale: 0.75 + m_TimeToSleep: 0.5 + m_LinearSleepTolerance: 0.01 + m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_JobOptions: + serializedVersion: 2 + useMultithreading: 0 + useConsistencySorting: 0 + m_InterpolationPosesPerJob: 100 + m_NewContactsPerJob: 30 + m_CollideContactsPerJob: 100 + m_ClearFlagsPerJob: 200 + m_ClearBodyForcesPerJob: 200 + m_SyncDiscreteFixturesPerJob: 50 + m_SyncContinuousFixturesPerJob: 50 + m_FindNearestContactsPerJob: 100 + m_UpdateTriggerContactsPerJob: 100 + m_IslandSolverCostThreshold: 100 + m_IslandSolverBodyCostScale: 1 + m_IslandSolverContactCostScale: 10 + m_IslandSolverJointCostScale: 10 + m_IslandSolverBodiesPerJob: 50 + m_IslandSolverContactsPerJob: 50 + m_SimulationMode: 0 + m_QueriesHitTriggers: 1 + m_QueriesStartInColliders: 1 + m_CallbacksOnDisable: 1 + m_ReuseCollisionCallbacks: 1 + m_AutoSyncTransforms: 0 + m_AlwaysShowColliders: 0 + m_ShowColliderSleep: 1 + m_ShowColliderContacts: 0 + m_ShowColliderAABB: 0 + m_ContactArrowScale: 0.2 + m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} + m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} + m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} + m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/ProjectSettings/PresetManager.asset b/ProjectSettings/PresetManager.asset new file mode 100644 index 0000000..67a94da --- /dev/null +++ b/ProjectSettings/PresetManager.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_DefaultPresets: {} diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset new file mode 100644 index 0000000..4610e58 --- /dev/null +++ b/ProjectSettings/ProjectSettings.asset @@ -0,0 +1,913 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!129 &1 +PlayerSettings: + m_ObjectHideFlags: 0 + serializedVersion: 26 + productGUID: a657b16e009618e43b103008d53a6cf8 + AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 + AndroidEnableSustainedPerformanceMode: 0 + defaultScreenOrientation: 4 + targetDevice: 2 + useOnDemandResources: 0 + accelerometerFrequency: 60 + companyName: DefaultCompany + productName: Commercialization.tapadn + defaultCursor: {fileID: 0} + cursorHotspot: {x: 0, y: 0} + m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} + m_ShowUnitySplashScreen: 1 + m_ShowUnitySplashLogo: 1 + m_SplashScreenOverlayOpacity: 1 + m_SplashScreenAnimation: 1 + m_SplashScreenLogoStyle: 1 + m_SplashScreenDrawMode: 0 + m_SplashScreenBackgroundAnimationZoom: 1 + m_SplashScreenLogoAnimationZoom: 1 + m_SplashScreenBackgroundLandscapeAspect: 1 + m_SplashScreenBackgroundPortraitAspect: 1 + m_SplashScreenBackgroundLandscapeUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenBackgroundPortraitUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenLogos: [] + m_VirtualRealitySplashScreen: {fileID: 0} + m_HolographicTrackingLossScreen: {fileID: 0} + defaultScreenWidth: 1920 + defaultScreenHeight: 1080 + defaultScreenWidthWeb: 960 + defaultScreenHeightWeb: 600 + m_StereoRenderingPath: 0 + m_ActiveColorSpace: 1 + unsupportedMSAAFallback: 0 + m_SpriteBatchVertexThreshold: 300 + m_MTRendering: 1 + mipStripping: 0 + numberOfMipsStripped: 0 + numberOfMipsStrippedPerMipmapLimitGroup: {} + m_StackTraceTypes: 010000000100000001000000010000000100000001000000 + iosShowActivityIndicatorOnLoading: -1 + androidShowActivityIndicatorOnLoading: -1 + iosUseCustomAppBackgroundBehavior: 0 + allowedAutorotateToPortrait: 1 + allowedAutorotateToPortraitUpsideDown: 1 + allowedAutorotateToLandscapeRight: 1 + allowedAutorotateToLandscapeLeft: 1 + useOSAutorotation: 1 + use32BitDisplayBuffer: 1 + preserveFramebufferAlpha: 0 + disableDepthAndStencilBuffers: 0 + androidStartInFullscreen: 1 + androidRenderOutsideSafeArea: 1 + androidUseSwappy: 1 + androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 + androidAutoRotationBehavior: 1 + androidPredictiveBackSupport: 1 + defaultIsNativeResolution: 1 + macRetinaSupport: 1 + runInBackground: 0 + captureSingleScreen: 0 + muteOtherAudioSources: 0 + Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 + audioSpatialExperience: 0 + deferSystemGesturesMode: 0 + hideHomeButton: 0 + submitAnalytics: 1 + usePlayerLog: 1 + dedicatedServerOptimizations: 0 + bakeCollisionMeshes: 0 + forceSingleInstance: 0 + useFlipModelSwapchain: 1 + resizableWindow: 0 + useMacAppStoreValidation: 0 + macAppStoreCategory: public.app-category.games + gpuSkinning: 0 + xboxPIXTextureCapture: 0 + xboxEnableAvatar: 0 + xboxEnableKinect: 0 + xboxEnableKinectAutoTracking: 0 + xboxEnableFitness: 0 + visibleInBackground: 1 + allowFullscreenSwitch: 1 + fullscreenMode: 1 + xboxSpeechDB: 0 + xboxEnableHeadOrientation: 0 + xboxEnableGuest: 0 + xboxEnablePIXSampling: 0 + metalFramebufferOnly: 0 + xboxOneResolution: 0 + xboxOneSResolution: 0 + xboxOneXResolution: 3 + xboxOneMonoLoggingLevel: 0 + xboxOneLoggingLevel: 1 + xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 + xboxOnePresentImmediateThreshold: 0 + switchQueueCommandMemory: 1048576 + switchQueueControlMemory: 16384 + switchQueueComputeMemory: 262144 + switchNVNShaderPoolsGranularity: 33554432 + switchNVNDefaultPoolsGranularity: 16777216 + switchNVNOtherPoolsGranularity: 16777216 + switchGpuScratchPoolGranularity: 2097152 + switchAllowGpuScratchShrinking: 0 + switchNVNMaxPublicTextureIDCount: 0 + switchNVNMaxPublicSamplerIDCount: 0 + switchNVNGraphicsFirmwareMemory: 32 + switchMaxWorkerMultiple: 8 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 + vulkanNumSwapchainBuffers: 3 + vulkanEnableSetSRGBWrite: 0 + vulkanEnablePreTransform: 0 + vulkanEnableLateAcquireNextImage: 0 + vulkanEnableCommandBufferRecycling: 1 + loadStoreDebugModeEnabled: 0 + visionOSBundleVersion: 1.0 + tvOSBundleVersion: 1.0 + bundleVersion: 1.0 + preloadedAssets: [] + metroInputSource: 0 + wsaTransparentSwapchain: 0 + m_HolographicPauseOnTrackingLoss: 1 + xboxOneDisableKinectGpuReservation: 1 + xboxOneEnable7thCore: 1 + vrSettings: + enable360StereoCapture: 0 + isWsaHolographicRemotingEnabled: 0 + enableFrameTimingStats: 0 + enableOpenGLProfilerGPURecorders: 1 + allowHDRDisplaySupport: 0 + useHDRDisplay: 0 + hdrBitDepth: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 30 + resolutionScalingMode: 0 + resetResolutionOnWindowResize: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 + applicationIdentifier: + Standalone: com.DefaultCompany.2DProject + buildNumber: + Standalone: 0 + VisionOS: 0 + iPhone: 0 + tvOS: 0 + overrideDefaultApplicationIdentifier: 1 + AndroidBundleVersionCode: 1 + AndroidMinSdkVersion: 22 + AndroidTargetSdkVersion: 0 + AndroidPreferredInstallLocation: 1 + aotOptions: + stripEngineCode: 1 + iPhoneStrippingLevel: 0 + iPhoneScriptCallOptimization: 0 + ForceInternetPermission: 0 + ForceSDCardPermission: 0 + CreateWallpaper: 0 + APKExpansionFiles: 0 + keepLoadedShadersAlive: 0 + StripUnusedMeshComponents: 0 + strictShaderVariantMatching: 0 + VertexChannelCompressionMask: 4054 + iPhoneSdkVersion: 988 + iOSSimulatorArchitecture: 0 + iOSTargetOSVersionString: 12.0 + tvOSSdkVersion: 0 + tvOSSimulatorArchitecture: 0 + tvOSRequireExtendedGameController: 0 + tvOSTargetOSVersionString: 12.0 + VisionOSSdkVersion: 0 + VisionOSTargetOSVersionString: 1.0 + uIPrerenderedIcon: 0 + uIRequiresPersistentWiFi: 0 + uIRequiresFullScreen: 1 + uIStatusBarHidden: 1 + uIExitOnSuspend: 0 + uIStatusBarStyle: 0 + appleTVSplashScreen: {fileID: 0} + appleTVSplashScreen2x: {fileID: 0} + tvOSSmallIconLayers: [] + tvOSSmallIconLayers2x: [] + tvOSLargeIconLayers: [] + tvOSLargeIconLayers2x: [] + tvOSTopShelfImageLayers: [] + tvOSTopShelfImageLayers2x: [] + tvOSTopShelfImageWideLayers: [] + tvOSTopShelfImageWideLayers2x: [] + iOSLaunchScreenType: 0 + iOSLaunchScreenPortrait: {fileID: 0} + iOSLaunchScreenLandscape: {fileID: 0} + iOSLaunchScreenBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreenFillPct: 100 + iOSLaunchScreenSize: 100 + iOSLaunchScreenCustomXibPath: + iOSLaunchScreeniPadType: 0 + iOSLaunchScreeniPadImage: {fileID: 0} + iOSLaunchScreeniPadBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreeniPadFillPct: 100 + iOSLaunchScreeniPadSize: 100 + iOSLaunchScreeniPadCustomXibPath: + iOSLaunchScreenCustomStoryboardPath: + iOSLaunchScreeniPadCustomStoryboardPath: + iOSDeviceRequirements: [] + iOSURLSchemes: [] + macOSURLSchemes: [] + iOSBackgroundModes: 0 + iOSMetalForceHardShadows: 0 + metalEditorSupport: 1 + metalAPIValidation: 1 + metalCompileShaderBinary: 0 + iOSRenderExtraFrameOnPause: 0 + iosCopyPluginsCodeInsteadOfSymlink: 0 + appleDeveloperTeamID: + iOSManualSigningProvisioningProfileID: + tvOSManualSigningProvisioningProfileID: + VisionOSManualSigningProvisioningProfileID: + iOSManualSigningProvisioningProfileType: 0 + tvOSManualSigningProvisioningProfileType: 0 + VisionOSManualSigningProvisioningProfileType: 0 + appleEnableAutomaticSigning: 0 + iOSRequireARKit: 0 + iOSAutomaticallyDetectAndAddCapabilities: 1 + appleEnableProMotion: 0 + shaderPrecisionModel: 0 + clonedFromGUID: 10ad67313f4034357812315f3c407484 + templatePackageId: com.unity.template.2d@7.0.4 + templateDefaultScene: Assets/Scenes/SampleScene.unity + useCustomMainManifest: 0 + useCustomLauncherManifest: 0 + useCustomMainGradleTemplate: 0 + useCustomLauncherGradleManifest: 0 + useCustomBaseGradleTemplate: 0 + useCustomGradlePropertiesTemplate: 0 + useCustomGradleSettingsTemplate: 0 + useCustomProguardFile: 0 + AndroidTargetArchitectures: 1 + AndroidTargetDevices: 0 + AndroidSplashScreenScale: 0 + androidSplashScreen: {fileID: 0} + AndroidKeystoreName: + AndroidKeyaliasName: + AndroidEnableArmv9SecurityFeatures: 0 + AndroidBuildApkPerCpuArchitecture: 0 + AndroidTVCompatibility: 0 + AndroidIsGame: 1 + AndroidEnableTango: 0 + androidEnableBanner: 1 + androidUseLowAccuracyLocation: 0 + androidUseCustomKeystore: 0 + m_AndroidBanners: + - width: 320 + height: 180 + banner: {fileID: 0} + androidGamepadSupportLevel: 0 + chromeosInputEmulation: 1 + AndroidMinifyRelease: 0 + AndroidMinifyDebug: 0 + AndroidValidateAppBundleSize: 1 + AndroidAppBundleSizeToValidate: 150 + m_BuildTargetIcons: [] + m_BuildTargetPlatformIcons: + - m_BuildTarget: iPhone + m_Icons: + - m_Textures: [] + m_Width: 180 + m_Height: 180 + m_Kind: 0 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 120 + m_Height: 120 + m_Kind: 0 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 167 + m_Height: 167 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 152 + m_Height: 152 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 76 + m_Height: 76 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 120 + m_Height: 120 + m_Kind: 3 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 80 + m_Height: 80 + m_Kind: 3 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 80 + m_Height: 80 + m_Kind: 3 + m_SubKind: iPad + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 3 + m_SubKind: iPad + - m_Textures: [] + m_Width: 87 + m_Height: 87 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 58 + m_Height: 58 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 29 + m_Height: 29 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 58 + m_Height: 58 + m_Kind: 1 + m_SubKind: iPad + - m_Textures: [] + m_Width: 29 + m_Height: 29 + m_Kind: 1 + m_SubKind: iPad + - m_Textures: [] + m_Width: 60 + m_Height: 60 + m_Kind: 2 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 2 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 2 + m_SubKind: iPad + - m_Textures: [] + m_Width: 20 + m_Height: 20 + m_Kind: 2 + m_SubKind: iPad + - m_Textures: [] + m_Width: 1024 + m_Height: 1024 + m_Kind: 4 + m_SubKind: App Store + - m_BuildTarget: Android + m_Icons: + - m_Textures: [] + m_Width: 432 + m_Height: 432 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 324 + m_Height: 324 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 216 + m_Height: 216 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 162 + m_Height: 162 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 108 + m_Height: 108 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 81 + m_Height: 81 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 0 + m_SubKind: + m_BuildTargetBatching: [] + m_BuildTargetShaderSettings: [] + m_BuildTargetGraphicsJobs: + - m_BuildTarget: MacStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: Switch + m_GraphicsJobs: 0 + - m_BuildTarget: MetroSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AppleTVSupport + m_GraphicsJobs: 0 + - m_BuildTarget: BJMSupport + m_GraphicsJobs: 0 + - m_BuildTarget: LinuxStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: PS4Player + m_GraphicsJobs: 0 + - m_BuildTarget: iOSSupport + m_GraphicsJobs: 0 + - m_BuildTarget: WindowsStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobs: 0 + - m_BuildTarget: LuminSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AndroidPlayer + m_GraphicsJobs: 0 + - m_BuildTarget: WebGLSupport + m_GraphicsJobs: 0 + m_BuildTargetGraphicsJobMode: [] + m_BuildTargetGraphicsAPIs: + - m_BuildTarget: AndroidPlayer + m_APIs: 150000000b000000 + m_Automatic: 1 + - m_BuildTarget: iOSSupport + m_APIs: 10000000 + m_Automatic: 1 + m_BuildTargetVRSettings: [] + m_DefaultShaderChunkSizeInMB: 16 + m_DefaultShaderChunkCount: 0 + openGLRequireES31: 0 + openGLRequireES31AEP: 0 + openGLRequireES32: 0 + m_TemplateCustomTags: {} + mobileMTRendering: + Android: 1 + iPhone: 1 + tvOS: 1 + m_BuildTargetGroupLightmapEncodingQuality: [] + m_BuildTargetGroupHDRCubemapEncodingQuality: [] + m_BuildTargetGroupLightmapSettings: [] + m_BuildTargetGroupLoadStoreDebugModeSettings: [] + m_BuildTargetNormalMapEncoding: [] + m_BuildTargetDefaultTextureCompressionFormat: + - m_BuildTarget: Android + m_Format: 3 + playModeTestRunnerEnabled: 0 + runPlayModeTestAsEditModeTest: 0 + actionOnDotNetUnhandledException: 1 + enableInternalProfiler: 0 + logObjCUncaughtExceptions: 1 + enableCrashReportAPI: 0 + cameraUsageDescription: + locationUsageDescription: + microphoneUsageDescription: + bluetoothUsageDescription: + macOSTargetOSVersion: 10.13.0 + switchNMETAOverride: + switchNetLibKey: + switchSocketMemoryPoolSize: 6144 + switchSocketAllocatorPoolSize: 128 + switchSocketConcurrencyLimit: 14 + switchScreenResolutionBehavior: 2 + switchUseCPUProfiler: 0 + switchEnableFileSystemTrace: 0 + switchLTOSetting: 0 + switchApplicationID: 0x01004b9000490000 + switchNSODependencies: + switchCompilerFlags: + switchTitleNames_0: + switchTitleNames_1: + switchTitleNames_2: + switchTitleNames_3: + switchTitleNames_4: + switchTitleNames_5: + switchTitleNames_6: + switchTitleNames_7: + switchTitleNames_8: + switchTitleNames_9: + switchTitleNames_10: + switchTitleNames_11: + switchTitleNames_12: + switchTitleNames_13: + switchTitleNames_14: + switchTitleNames_15: + switchPublisherNames_0: + switchPublisherNames_1: + switchPublisherNames_2: + switchPublisherNames_3: + switchPublisherNames_4: + switchPublisherNames_5: + switchPublisherNames_6: + switchPublisherNames_7: + switchPublisherNames_8: + switchPublisherNames_9: + switchPublisherNames_10: + switchPublisherNames_11: + switchPublisherNames_12: + switchPublisherNames_13: + switchPublisherNames_14: + switchPublisherNames_15: + switchIcons_0: {fileID: 0} + switchIcons_1: {fileID: 0} + switchIcons_2: {fileID: 0} + switchIcons_3: {fileID: 0} + switchIcons_4: {fileID: 0} + switchIcons_5: {fileID: 0} + switchIcons_6: {fileID: 0} + switchIcons_7: {fileID: 0} + switchIcons_8: {fileID: 0} + switchIcons_9: {fileID: 0} + switchIcons_10: {fileID: 0} + switchIcons_11: {fileID: 0} + switchIcons_12: {fileID: 0} + switchIcons_13: {fileID: 0} + switchIcons_14: {fileID: 0} + switchIcons_15: {fileID: 0} + switchSmallIcons_0: {fileID: 0} + switchSmallIcons_1: {fileID: 0} + switchSmallIcons_2: {fileID: 0} + switchSmallIcons_3: {fileID: 0} + switchSmallIcons_4: {fileID: 0} + switchSmallIcons_5: {fileID: 0} + switchSmallIcons_6: {fileID: 0} + switchSmallIcons_7: {fileID: 0} + switchSmallIcons_8: {fileID: 0} + switchSmallIcons_9: {fileID: 0} + switchSmallIcons_10: {fileID: 0} + switchSmallIcons_11: {fileID: 0} + switchSmallIcons_12: {fileID: 0} + switchSmallIcons_13: {fileID: 0} + switchSmallIcons_14: {fileID: 0} + switchSmallIcons_15: {fileID: 0} + switchManualHTML: + switchAccessibleURLs: + switchLegalInformation: + switchMainThreadStackSize: 1048576 + switchPresenceGroupId: + switchLogoHandling: 0 + switchReleaseVersion: 0 + switchDisplayVersion: 1.0.0 + switchStartupUserAccount: 0 + switchSupportedLanguagesMask: 0 + switchLogoType: 0 + switchApplicationErrorCodeCategory: + switchUserAccountSaveDataSize: 0 + switchUserAccountSaveDataJournalSize: 0 + switchApplicationAttribute: 0 + switchCardSpecSize: -1 + switchCardSpecClock: -1 + switchRatingsMask: 0 + switchRatingsInt_0: 0 + switchRatingsInt_1: 0 + switchRatingsInt_2: 0 + switchRatingsInt_3: 0 + switchRatingsInt_4: 0 + switchRatingsInt_5: 0 + switchRatingsInt_6: 0 + switchRatingsInt_7: 0 + switchRatingsInt_8: 0 + switchRatingsInt_9: 0 + switchRatingsInt_10: 0 + switchRatingsInt_11: 0 + switchRatingsInt_12: 0 + switchLocalCommunicationIds_0: + switchLocalCommunicationIds_1: + switchLocalCommunicationIds_2: + switchLocalCommunicationIds_3: + switchLocalCommunicationIds_4: + switchLocalCommunicationIds_5: + switchLocalCommunicationIds_6: + switchLocalCommunicationIds_7: + switchParentalControl: 0 + switchAllowsScreenshot: 1 + switchAllowsVideoCapturing: 1 + switchAllowsRuntimeAddOnContentInstall: 0 + switchDataLossConfirmation: 0 + switchUserAccountLockEnabled: 0 + switchSystemResourceMemory: 16777216 + switchSupportedNpadStyles: 22 + switchNativeFsCacheSize: 32 + switchIsHoldTypeHorizontal: 0 + switchSupportedNpadCount: 8 + switchEnableTouchScreen: 1 + switchSocketConfigEnabled: 0 + switchTcpInitialSendBufferSize: 32 + switchTcpInitialReceiveBufferSize: 64 + switchTcpAutoSendBufferSizeMax: 256 + switchTcpAutoReceiveBufferSizeMax: 256 + switchUdpSendBufferSize: 9 + switchUdpReceiveBufferSize: 42 + switchSocketBufferEfficiency: 4 + switchSocketInitializeEnabled: 1 + switchNetworkInterfaceManagerInitializeEnabled: 1 + switchDisableHTCSPlayerConnection: 0 + switchUseNewStyleFilepaths: 0 + switchUseLegacyFmodPriorities: 0 + switchUseMicroSleepForYield: 1 + switchEnableRamDiskSupport: 0 + switchMicroSleepForYieldTime: 25 + switchRamDiskSpaceSize: 12 + ps4NPAgeRating: 12 + ps4NPTitleSecret: + ps4NPTrophyPackPath: + ps4ParentalLevel: 11 + ps4ContentID: ED1633-NPXX51362_00-0000000000000000 + ps4Category: 0 + ps4MasterVersion: 01.00 + ps4AppVersion: 01.00 + ps4AppType: 0 + ps4ParamSfxPath: + ps4VideoOutPixelFormat: 0 + ps4VideoOutInitialWidth: 1920 + ps4VideoOutBaseModeInitialWidth: 1920 + ps4VideoOutReprojectionRate: 60 + ps4PronunciationXMLPath: + ps4PronunciationSIGPath: + ps4BackgroundImagePath: + ps4StartupImagePath: + ps4StartupImagesFolder: + ps4IconImagesFolder: + ps4SaveDataImagePath: + ps4SdkOverride: + ps4BGMPath: + ps4ShareFilePath: + ps4ShareOverlayImagePath: + ps4PrivacyGuardImagePath: + ps4ExtraSceSysFile: + ps4NPtitleDatPath: + ps4RemotePlayKeyAssignment: -1 + ps4RemotePlayKeyMappingDir: + ps4PlayTogetherPlayerCount: 0 + ps4EnterButtonAssignment: 2 + ps4ApplicationParam1: 0 + ps4ApplicationParam2: 0 + ps4ApplicationParam3: 0 + ps4ApplicationParam4: 0 + ps4DownloadDataSize: 0 + ps4GarlicHeapSize: 2048 + ps4ProGarlicHeapSize: 2560 + playerPrefsMaxSize: 32768 + ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ + ps4pnSessions: 1 + ps4pnPresence: 1 + ps4pnFriends: 1 + ps4pnGameCustomData: 1 + playerPrefsSupport: 0 + enableApplicationExit: 0 + resetTempFolder: 1 + restrictedAudioUsageRights: 0 + ps4UseResolutionFallback: 0 + ps4ReprojectionSupport: 0 + ps4UseAudio3dBackend: 0 + ps4UseLowGarlicFragmentationMode: 1 + ps4SocialScreenEnabled: 0 + ps4ScriptOptimizationLevel: 2 + ps4Audio3dVirtualSpeakerCount: 14 + ps4attribCpuUsage: 0 + ps4PatchPkgPath: + ps4PatchLatestPkgPath: + ps4PatchChangeinfoPath: + ps4PatchDayOne: 0 + ps4attribUserManagement: 0 + ps4attribMoveSupport: 0 + ps4attrib3DSupport: 0 + ps4attribShareSupport: 0 + ps4attribExclusiveVR: 0 + ps4disableAutoHideSplash: 0 + ps4videoRecordingFeaturesUsed: 0 + ps4contentSearchFeaturesUsed: 0 + ps4CompatibilityPS5: 0 + ps4AllowPS5Detection: 0 + ps4GPU800MHz: 1 + ps4attribEyeToEyeDistanceSettingVR: 0 + ps4IncludedModules: [] + ps4attribVROutputEnabled: 0 + monoEnv: + splashScreenBackgroundSourceLandscape: {fileID: 0} + splashScreenBackgroundSourcePortrait: {fileID: 0} + blurSplashScreenBackground: 1 + spritePackerPolicy: + webGLMemorySize: 32 + webGLExceptionSupport: 1 + webGLNameFilesAsHashes: 0 + webGLShowDiagnostics: 0 + webGLDataCaching: 1 + webGLDebugSymbols: 0 + webGLEmscriptenArgs: + webGLModulesDirectory: + webGLTemplate: APPLICATION:Default + webGLAnalyzeBuildSize: 0 + webGLUseEmbeddedResources: 0 + webGLCompressionFormat: 0 + webGLWasmArithmeticExceptions: 0 + webGLLinkerTarget: 1 + webGLThreadsSupport: 0 + webGLDecompressionFallback: 0 + webGLInitialMemorySize: 32 + webGLMaximumMemorySize: 2048 + webGLMemoryGrowthMode: 2 + webGLMemoryLinearGrowthStep: 16 + webGLMemoryGeometricGrowthStep: 0.2 + webGLMemoryGeometricGrowthCap: 96 + webGLPowerPreference: 2 + scriptingDefineSymbols: {} + additionalCompilerArguments: {} + platformArchitecture: {} + scriptingBackend: {} + il2cppCompilerConfiguration: {} + il2cppCodeGeneration: {} + managedStrippingLevel: + EmbeddedLinux: 1 + GameCoreScarlett: 1 + GameCoreXboxOne: 1 + Nintendo Switch: 1 + PS4: 1 + PS5: 1 + QNX: 1 + Stadia: 1 + VisionOS: 1 + WebGL: 1 + Windows Store Apps: 1 + XboxOne: 1 + iPhone: 1 + tvOS: 1 + incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 + allowUnsafeCode: 0 + useDeterministicCompilation: 1 + additionalIl2CppArgs: + scriptingRuntimeVersion: 1 + gcIncremental: 1 + gcWBarrierValidation: 0 + apiCompatibilityLevelPerPlatform: {} + m_RenderingPath: 1 + m_MobileRenderingPath: 1 + metroPackageName: Commercialization.tapadn + metroPackageVersion: + metroCertificatePath: + metroCertificatePassword: + metroCertificateSubject: + metroCertificateIssuer: + metroCertificateNotAfter: 0000000000000000 + metroApplicationDescription: Commercialization.tapadn + wsaImages: {} + metroTileShortName: + metroTileShowName: 0 + metroMediumTileShowName: 0 + metroLargeTileShowName: 0 + metroWideTileShowName: 0 + metroSupportStreamingInstall: 0 + metroLastRequiredScene: 0 + metroDefaultTileSize: 1 + metroTileForegroundText: 2 + metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0} + metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, a: 1} + metroSplashScreenUseBackgroundColor: 0 + syncCapabilities: 0 + platformCapabilities: {} + metroTargetDeviceFamilies: {} + metroFTAName: + metroFTAFileTypes: [] + metroProtocolName: + vcxProjDefaultLanguage: + XboxOneProductId: + XboxOneUpdateKey: + XboxOneSandboxId: + XboxOneContentId: + XboxOneTitleId: + XboxOneSCId: + XboxOneGameOsOverridePath: + XboxOnePackagingOverridePath: + XboxOneAppManifestOverridePath: + XboxOneVersion: 1.0.0.0 + XboxOnePackageEncryption: 0 + XboxOnePackageUpdateGranularity: 2 + XboxOneDescription: + XboxOneLanguage: + - enus + XboxOneCapability: [] + XboxOneGameRating: {} + XboxOneIsContentPackage: 0 + XboxOneEnhancedXboxCompatibilityMode: 0 + XboxOneEnableGPUVariability: 1 + XboxOneSockets: {} + XboxOneSplashScreen: {fileID: 0} + XboxOneAllowedProductIds: [] + XboxOnePersistentLocalStorageSize: 0 + XboxOneXTitleMemory: 8 + XboxOneOverrideIdentityName: + XboxOneOverrideIdentityPublisher: + vrEditorSettings: {} + cloudServicesEnabled: {} + luminIcon: + m_Name: + m_ModelFolderPath: + m_PortalFolderPath: + luminCert: + m_CertPath: + m_SignPackage: 1 + luminIsChannelApp: 0 + luminVersion: + m_VersionCode: 1 + m_VersionName: + hmiPlayerDataPath: + hmiForceSRGBBlit: 1 + embeddedLinuxEnableGamepadInput: 1 + hmiLogStartupTiming: 0 + hmiCpuConfiguration: + apiCompatibilityLevel: 6 + activeInputHandler: 0 + windowsGamepadBackendHint: 0 + cloudProjectId: + framebufferDepthMemorylessMode: 0 + qualitySettingsNames: [] + projectName: + organizationId: + cloudEnabled: 0 + legacyClampBlendShapeWeights: 0 + hmiLoadingImage: {fileID: 0} + platformRequiresReadableAssets: 0 + virtualTexturingSupportEnabled: 0 + insecureHttpOption: 0 diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..11e271c --- /dev/null +++ b/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2022.3.62f3c1 +m_EditorVersionWithRevision: 2022.3.62f3c1 (1623fc0bbb97) diff --git a/ProjectSettings/QualitySettings.asset b/ProjectSettings/QualitySettings.asset new file mode 100644 index 0000000..bcd6706 --- /dev/null +++ b/ProjectSettings/QualitySettings.asset @@ -0,0 +1,239 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!47 &1 +QualitySettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_CurrentQuality: 5 + m_QualitySettings: + - serializedVersion: 2 + name: Very Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 15 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 1 + textureQuality: 1 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.3 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.4 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 16 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Medium + pixelLightCount: 1 + shadows: 1 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 0.7 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 64 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: High + pixelLightCount: 2 + shadows: 2 + shadowResolution: 1 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 40 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 256 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Very High + pixelLightCount: 3 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 70 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 4 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1.5 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 1024 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Ultra + pixelLightCount: 4 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 4 + shadowDistance: 150 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 255 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 2 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4096 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + excludedTargetPlatforms: [] + m_PerPlatformDefaultQuality: + Android: 2 + Lumin: 5 + GameCoreScarlett: 5 + GameCoreXboxOne: 5 + Nintendo Switch: 5 + PS4: 5 + PS5: 5 + Stadia: 5 + Standalone: 5 + WebGL: 3 + Windows Store Apps: 5 + XboxOne: 5 + iPhone: 2 + tvOS: 2 diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset new file mode 100644 index 0000000..1c92a78 --- /dev/null +++ b/ProjectSettings/TagManager.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!78 &1 +TagManager: + serializedVersion: 2 + tags: [] + layers: + - Default + - TransparentFX + - Ignore Raycast + - + - Water + - UI + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + m_SortingLayers: + - name: Default + uniqueID: 0 + locked: 0 diff --git a/ProjectSettings/TimeManager.asset b/ProjectSettings/TimeManager.asset new file mode 100644 index 0000000..558a017 --- /dev/null +++ b/ProjectSettings/TimeManager.asset @@ -0,0 +1,9 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!5 &1 +TimeManager: + m_ObjectHideFlags: 0 + Fixed Timestep: 0.02 + Maximum Allowed Timestep: 0.33333334 + m_TimeScale: 1 + Maximum Particle Timestep: 0.03 diff --git a/ProjectSettings/UnityConnectSettings.asset b/ProjectSettings/UnityConnectSettings.asset new file mode 100644 index 0000000..a27ab5f --- /dev/null +++ b/ProjectSettings/UnityConnectSettings.asset @@ -0,0 +1,38 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!310 &1 +UnityConnectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 1 + m_Enabled: 0 + m_TestMode: 0 + m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events + m_EventUrl: https://cdp.cloud.unity3d.com/v1/events + m_ConfigUrl: https://config.uca.cloud.unity3d.com + m_DashboardUrl: https://dashboard.unity3d.com + m_CNEventUrl: https://cdp.cloud.unity.cn/v1/events + m_CNConfigUrl: https://cdp.cloud.unity.cn/config + m_TestInitMode: 0 + CrashReportingSettings: + m_EventUrl: https://perf-events.cloud.unity.cn + m_Enabled: 0 + m_LogBufferSize: 10 + m_CaptureEditorExceptions: 1 + UnityPurchasingSettings: + m_Enabled: 0 + m_TestMode: 0 + UnityAnalyticsSettings: + m_Enabled: 0 + m_TestMode: 0 + m_InitializeOnStartup: 1 + m_PackageRequiringCoreStatsPresent: 0 + UnityAdsSettings: + m_Enabled: 0 + m_InitializeOnStartup: 1 + m_TestMode: 0 + m_IosGameId: + m_AndroidGameId: + m_GameIds: {} + m_GameId: + PerformanceReportingSettings: + m_Enabled: 0 diff --git a/ProjectSettings/VFXManager.asset b/ProjectSettings/VFXManager.asset new file mode 100644 index 0000000..46f38e1 --- /dev/null +++ b/ProjectSettings/VFXManager.asset @@ -0,0 +1,14 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!937362698 &1 +VFXManager: + m_ObjectHideFlags: 0 + m_IndirectShader: {fileID: 0} + m_CopyBufferShader: {fileID: 0} + m_SortShader: {fileID: 0} + m_StripUpdateShader: {fileID: 0} + m_RenderPipeSettingsPath: + m_FixedTimeStep: 0.016666668 + m_MaxDeltaTime: 0.05 + m_CompiledVersion: 0 + m_RuntimeVersion: 0 diff --git a/ProjectSettings/VersionControlSettings.asset b/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 0000000..dca2881 --- /dev/null +++ b/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!890905787 &1 +VersionControlSettings: + m_ObjectHideFlags: 0 + m_Mode: Visible Meta Files + m_CollabEditorSettings: + inProgressEnabled: 1 diff --git a/ProjectSettings/XRSettings.asset b/ProjectSettings/XRSettings.asset new file mode 100644 index 0000000..482590c --- /dev/null +++ b/ProjectSettings/XRSettings.asset @@ -0,0 +1,10 @@ +{ + "m_SettingKeys": [ + "VR Device Disabled", + "VR Device User Alert" + ], + "m_SettingValues": [ + "False", + "False" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cfaedd --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Commercialization.tapadn + +`Commercialization.tapadn` 是 TapADN / Dirichlet 聚合广告平台在 `CC-Framework.Commercialization` 抽象层上的实现包。 + +## 接入定位 + +业务项目只需要同时引入: + +```json +{ + "com.foldcc.cc-framework.commercialization": "http://private.lightyears.ltd:18650/foldcc/CC-Framework.Commercialization.git#1.0.14", + "com.commercialization.tapadn": "http://private.lightyears.ltd:18650/foldcc/Commercialization.tapadn.git#1.0.0" +} +``` + +项目层继续面向 `ADManager`、`ADConfig`、`AD_Type` 工作,不直接依赖 TapADN SDK API。 + +## 初始化方式 + +推荐让本模块创建 controller 并初始化广告管理器: + +```csharp +var config = TapadnCommercialization.CreateConfig( + mediaId: "100000", + mediaKey: "media-key", + mediaName: "GameName", + rewardSlotId: "200000", + interstitialSlotId: "200001", + splashSlotId: "200002"); + +TapadnCommercialization.InitADManager( + onCallback: () => Debug.Log("AD ready"), + userId: userId, + adConfig: config); +``` + +如项目已有 `ADConfig` 资产,也可以直接调用: + +```csharp +ADManager.Instance.Init(callback, userId, adConfig, new TapadnAdController()); +``` + +`ADConfig` 字段约定: + +* `Id`: TapADN MediaId。 +* `Key`: TapADN MediaKey。 +* `Key2`: MediaName。 +* `BaseAwardAdKeyValue.value`: 激励视频广告位 SpaceId。 +* `BaseInteractionAdKeyValue.value`: 插屏广告位 SpaceId。 +* `BaseSplashAdKeyValue.value`: 开屏广告位 SpaceId。 + +## 配置 key + +可通过 `ADConfig.CommonKeyValues` 或 `InitADManager(..., args)` 中传入 `IDictionary` 覆盖: + +* `tapadn.media_id` +* `tapadn.media_key` +* `tapadn.media_name` +* `tapadn.channel` +* `tapadn.sub_channel` +* `tapadn.debug` +* `tapadn.tap_client_id` +* `tapadn.shake_enabled` +* `tapadn.custom_config_json` +* `tapadn.data_json` +* `tapadn.atags` +* `tapadn.allow_idfa_access` +* `tapadn.request_permission_on_init` +* `tapadn.reward_name` +* `tapadn.reward_amount` +* `tapadn.rewarded_auto_load` +* `tapadn.rewarded_prewarm_on_init` +* `tapadn.rewarded_max_load_attempts` +* `tapadn.rewarded_load_retry_delay_ms` +* `tapadn.rewarded_show_timeout_ms` +* `tapadn.interstitial_auto_load` +* `tapadn.interstitial_prewarm_on_init` +* `tapadn.interstitial_max_load_attempts` +* `tapadn.interstitial_load_retry_delay_ms` +* `tapadn.interstitial_show_timeout_ms` +* `tapadn.splash_auto_load` +* `tapadn.splash_prewarm_on_init` +* `tapadn.splash_max_load_attempts` +* `tapadn.splash_load_retry_delay_ms` +* `tapadn.splash_show_timeout_ms` +* `tapadn.express_width` +* `tapadn.express_height` + +默认激励、插屏、开屏都使用 TapADN Android auto-ad 接口。若遇到渠道缓存策略差异,可将对应 `*_auto_load` 设为 `false`,切换为手动 load/show。 + +## Android 构建 + +包内包含官方 `DirichletMediation` SDK、Android AAR、iOS bridge、EDM4U 依赖声明和构建后处理。 + +构建后处理会自动补齐: + +* TapADN 所需权限。 +* TapADN `TapADFileProvider` 与 `tapad_ad_file_path.xml`。 +* 微信 OpenSDK `WXEntryActivity`、`queries`、本地 `wechat-sdk-android-6.8.34.aar`。 +* `android.useAndroidX=true` 与 `android.enableJetifier=true`。 + +包内不默认暴露可视化编辑面板;调试样例通过 `Samples~` 作为可选导入内容。