Space Ad Mobile 9.5.1
Space Ad Mobile makes ads easy to add into your mobile game: one unified workflow for LevelPlay, AdMob, Yandex.Ads across banners, interstitials and rewarded.
Loading...
Searching...
No Matches
AdControllerBaseEditor.cs
1#if UNITY_EDITOR
2using System;
3using System.Collections.Generic;
4using System.IO;
5using System.Linq;
6using System.Text.RegularExpressions;
7using UnityEditor;
8using UnityEditor.SceneManagement;
9using UnityEngine;
10using UnityEngine.SceneManagement;
11
12namespace SCG.SpaceAdMobile
13{
20 [CustomEditor(typeof(AdControllerBase), true)]
21 [CanEditMultipleObjects]
22 internal sealed class AdControllerBaseEditor : UnityEditor.Editor
23 {
24 #region Constants
25
26 private const string ScriptPropertyName = "m_Script";
27 private const string IdPropertyName = "<" + nameof(AdControllerBase.Id) + ">k__BackingField";
28 private const string GenerateButtonLabel = "Generate";
29 private const float GenerateButtonWidth = 72f;
30 private const float FieldSpacing = 2f;
31
32 #endregion
33
34 #region Unity API
35
37 public override void OnInspectorGUI()
38 {
39 serializedObject.Update();
40
41 DrawScriptProperty();
42
43 var idProperty = serializedObject.FindProperty(IdPropertyName);
44 if (idProperty == null)
45 {
46 EditorGUILayout.HelpBox($"Unable to locate serialized property '{IdPropertyName}'.", MessageType.Error);
47 DrawRemainingProperties();
48 serializedObject.ApplyModifiedProperties();
49 return;
50 }
51
52 DrawIdProperty(idProperty);
53 DrawStateWarnings();
54 DrawRemainingProperties();
55
56 serializedObject.ApplyModifiedProperties();
57 }
58
59 #endregion
60
61 #region Inspector
62
63 private void DrawScriptProperty()
64 {
65 var scriptProperty = serializedObject.FindProperty(ScriptPropertyName);
66 if (scriptProperty == null)
67 return;
68
69 using var disabledScope = new EditorGUI.DisabledScope(true);
70 EditorGUILayout.PropertyField(scriptProperty);
71 }
72
73 private void DrawIdProperty(SerializedProperty idProperty)
74 {
75 var controller = target as AdControllerBase;
76 var isSceneInstance = IsSceneInstance(controller);
77 var isInteractive = isSceneInstance && !serializedObject.isEditingMultipleObjects;
78 var rect = EditorGUILayout.GetControlRect();
79 var contentRect = EditorGUI.PrefixLabel(rect, new GUIContent(nameof(IdentifierUtility.Id)));
80 var dropdownRect = new Rect(
81 contentRect.x,
82 contentRect.y,
83 Mathf.Max(0f, contentRect.width - GenerateButtonWidth - FieldSpacing),
84 contentRect.height);
85 var buttonRect = new Rect(
86 dropdownRect.xMax + FieldSpacing,
87 contentRect.y,
88 GenerateButtonWidth,
89 contentRect.height);
90
91 using (new EditorGUI.DisabledScope(!isInteractive))
92 {
93 if (EditorGUI.DropdownButton(
94 dropdownRect,
95 new GUIContent(GetDisplayedId(idProperty.stringValue)),
96 FocusType.Passive,
97 EditorStyles.popup))
98 {
99 ShowDropdownMenu(controller, idProperty.stringValue, dropdownRect);
100 }
101
102 // ReSharper disable once InvertIf
103 if (GUI.Button(buttonRect, GenerateButtonLabel))
104 {
105 serializedObject.ApplyModifiedProperties();
106 AssignId(controller, Guid.NewGuid().ToString());
107 GUIUtility.ExitGUI();
108 }
109 }
110 }
111
112 private void DrawStateWarnings()
113 {
114 if (serializedObject.isEditingMultipleObjects)
115 {
116 EditorGUILayout.HelpBox("Id selection is disabled for multi-object editing.", MessageType.Info);
117 return;
118 }
119
120 if (target is not AdControllerBase controller)
121 return;
122
123 if (!IsSceneInstance(controller))
124 {
125 EditorGUILayout.HelpBox("Id selection is available only for scene instances.", MessageType.Info);
126 return;
127 }
128
129 if (controller.Ad == null)
130 {
131 EditorGUILayout.HelpBox(
132 "The linked AdUnit is missing. Id changes will be applied only to the controller.",
133 MessageType.Warning);
134 return;
135 }
136
137 if (controller.Ad.TryGetComponent<UndestroyableUtility>(out _))
138 return;
139
140 EditorGUILayout.HelpBox(
141 $"The linked AdUnit is missing {nameof(UndestroyableUtility)}. Id changes will be applied only to the controller, and runtime initialization may still fail.",
142 MessageType.Warning);
143 }
144
145 private void DrawRemainingProperties()
146 {
147 var iterator = serializedObject.GetIterator();
148 var enterChildren = true;
149
150 while (iterator.NextVisible(enterChildren))
151 {
152 enterChildren = false;
153 if (iterator.propertyPath is ScriptPropertyName or IdPropertyName)
154 continue;
155
156 EditorGUILayout.PropertyField(iterator, true);
157 }
158 }
159
160 #endregion
161
162 #region Id Changes
163
164 private static void ShowDropdownMenu(AdControllerBase controller, string currentId, Rect dropdownRect)
165 {
166 if (controller == null)
167 return;
168
169 var menu = new GenericMenu();
170 var candidates = AdControllerIdSceneIndex.GetCandidates(controller);
171
172 if (candidates.Count == 0)
173 {
174 menu.AddDisabledItem(new GUIContent("No matching Ids found in other scenes."));
175 }
176 else
177 {
178 foreach (var candidate in candidates)
179 {
180 var candidateId = candidate.Id;
181 menu.AddItem(
182 new GUIContent(candidate.DisplayLabel),
183 string.Equals(currentId, candidateId, StringComparison.Ordinal),
184 () => AssignId(controller, candidateId));
185 }
186 }
187
188 GenericMenuPopupHost.Show(menu, GUIUtility.GUIToScreenRect(dropdownRect));
189 }
190
191 private static void AssignId(AdControllerBase controller, string id)
192 {
193 if (controller == null || string.IsNullOrWhiteSpace(id))
194 return;
195
196 var objectsToRecord = new List<UnityEngine.Object> { controller };
197 var scenesToMark = new HashSet<Scene>(SceneEqualityComparer.Instance) { controller.gameObject.scene };
198
199 UndestroyableUtility undestroyable = null;
200 if (controller.Ad != null && controller.Ad.TryGetComponent(out undestroyable))
201 {
202 objectsToRecord.Add(undestroyable);
203 scenesToMark.Add(undestroyable.gameObject.scene);
204 }
205
206 Undo.RecordObjects(objectsToRecord.ToArray(), $"Change {nameof(IdentifierUtility.Id)}");
207
208 SetIdValue(controller, id);
209 if (undestroyable != null)
210 SetIdValue(undestroyable, id);
211
212 foreach (var scene in scenesToMark.Where(scene => scene.IsValid()))
213 EditorSceneManager.MarkSceneDirty(scene);
214 }
215
216 private static void SetIdValue(IdentifierUtility targetUtility, string id)
217 {
218 var serializedObject = new SerializedObject(targetUtility);
219 var property = serializedObject.FindProperty(IdPropertyName);
220 if (property == null)
221 return;
222
223 serializedObject.Update();
224 property.stringValue = id;
225 serializedObject.ApplyModifiedPropertiesWithoutUndo();
226 }
227
228 private static string GetDisplayedId(string id) =>
229 string.IsNullOrWhiteSpace(id) ? "<Empty>" : id;
230
231 private static bool IsSceneInstance(AdControllerBase controller) =>
232 controller != null &&
233 controller.gameObject.scene.IsValid() &&
234 !string.IsNullOrWhiteSpace(controller.gameObject.scene.path) &&
235 controller.gameObject.scene.path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase);
236
237 #endregion
238 }
239
244 internal static class AdControllerIdSceneIndex
245 {
246 #region Constants
247
248 private const string IdPropertyName = "<" + nameof(AdControllerBase.Id) + ">k__BackingField";
249 private const string GameObjectDocumentType = "1";
250 private const string TransformDocumentType = "4";
251 private const string MonoBehaviourDocumentType = "114";
252 private const string RectTransformDocumentType = "224";
253 private static readonly Regex documentHeaderRegex = new(@"^--- !u!(\d+) &(-?\d+)", RegexOptions.Compiled);
254 private static readonly Regex fileIdRegex = new(@"\{fileID:\s*(-?\d+)", RegexOptions.Compiled);
255 private static readonly Regex scriptGuidRegex = new(@"\{fileID:\s*11500000,\s*guid:\s*([0-9a-fA-F]+),\s*type:\s*3\s*\}", RegexOptions.Compiled);
256
257 #endregion
258
259 #region Public API
260
266 public static IReadOnlyList<AdControllerIdCandidate> GetCandidates(AdControllerBase controller)
267 {
268 if (controller == null)
269 return Array.Empty<AdControllerIdCandidate>();
270
271 var scriptGuid = GetScriptGuid(controller);
272 if (string.IsNullOrWhiteSpace(scriptGuid))
273 return Array.Empty<AdControllerIdCandidate>();
274
275 var scenePaths = GetSourceScenePaths(controller.gameObject.scene.path);
276 if (scenePaths.Count == 0)
277 return Array.Empty<AdControllerIdCandidate>();
278
279 var seenIds = new HashSet<string>(StringComparer.Ordinal);
280
281 return (from scenePath in scenePaths
282 from candidate in ParseScene(scenePath, scriptGuid)
283 where seenIds.Add(candidate.Id)
284 select candidate).ToList();
285 }
286
287 #endregion
288
289 #region Scene Discovery
290
291 private static List<string> GetSourceScenePaths(string currentScenePath)
292 {
293 var buildScenes = EditorBuildSettings.scenes
294 .Select(scene => scene.path)
295 .Where(IsValidSceneAssetPath)
296 .Distinct(StringComparer.OrdinalIgnoreCase)
297 .Where(path => !string.Equals(path, currentScenePath, StringComparison.OrdinalIgnoreCase))
298 .ToList();
299
300 if (buildScenes.Count > 0)
301 return buildScenes;
302
303 return AssetDatabase.FindAssets("t:Scene", new[] { "Assets" })
304 .Select(AssetDatabase.GUIDToAssetPath)
305 .Where(IsValidSceneAssetPath)
306 .Distinct(StringComparer.OrdinalIgnoreCase)
307 .Where(path => !string.Equals(path, currentScenePath, StringComparison.OrdinalIgnoreCase))
308 .OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
309 .ToList();
310 }
311
312 private static bool IsValidSceneAssetPath(string scenePath) =>
313 !string.IsNullOrWhiteSpace(scenePath) &&
314 scenePath.EndsWith(".unity", StringComparison.OrdinalIgnoreCase) &&
315 File.Exists(Path.GetFullPath(scenePath));
316
317 private static string GetScriptGuid(MonoBehaviour behaviour)
318 {
319 if (behaviour == null)
320 return null;
321
322 var script = MonoScript.FromMonoBehaviour(behaviour);
323 if (script == null)
324 return null;
325
326 var assetPath = AssetDatabase.GetAssetPath(script);
327 return string.IsNullOrWhiteSpace(assetPath) ? null : AssetDatabase.AssetPathToGUID(assetPath);
328 }
329
330 #endregion
331
332 #region YAML Parsing
333
334 private static IEnumerable<AdControllerIdCandidate> ParseScene(string scenePath, string scriptGuid)
335 {
336 var sceneData = ParseSceneData(scenePath, scriptGuid);
337 if (sceneData == null)
338 yield break;
339
340 var sceneName = Path.GetFileNameWithoutExtension(scenePath);
341 foreach (var component in sceneData.Components)
342 {
343 var objectPath = sceneData.GetGameObjectPath(component.GameObjectFileId);
344 if (string.IsNullOrWhiteSpace(objectPath))
345 objectPath = "<Unknown>";
346
347 yield return new AdControllerIdCandidate(
348 component.Id,
349 scenePath,
350 objectPath,
351 $"{sceneName} / {objectPath} [{component.Id}]");
352 }
353 }
354
355 private static ParsedSceneData ParseSceneData(string scenePath, string scriptGuid)
356 {
357 var parsed = new ParsedSceneData();
358 var fullPath = Path.GetFullPath(scenePath);
359 if (!File.Exists(fullPath))
360 return null;
361
362 string documentType = null;
363 string documentFileId = null;
364 string gameObjectFileId = null;
365 string parentTransformFileId = null;
366 string gameObjectName = null;
367 string componentId = null;
368 string componentScriptGuid = null;
369
370 foreach (var rawLine in File.ReadLines(fullPath))
371 {
372 var line = rawLine.TrimStart();
373 var headerMatch = documentHeaderRegex.Match(line);
374 if (headerMatch.Success)
375 {
376 FlushCurrentDocument();
377
378 documentType = headerMatch.Groups[1].Value;
379 documentFileId = headerMatch.Groups[2].Value;
380 gameObjectFileId = null;
381 parentTransformFileId = null;
382 gameObjectName = null;
383 componentId = null;
384 componentScriptGuid = null;
385 continue;
386 }
387
388 switch (documentType)
389 {
390 case GameObjectDocumentType:
391 if (line.StartsWith("m_Name:", StringComparison.Ordinal))
392 gameObjectName = line["m_Name:".Length..].Trim();
393 break;
394
395 case TransformDocumentType:
396 case RectTransformDocumentType:
397 if (line.StartsWith("m_GameObject:", StringComparison.Ordinal))
398 gameObjectFileId = ExtractFileId(line);
399 else if (line.StartsWith("m_Father:", StringComparison.Ordinal))
400 parentTransformFileId = ExtractFileId(line);
401 break;
402
403 case MonoBehaviourDocumentType:
404 if (line.StartsWith("m_GameObject:", StringComparison.Ordinal))
405 gameObjectFileId = ExtractFileId(line);
406 else if (line.StartsWith("m_Script:", StringComparison.Ordinal))
407 componentScriptGuid = ExtractScriptGuid(line);
408 else if (line.StartsWith($"{IdPropertyName}:", StringComparison.Ordinal))
409 componentId = line[$"{IdPropertyName}:".Length..].Trim();
410 break;
411 }
412 }
413
414 FlushCurrentDocument();
415 return parsed;
416
417 void FlushCurrentDocument()
418 {
419 if (string.IsNullOrWhiteSpace(documentType) || string.IsNullOrWhiteSpace(documentFileId))
420 return;
421
422 switch (documentType)
423 {
424 case GameObjectDocumentType:
425 parsed.GameObjectNames[documentFileId] = string.IsNullOrWhiteSpace(gameObjectName)
426 ? "<Unnamed>"
427 : UnquoteYamlValue(gameObjectName);
428 break;
429
430 case TransformDocumentType:
431 case RectTransformDocumentType:
432 if (!string.IsNullOrWhiteSpace(gameObjectFileId))
433 {
434 parsed.Transforms[documentFileId] = new AdControllerSceneTransformInfo(
435 gameObjectFileId,
436 NormalizeFileId(parentTransformFileId));
437 }
438 break;
439
440 case MonoBehaviourDocumentType:
441 if (string.Equals(componentScriptGuid, scriptGuid, StringComparison.OrdinalIgnoreCase) &&
442 !string.IsNullOrWhiteSpace(gameObjectFileId) &&
443 !string.IsNullOrWhiteSpace(componentId))
444 {
445 parsed.Components.Add(new AdControllerSceneComponentInfo(gameObjectFileId, componentId));
446 }
447 break;
448 }
449 }
450 }
451
452 private static string ExtractFileId(string line)
453 {
454 var match = fileIdRegex.Match(line);
455 return match.Success ? match.Groups[1].Value : null;
456 }
457
458 private static string ExtractScriptGuid(string line)
459 {
460 var match = scriptGuidRegex.Match(line);
461 return match.Success ? match.Groups[1].Value : null;
462 }
463
464 private static string NormalizeFileId(string fileId) =>
465 string.IsNullOrWhiteSpace(fileId) || fileId == "0" ? null : fileId;
466
467 private static string UnquoteYamlValue(string value)
468 {
469 if (string.IsNullOrWhiteSpace(value))
470 return value;
471
472 if (value.Length >= 2 && value[0] == '\'' && value[^1] == '\'')
473 return value[1..^1].Replace("''", "'");
474
475 return value;
476 }
477
478 #endregion
479 }
480
485 internal sealed class ParsedSceneData
486 {
487 #region Fields
488
489 private readonly Dictionary<string, string> gameObjectPathCache = new(StringComparer.Ordinal);
490 private Dictionary<string, AdControllerSceneTransformInfo> transformsByGameObject;
491
492 #endregion
493
494 #region Properties
495
497 public Dictionary<string, string> GameObjectNames { get; } = new(StringComparer.Ordinal);
498
500 public Dictionary<string, AdControllerSceneTransformInfo> Transforms { get; } = new(StringComparer.Ordinal);
501
503 public List<AdControllerSceneComponentInfo> Components { get; } = new();
504
505 #endregion
506
507 #region Public API
508
514 public string GetGameObjectPath(string gameObjectFileId) =>
515 string.IsNullOrWhiteSpace(gameObjectFileId)
516 ? null
517 : gameObjectPathCache.TryGetValue(gameObjectFileId, out var cachedPath)
518 ? cachedPath
519 : gameObjectPathCache[gameObjectFileId] = BuildGameObjectPath(gameObjectFileId,
520 new HashSet<string>(StringComparer.Ordinal));
521
522 #endregion
523
524 #region Helpers
525
526 private string BuildGameObjectPath(string gameObjectFileId, HashSet<string> visitedGameObjects)
527 {
528 if (!visitedGameObjects.Add(gameObjectFileId))
529 return TryGetGameObjectName(gameObjectFileId);
530
531 var name = TryGetGameObjectName(gameObjectFileId);
532 var byGameObject = GetTransformsByGameObject();
533
534 if (!byGameObject.TryGetValue(gameObjectFileId, out var transform) ||
535 string.IsNullOrWhiteSpace(transform.ParentTransformFileId) ||
536 !Transforms.TryGetValue(transform.ParentTransformFileId, out var parentTransform))
537 return name;
538
539 var parentPath = BuildGameObjectPath(parentTransform.GameObjectFileId, visitedGameObjects);
540 return string.IsNullOrWhiteSpace(parentPath) ? name : $"{parentPath}/{name}";
541 }
542
543 private Dictionary<string, AdControllerSceneTransformInfo> GetTransformsByGameObject()
544 {
545 if (transformsByGameObject != null)
546 return transformsByGameObject;
547
548 transformsByGameObject = new Dictionary<string, AdControllerSceneTransformInfo>(
549 Transforms.Count, StringComparer.Ordinal);
550
551 foreach (var transform in Transforms.Values)
552 transformsByGameObject.TryAdd(transform.GameObjectFileId, transform);
553
554 return transformsByGameObject;
555 }
556
557 private string TryGetGameObjectName(string gameObjectFileId) =>
558 GameObjectNames.GetValueOrDefault(gameObjectFileId, "<Unknown>");
559
560 #endregion
561 }
562
566 internal sealed class SceneEqualityComparer : IEqualityComparer<Scene>
567 {
568 #region Static
569
571 public static SceneEqualityComparer Instance { get; } = new();
572
573 #endregion
574
575 #region Equality
576
578 public bool Equals(Scene x, Scene y) =>
579 x == y &&
580 string.Equals(x.path, y.path, StringComparison.OrdinalIgnoreCase);
581
583 public int GetHashCode(Scene obj) =>
584 HashCode.Combine(
585 StringComparer.OrdinalIgnoreCase.GetHashCode(obj.path ?? string.Empty),
586 StringComparer.OrdinalIgnoreCase.GetHashCode(obj.name ?? string.Empty),
587 obj.buildIndex);
588
589 #endregion
590 }
591
595 internal sealed class AdControllerSceneTransformInfo
596 {
597 #region Construction
598
604 public AdControllerSceneTransformInfo(string gameObjectFileId, string parentTransformFileId)
605 {
606 GameObjectFileId = gameObjectFileId;
607 ParentTransformFileId = parentTransformFileId;
608 }
609
610 #endregion
611
612 #region Properties
613
615 public string GameObjectFileId { get; }
616
618 public string ParentTransformFileId { get; }
619
620 #endregion
621 }
622
626 internal sealed class AdControllerSceneComponentInfo
627 {
628 #region Construction
629
635 public AdControllerSceneComponentInfo(string gameObjectFileId, string id)
636 {
637 GameObjectFileId = gameObjectFileId;
638 Id = id;
639 }
640
641 #endregion
642
643 #region Properties
644
646 public string GameObjectFileId { get; }
647
649 public string Id { get; }
650
651 #endregion
652 }
653
657 internal sealed class AdControllerIdCandidate
658 {
659 #region Construction
660
668 public AdControllerIdCandidate(string id, string scenePath, string objectPath, string displayLabel)
669 {
670 Id = id;
671 ScenePath = scenePath;
672 ObjectPath = objectPath;
673 DisplayLabel = displayLabel;
674 }
675
676 #endregion
677
678 #region Properties
679
681 public string Id { get; }
682
684 public string ScenePath { get; }
685
687 public string ObjectPath { get; }
688
690 public string DisplayLabel { get; }
691
692 #endregion
693 }
694}
695#endif
Base class for controlling ad units within Unity, providing the foundation for specific ad functional...