3using System.Collections.Generic;
6using System.Text.RegularExpressions;
8using UnityEditor.SceneManagement;
10using UnityEngine.SceneManagement;
21 [CanEditMultipleObjects]
22 internal sealed class AdControllerBaseEditor : UnityEditor.Editor
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;
37 public override void OnInspectorGUI()
39 serializedObject.Update();
43 var idProperty = serializedObject.FindProperty(IdPropertyName);
44 if (idProperty ==
null)
46 EditorGUILayout.HelpBox($
"Unable to locate serialized property '{IdPropertyName}'.", MessageType.Error);
47 DrawRemainingProperties();
48 serializedObject.ApplyModifiedProperties();
52 DrawIdProperty(idProperty);
54 DrawRemainingProperties();
56 serializedObject.ApplyModifiedProperties();
63 private void DrawScriptProperty()
65 var scriptProperty = serializedObject.FindProperty(ScriptPropertyName);
66 if (scriptProperty ==
null)
69 using var disabledScope =
new EditorGUI.DisabledScope(
true);
70 EditorGUILayout.PropertyField(scriptProperty);
73 private void DrawIdProperty(SerializedProperty idProperty)
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(
83 Mathf.Max(0f, contentRect.width - GenerateButtonWidth - FieldSpacing),
85 var buttonRect =
new Rect(
86 dropdownRect.xMax + FieldSpacing,
91 using (
new EditorGUI.DisabledScope(!isInteractive))
93 if (EditorGUI.DropdownButton(
95 new GUIContent(GetDisplayedId(idProperty.stringValue)),
99 ShowDropdownMenu(controller, idProperty.stringValue, dropdownRect);
103 if (GUI.Button(buttonRect, GenerateButtonLabel))
105 serializedObject.ApplyModifiedProperties();
106 AssignId(controller, Guid.NewGuid().ToString());
107 GUIUtility.ExitGUI();
112 private void DrawStateWarnings()
114 if (serializedObject.isEditingMultipleObjects)
116 EditorGUILayout.HelpBox(
"Id selection is disabled for multi-object editing.", MessageType.Info);
120 if (target is not AdControllerBase controller)
123 if (!IsSceneInstance(controller))
125 EditorGUILayout.HelpBox(
"Id selection is available only for scene instances.", MessageType.Info);
129 if (controller.Ad ==
null)
131 EditorGUILayout.HelpBox(
132 "The linked AdUnit is missing. Id changes will be applied only to the controller.",
133 MessageType.Warning);
137 if (controller.Ad.TryGetComponent<UndestroyableUtility>(out _))
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);
145 private void DrawRemainingProperties()
147 var iterator = serializedObject.GetIterator();
148 var enterChildren =
true;
150 while (iterator.NextVisible(enterChildren))
152 enterChildren =
false;
153 if (iterator.propertyPath is ScriptPropertyName or IdPropertyName)
156 EditorGUILayout.PropertyField(iterator,
true);
164 private static void ShowDropdownMenu(AdControllerBase controller,
string currentId, Rect dropdownRect)
166 if (controller ==
null)
169 var menu =
new GenericMenu();
170 var candidates = AdControllerIdSceneIndex.GetCandidates(controller);
172 if (candidates.Count == 0)
174 menu.AddDisabledItem(
new GUIContent(
"No matching Ids found in other scenes."));
178 foreach (var candidate
in candidates)
180 var candidateId = candidate.Id;
182 new GUIContent(candidate.DisplayLabel),
183 string.Equals(currentId, candidateId, StringComparison.Ordinal),
184 () => AssignId(controller, candidateId));
188 GenericMenuPopupHost.Show(menu, GUIUtility.GUIToScreenRect(dropdownRect));
191 private static void AssignId(AdControllerBase controller,
string id)
193 if (controller ==
null ||
string.IsNullOrWhiteSpace(
id))
196 var objectsToRecord =
new List<UnityEngine.Object> { controller };
197 var scenesToMark =
new HashSet<Scene>(SceneEqualityComparer.Instance) { controller.gameObject.scene };
199 UndestroyableUtility undestroyable =
null;
200 if (controller.Ad !=
null && controller.Ad.TryGetComponent(out undestroyable))
202 objectsToRecord.Add(undestroyable);
203 scenesToMark.Add(undestroyable.gameObject.scene);
206 Undo.RecordObjects(objectsToRecord.ToArray(), $
"Change {nameof(IdentifierUtility.Id)}");
208 SetIdValue(controller,
id);
209 if (undestroyable !=
null)
210 SetIdValue(undestroyable,
id);
212 foreach (var scene
in scenesToMark.Where(scene => scene.IsValid()))
213 EditorSceneManager.MarkSceneDirty(scene);
216 private static void SetIdValue(IdentifierUtility targetUtility,
string id)
218 var serializedObject =
new SerializedObject(targetUtility);
219 var
property = serializedObject.FindProperty(IdPropertyName);
220 if (property ==
null)
223 serializedObject.Update();
224 property.stringValue = id;
225 serializedObject.ApplyModifiedPropertiesWithoutUndo();
228 private static string GetDisplayedId(
string id) =>
229 string.IsNullOrWhiteSpace(
id) ?
"<Empty>" : id;
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);
244 internal static class AdControllerIdSceneIndex
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);
266 public static IReadOnlyList<AdControllerIdCandidate> GetCandidates(AdControllerBase controller)
268 if (controller ==
null)
269 return Array.Empty<AdControllerIdCandidate>();
271 var scriptGuid = GetScriptGuid(controller);
272 if (
string.IsNullOrWhiteSpace(scriptGuid))
273 return Array.Empty<AdControllerIdCandidate>();
275 var scenePaths = GetSourceScenePaths(controller.gameObject.scene.path);
276 if (scenePaths.Count == 0)
277 return Array.Empty<AdControllerIdCandidate>();
279 var seenIds =
new HashSet<string>(StringComparer.Ordinal);
281 return (from scenePath in scenePaths
282 from candidate in ParseScene(scenePath, scriptGuid)
283 where seenIds.Add(candidate.Id)
284 select candidate).ToList();
289 #region Scene Discovery
291 private static List<string> GetSourceScenePaths(
string currentScenePath)
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))
300 if (buildScenes.Count > 0)
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)
312 private static bool IsValidSceneAssetPath(
string scenePath) =>
313 !
string.IsNullOrWhiteSpace(scenePath) &&
314 scenePath.EndsWith(
".unity", StringComparison.OrdinalIgnoreCase) &&
315 File.Exists(Path.GetFullPath(scenePath));
317 private static string GetScriptGuid(MonoBehaviour behaviour)
319 if (behaviour ==
null)
322 var script = MonoScript.FromMonoBehaviour(behaviour);
326 var assetPath = AssetDatabase.GetAssetPath(script);
327 return string.IsNullOrWhiteSpace(assetPath) ? null : AssetDatabase.AssetPathToGUID(assetPath);
334 private static IEnumerable<AdControllerIdCandidate> ParseScene(
string scenePath,
string scriptGuid)
336 var sceneData = ParseSceneData(scenePath, scriptGuid);
337 if (sceneData ==
null)
340 var sceneName = Path.GetFileNameWithoutExtension(scenePath);
341 foreach (var component
in sceneData.Components)
343 var objectPath = sceneData.GetGameObjectPath(component.GameObjectFileId);
344 if (
string.IsNullOrWhiteSpace(objectPath))
345 objectPath =
"<Unknown>";
347 yield
return new AdControllerIdCandidate(
351 $
"{sceneName} / {objectPath} [{component.Id}]");
355 private static ParsedSceneData ParseSceneData(
string scenePath,
string scriptGuid)
357 var parsed =
new ParsedSceneData();
358 var fullPath = Path.GetFullPath(scenePath);
359 if (!File.Exists(fullPath))
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;
370 foreach (var rawLine
in File.ReadLines(fullPath))
372 var line = rawLine.TrimStart();
373 var headerMatch = documentHeaderRegex.Match(line);
374 if (headerMatch.Success)
376 FlushCurrentDocument();
378 documentType = headerMatch.Groups[1].Value;
379 documentFileId = headerMatch.Groups[2].Value;
380 gameObjectFileId =
null;
381 parentTransformFileId =
null;
382 gameObjectName =
null;
384 componentScriptGuid =
null;
388 switch (documentType)
390 case GameObjectDocumentType:
391 if (line.StartsWith(
"m_Name:", StringComparison.Ordinal))
392 gameObjectName = line[
"m_Name:".Length..].Trim();
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);
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();
414 FlushCurrentDocument();
417 void FlushCurrentDocument()
419 if (
string.IsNullOrWhiteSpace(documentType) ||
string.IsNullOrWhiteSpace(documentFileId))
422 switch (documentType)
424 case GameObjectDocumentType:
425 parsed.GameObjectNames[documentFileId] =
string.IsNullOrWhiteSpace(gameObjectName)
427 : UnquoteYamlValue(gameObjectName);
430 case TransformDocumentType:
431 case RectTransformDocumentType:
432 if (!
string.IsNullOrWhiteSpace(gameObjectFileId))
434 parsed.Transforms[documentFileId] =
new AdControllerSceneTransformInfo(
436 NormalizeFileId(parentTransformFileId));
440 case MonoBehaviourDocumentType:
441 if (
string.Equals(componentScriptGuid, scriptGuid, StringComparison.OrdinalIgnoreCase) &&
442 !
string.IsNullOrWhiteSpace(gameObjectFileId) &&
443 !
string.IsNullOrWhiteSpace(componentId))
445 parsed.Components.Add(
new AdControllerSceneComponentInfo(gameObjectFileId, componentId));
452 private static string ExtractFileId(
string line)
454 var match = fileIdRegex.Match(line);
455 return match.Success ? match.Groups[1].Value :
null;
458 private static string ExtractScriptGuid(
string line)
460 var match = scriptGuidRegex.Match(line);
461 return match.Success ? match.Groups[1].Value :
null;
464 private static string NormalizeFileId(
string fileId) =>
465 string.IsNullOrWhiteSpace(fileId) || fileId ==
"0" ? null : fileId;
467 private static string UnquoteYamlValue(
string value)
469 if (
string.IsNullOrWhiteSpace(value))
472 if (value.Length >= 2 && value[0] ==
'\'' && value[^1] ==
'\'')
473 return value[1..^1].Replace(
"''",
"'");
485 internal sealed class ParsedSceneData
489 private readonly Dictionary<string, string> gameObjectPathCache =
new(StringComparer.Ordinal);
490 private Dictionary<string, AdControllerSceneTransformInfo> transformsByGameObject;
497 public Dictionary<string, string> GameObjectNames {
get; } =
new(StringComparer.Ordinal);
500 public Dictionary<string, AdControllerSceneTransformInfo> Transforms {
get; } =
new(StringComparer.Ordinal);
503 public List<AdControllerSceneComponentInfo> Components {
get; } =
new();
514 public string GetGameObjectPath(
string gameObjectFileId) =>
515 string.IsNullOrWhiteSpace(gameObjectFileId)
517 : gameObjectPathCache.TryGetValue(gameObjectFileId, out var cachedPath)
519 : gameObjectPathCache[gameObjectFileId] = BuildGameObjectPath(gameObjectFileId,
520 new HashSet<string>(StringComparer.Ordinal));
526 private string BuildGameObjectPath(
string gameObjectFileId, HashSet<string> visitedGameObjects)
528 if (!visitedGameObjects.Add(gameObjectFileId))
529 return TryGetGameObjectName(gameObjectFileId);
531 var name = TryGetGameObjectName(gameObjectFileId);
532 var byGameObject = GetTransformsByGameObject();
534 if (!byGameObject.TryGetValue(gameObjectFileId, out var transform) ||
535 string.IsNullOrWhiteSpace(transform.ParentTransformFileId) ||
536 !Transforms.TryGetValue(transform.ParentTransformFileId, out var parentTransform))
539 var parentPath = BuildGameObjectPath(parentTransform.GameObjectFileId, visitedGameObjects);
540 return string.IsNullOrWhiteSpace(parentPath) ? name : $
"{parentPath}/{name}";
543 private Dictionary<string, AdControllerSceneTransformInfo> GetTransformsByGameObject()
545 if (transformsByGameObject !=
null)
546 return transformsByGameObject;
548 transformsByGameObject =
new Dictionary<string, AdControllerSceneTransformInfo>(
549 Transforms.Count, StringComparer.Ordinal);
551 foreach (var transform
in Transforms.Values)
552 transformsByGameObject.TryAdd(transform.GameObjectFileId, transform);
554 return transformsByGameObject;
557 private string TryGetGameObjectName(
string gameObjectFileId) =>
558 GameObjectNames.GetValueOrDefault(gameObjectFileId,
"<Unknown>");
566 internal sealed class SceneEqualityComparer : IEqualityComparer<Scene>
571 public static SceneEqualityComparer Instance {
get; } =
new();
578 public bool Equals(Scene x, Scene y) =>
580 string.Equals(x.path, y.path, StringComparison.OrdinalIgnoreCase);
583 public int GetHashCode(Scene obj) =>
585 StringComparer.OrdinalIgnoreCase.GetHashCode(obj.path ??
string.Empty),
586 StringComparer.OrdinalIgnoreCase.GetHashCode(obj.name ??
string.Empty),
595 internal sealed class AdControllerSceneTransformInfo
604 public AdControllerSceneTransformInfo(
string gameObjectFileId,
string parentTransformFileId)
606 GameObjectFileId = gameObjectFileId;
607 ParentTransformFileId = parentTransformFileId;
615 public string GameObjectFileId {
get; }
618 public string ParentTransformFileId {
get; }
626 internal sealed class AdControllerSceneComponentInfo
635 public AdControllerSceneComponentInfo(
string gameObjectFileId,
string id)
637 GameObjectFileId = gameObjectFileId;
646 public string GameObjectFileId {
get; }
649 public string Id {
get; }
657 internal sealed class AdControllerIdCandidate
668 public AdControllerIdCandidate(
string id,
string scenePath,
string objectPath,
string displayLabel)
671 ScenePath = scenePath;
672 ObjectPath = objectPath;
673 DisplayLabel = displayLabel;
681 public string Id {
get; }
684 public string ScenePath {
get; }
687 public string ObjectPath {
get; }
690 public string DisplayLabel {
get; }
Base class for controlling ad units within Unity, providing the foundation for specific ad functional...