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
YandexAssemblyUpdater.cs
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Text;
6using System.Text.RegularExpressions;
7using UnityEditor;
8using UnityEngine;
9using PackageInfo = UnityEditor.PackageManager.PackageInfo;
10
11namespace SCG.SpaceAdMobile
12{
13 [InitializeOnLoad]
14 internal class YandexAssemblyUpdater
15 {
16 #region Common paths
17
18 private const string AssetsFolder = "Assets";
19 private const string PackagesFolder = "Packages";
20
21 #endregion
22
23 #region Yandex updater asset constants
24
25 private const string YandexUpdaterScriptFileName = "update_yandex_sdk.py";
26 private const string PackageImportBridgeTemplateFileName = "PackageImportBridgeTemplate.cs.txt";
27 private const string PackageImportBridgeFileName = "PackageImportBridge.cs";
28
29 #endregion
30
31 #region CS0414 suppression constants
32
33 private const string SuppressYandex0414WarningsPreferenceKey = nameof(SpaceAdMobile) + ".SuppressYandex0414Warnings";
34 private const string WarningCode0067 = "67";
35 private const string WarningCode0414 = "414";
36 private const string Warning414SuppressionMarker = "// UNITY_SUPPRESS_CS0414";
37
38 private static readonly string[] s_cs0414WarningFileNames =
39 {
40 "DummyRewardedAdLoaderClient.cs",
41 "DummyInterstitialAdLoaderClient.cs",
42 "DummyAppOpenAdLoaderClient.cs"
43 };
44
45 private static readonly Regex s_warningDisableRegex = new(
46 @"^(?<prefix>\s*#pragma\s+warning\s+disable\s+)(?<codes>[^/\r\n]+?)(?<comment>\s*//.*)?$",
47 RegexOptions.CultureInvariant);
48
49 #endregion
50
51 #region Reload Fix
52 private const string AdRevenueAutoCollectionResolverFileName = "AdRevenueAutoCollectionResolver.cs";
53 private const string ImportWorkerGuardComment = "SCG_IMPORT_WORKER_GUARD";
54 private const string ImportWorkerGuardCondition = "AssetDatabase.IsAssetImportWorkerProcess()";
55
56 private static readonly string[] adRevenueAutoCollectionResolverGuardMethods =
57 {
58 "internal static void Init()",
59 "private static void ApplyDefines()"
60 };
61 #endregion
62
63 #region iOS Fix: Readonly & Constants
64 private static readonly string[] iosMethodSignatures =
65 {
66 "private static void ModifyPlist(string pathToBuildProject)",
67 "public static void OnPostProcessBuild(BuildTarget buildTarget, string pathToBuiltProject)"
68 };
69
70 private const string IosFixIfDefine = "#if UNITY_IOS || UNITY_TVOS // " + IosWrappedComment;
71 private const string IosFixEndIfDefine = "#endif // " + IosWrappedComment;
72 private const string IosUsingStarts = "using UnityEditor.iOS.Xcode";
73 private const string IosIfCondition = "BuildTarget.iOS"; // if (buildTarget == BuildTarget.iOS)
74 private const string IosWrappedComment = "UNITY_IOS_WRAPPED";
75 #endregion
76
77 #region State
78
83 private static YandexData yandexData;
84
86 private static bool is0414WarningSuppressionUpdateScheduled;
87
88 #endregion
89
90 #region Bootstrap
91
92 private static bool isUpdateScheduled;
93 private const string ActiveSdkSessionPath = "Library/YandexSdkUpdater/active-session.json";
94 private const double SdkSessionCheckIntervalSeconds = 1;
95 private static double nextSdkSessionCheckTime;
96
100 private static bool IsSdkUpdateRunning()
101 {
102 if (EditorApplication.timeSinceStartup < nextSdkSessionCheckTime)
103 return true;
104
105 var running = CheckSdkUpdateSession();
106 if (running)
107 nextSdkSessionCheckTime = EditorApplication.timeSinceStartup + SdkSessionCheckIntervalSeconds;
108
109 return running;
110 }
111
115 private static bool CheckSdkUpdateSession()
116 {
117 if (!File.Exists(ActiveSdkSessionPath))
118 return false;
119
120 try
121 {
122 var marker = File.ReadAllText(ActiveSdkSessionPath);
123 var match = Regex.Match(marker, @"""ProcessId""\s*:\s*(\d+)");
124 if (!match.Success || !int.TryParse(match.Groups[1].Value, out var processId))
125 return true;
126
127 using var process = System.Diagnostics.Process.GetProcessById(processId);
128 return !process.HasExited;
129 }
130 catch (ArgumentException)
131 {
132 return false;
133 }
134 catch (IOException)
135 {
136 return true;
137 }
138 catch (System.ComponentModel.Win32Exception)
139 {
140 return true;
141 }
142 }
143
144 static YandexAssemblyUpdater()
145 {
146 if (AssetDatabase.IsAssetImportWorkerProcess())
147 return;
148
149 if (!DefineSymbolsManager.HasDefineSymbol(DefineSymbolsManager.YandexMobileAds))
150 return;
151
152 if (IsSdkUpdateRunning())
153 {
154 ScheduleUpdate();
155 return;
156 }
157
158 var resource = AdAggregatorResource.Instance;
159 var data = GetConfiguredYandexData(resource);
160 if (data != null &&
161 IsRuntimeDefineConstraintConfigured(data) &&
162 IsYandexUpdaterAssetsSynchronized(data) &&
163 Is0414WarningSuppressionApplied(data, SuppressYandex0414Warnings) &&
164 !FixIosReferences(false))
165 return;
166
167 if (data != null)
168 ScheduleUpdate();
169 }
170
171 private static void ScheduleUpdate()
172 {
173 if (isUpdateScheduled)
174 return;
175
176 isUpdateScheduled = true;
177 EditorApplication.delayCall += UpdateOnDelayCall;
178 }
179
180 private static void UpdateOnDelayCall()
181 {
182 EditorApplication.delayCall -= UpdateOnDelayCall;
183
184 if (AssetDatabase.IsAssetImportWorkerProcess())
185 {
186 isUpdateScheduled = false;
187 return;
188 }
189
190 if (EditorApplication.isUpdating || EditorApplication.isCompiling || IsSdkUpdateRunning())
191 {
192 isUpdateScheduled = false;
193 ScheduleUpdate();
194 return;
195 }
196
197 isUpdateScheduled = false;
198 Update();
199 }
200
201 #endregion
202
211 internal static void Update()
212 {
213 if (AssetDatabase.IsAssetImportWorkerProcess())
214 return;
215
216 if (IsSdkUpdateRunning())
217 {
218 ScheduleUpdate();
219 return;
220 }
221
222 var resource = AdAggregatorResource.Instance;
223 if (resource == null)
224 {
225 throw new UnityException($"The resource file '{nameof(AdAggregatorResource)}' is missing. Please reinstall the package.");
226 }
227
228 resource.UseEnhancedYandexEditorDummies =
229 EditorPrefs.GetBool(AdAggregatorResource.KeyUseEnhancedYandexEditorDummies, false);
230 yandexData = GetConfiguredYandexData(resource) ??
231 throw new UnityException("Yandex configuration is missing in AdAggregatorResource.");
232 var runtimeDefineConfigured = IsRuntimeDefineConstraintConfigured(yandexData);
233 var changed =
234 CreateYandexAssemblyDefinition() |
235 SynchronizeYandexUpdaterAssets() |
236 Synchronize0414WarningSuppressions(yandexData) |
237 ApplyOrRestoreEnhancedEditorDummies(resource.UseEnhancedYandexEditorDummies) |
238 UpdateAssemblyReferences() |
239 PatchAdRevenueAutoCollectionResolverGuard() |
240 FixIosReferences();
241
242 if (!runtimeDefineConfigured)
243 ScheduleRuntimeDefineConstraintUpdate();
244
245 if (changed && !EditorApplication.isUpdating)
246 EditorApplication.delayCall += AssetDatabase.Refresh;
247 }
248
249 #region Yandex configuration
250
256 private static YandexData GetConfiguredYandexData(AdAggregatorResource resource)
257 {
258 if (resource != null)
259 yandexData = resource.Yandex;
260
261 return yandexData;
262 }
263
264 #endregion
265
266 #region Yandex asmdef creation
267
272 private static bool CreateYandexAssemblyDefinition()
273 {
274 var changed = false;
275
276 // Runtime asmdef
277 var assetsRuntimeDir = ToAssetsPath(yandexData.AssetPath);
278 changed |= EnsureFolderExists(assetsRuntimeDir);
279
280 var runtimeAsmdefRel = $"{assetsRuntimeDir}/{yandexData.MainAsmdefName}.asmdef";
281 changed |= EnsureAsmdefExists(runtimeAsmdefRel, yandexData.AsmdefTemplate,
282 $"[{nameof(YandexAssemblyUpdater)}]: {Path.GetFileName(runtimeAsmdefRel)} has been successfully created!");
283
284 // Editor asmdef (only if the folder exists or we can create it)
285 var assetsEditorDir = ToAssetsPath(yandexData.EditorPath);
286
287 changed |= EnsureFolderExists(assetsEditorDir);
288 var editorAsmdefRel = $"{assetsEditorDir}/{yandexData.EditorAsmdefName}.asmdef";
289 changed |= EnsureAsmdefExists(editorAsmdefRel, yandexData.AsmdefEditorTemplate,
290 $"[{nameof(YandexAssemblyUpdater)}]: {Path.GetFileName(editorAsmdefRel)} has been successfully created!");
291
292 return changed;
293 }
294
295 private static void ScheduleRuntimeDefineConstraintUpdate()
296 {
297 EditorApplication.delayCall -= AddRuntimeDefineConstraintOnDelayCall;
298 EditorApplication.delayCall += AddRuntimeDefineConstraintOnDelayCall;
299 }
300
301 private static void AddRuntimeDefineConstraintOnDelayCall()
302 {
303 EditorApplication.delayCall -= AddRuntimeDefineConstraintOnDelayCall;
304
305 if (AssetDatabase.IsAssetImportWorkerProcess() || EditorApplication.isUpdating || EditorApplication.isCompiling || IsSdkUpdateRunning())
306 {
307 ScheduleRuntimeDefineConstraintUpdate();
308 return;
309 }
310
311 if (yandexData == null)
312 return;
313
314 if (IsRuntimeDefineConstraintConfigured(yandexData))
315 return;
316
317 var assetsRuntimeDir = ToAssetsPath(yandexData.AssetPath);
318 var runtimeAsmdefRel = $"{assetsRuntimeDir}/{yandexData.MainAsmdefName}.asmdef";
319 var runtimeAsmdefAbs = ToAbsolutePath(runtimeAsmdefRel);
320 if (!File.Exists(runtimeAsmdefAbs))
321 return;
322
323 var original = File.ReadAllText(runtimeAsmdefAbs, Encoding.UTF8);
324 var updated = AddDefineConstraint(original, DefineSymbolsManager.YandexMobileAds);
325 if (string.Equals(original, updated, StringComparison.Ordinal))
326 return;
327
328 File.WriteAllText(runtimeAsmdefAbs, updated, Encoding.UTF8);
329 Debug.Log($"[{nameof(YandexAssemblyUpdater)}]: Added {DefineSymbolsManager.YandexMobileAds} define constraint to {Path.GetFileName(runtimeAsmdefRel)}.");
330 AssetDatabase.ImportAsset(runtimeAsmdefRel);
331 }
332
333 private static bool IsRuntimeDefineConstraintConfigured(YandexData data)
334 {
335 if (data == null)
336 return false;
337
338 var assetsRuntimeDir = ToAssetsPath(data.AssetPath);
339 var runtimeAsmdefRel = $"{assetsRuntimeDir}/{data.MainAsmdefName}.asmdef";
340 var runtimeAsmdefAbs = ToAbsolutePath(runtimeAsmdefRel);
341 return File.Exists(runtimeAsmdefAbs) &&
342 File.ReadAllText(runtimeAsmdefAbs, Encoding.UTF8)
343 .Contains($"\"{DefineSymbolsManager.YandexMobileAds}\"", StringComparison.Ordinal);
344 }
345
346 #endregion
347
348 #region Yandex SDK updater assets
349
354 private static bool SynchronizeYandexUpdaterAssets()
355 {
356 var runtimeDirectory = ToAssetsPath(yandexData.AssetPath);
357 var editorDirectory = ToAssetsPath(yandexData.EditorPath);
358 var templateDirectory = GetYandexSupportTemplateDirectory();
359
360 var changed = SynchronizeAssetFile(
361 $"{templateDirectory}/{YandexUpdaterScriptFileName}",
362 $"{runtimeDirectory}/{YandexUpdaterScriptFileName}");
363 changed |= SynchronizeAssetFile(
364 $"{templateDirectory}/{PackageImportBridgeTemplateFileName}",
365 $"{editorDirectory}/{PackageImportBridgeFileName}");
366
367 return changed;
368 }
369
375 private static bool IsYandexUpdaterAssetsSynchronized(YandexData data)
376 {
377 if (data == null)
378 return false;
379
380 var runtimeDirectory = ToAssetsPath(data.AssetPath);
381 var editorDirectory = ToAssetsPath(data.EditorPath);
382 var templateDirectory = GetYandexSupportTemplateDirectory();
383
384 return AssetFilesMatch(
385 $"{templateDirectory}/{YandexUpdaterScriptFileName}",
386 $"{runtimeDirectory}/{YandexUpdaterScriptFileName}") &&
387 AssetFilesMatch(
388 $"{templateDirectory}/{PackageImportBridgeTemplateFileName}",
389 $"{editorDirectory}/{PackageImportBridgeFileName}");
390 }
391
392 private static string GetYandexSupportTemplateDirectory()
393 {
394 var templateScriptPath = AssetDatabase.FindAssets($"{nameof(YandexAssemblyUpdater)} t:MonoScript")
395 .Select(AssetDatabase.GUIDToAssetPath)
396 .FirstOrDefault(path =>
397 {
398 var script = AssetDatabase.LoadAssetAtPath<MonoScript>(path);
399 return script != null && script.GetClass() == typeof(YandexAssemblyUpdater);
400 });
401 return string.IsNullOrEmpty(templateScriptPath)
402 ? throw new UnityException($"The source file for {nameof(YandexAssemblyUpdater)} could not be found.")
403 : Path.GetDirectoryName(templateScriptPath)?.Replace('\\', '/') ??
404 throw new UnityException(
405 $"The source directory for {nameof(YandexAssemblyUpdater)} could not be found.");
406 }
407
414 private static bool SynchronizeAssetFile(string templateAssetPath, string targetAssetPath)
415 {
416 var templatePath = ToAbsolutePath(templateAssetPath);
417 if (!File.Exists(templatePath))
418 {
419 Debug.LogWarning($"[{nameof(YandexAssemblyUpdater)}]: Yandex updater template was not found: {templateAssetPath}");
420 return false;
421 }
422
423 var targetPath = ToAbsolutePath(targetAssetPath);
424 var templateContent = File.ReadAllText(templatePath, Encoding.UTF8);
425 if (File.Exists(targetPath) &&
426 string.Equals(File.ReadAllText(targetPath, Encoding.UTF8), templateContent, StringComparison.Ordinal))
427 {
428 return false;
429 }
430
431 var targetDirectory = Path.GetDirectoryName(targetPath);
432 if (!string.IsNullOrEmpty(targetDirectory))
433 Directory.CreateDirectory(targetDirectory);
434
435 File.WriteAllText(targetPath, templateContent, new UTF8Encoding(false));
436 return true;
437 }
438
445 private static bool AssetFilesMatch(string templateAssetPath, string targetAssetPath)
446 {
447 var templatePath = ToAbsolutePath(templateAssetPath);
448 var targetPath = ToAbsolutePath(targetAssetPath);
449 return File.Exists(templatePath) && File.Exists(targetPath) &&
450 string.Equals(
451 File.ReadAllText(templatePath, Encoding.UTF8),
452 File.ReadAllText(targetPath, Encoding.UTF8),
453 StringComparison.Ordinal);
454 }
455
456 #endregion
457
458 #region CS0414 warning suppression
459
463 internal static bool SuppressYandex0414Warnings
464 {
465 get => EditorPrefs.GetBool(SuppressYandex0414WarningsPreferenceKey, false);
466 set
467 {
468 if (EditorPrefs.GetBool(SuppressYandex0414WarningsPreferenceKey, false) == value)
469 return;
470
471 EditorPrefs.SetBool(SuppressYandex0414WarningsPreferenceKey, value);
472 Schedule0414WarningSuppressionUpdate();
473 }
474 }
475
479 private static void Schedule0414WarningSuppressionUpdate()
480 {
481 if (is0414WarningSuppressionUpdateScheduled)
482 return;
483
484 is0414WarningSuppressionUpdateScheduled = true;
485 EditorApplication.delayCall += Update0414WarningSuppressionsOnDelayCall;
486 }
487
491 private static void Update0414WarningSuppressionsOnDelayCall()
492 {
493 EditorApplication.delayCall -= Update0414WarningSuppressionsOnDelayCall;
494
495 if (AssetDatabase.IsAssetImportWorkerProcess())
496 {
497 is0414WarningSuppressionUpdateScheduled = false;
498 return;
499 }
500
501 if (EditorApplication.isUpdating || EditorApplication.isCompiling || IsSdkUpdateRunning())
502 {
503 EditorApplication.delayCall += Update0414WarningSuppressionsOnDelayCall;
504 return;
505 }
506
507 is0414WarningSuppressionUpdateScheduled = false;
508 var data = GetConfiguredYandexData(AdAggregatorResource.Instance);
509 if (data == null || !Synchronize0414WarningSuppressions(data))
510 return;
511
512 EditorApplication.delayCall += AssetDatabase.Refresh;
513 }
514
521 private static bool Synchronize0414WarningSuppressions(YandexData data)
522 {
523 if (data == null)
524 return false;
525
526 var shouldSuppress = SuppressYandex0414Warnings;
527 var changedFileNames = new List<string>();
528 foreach (var assetPath in Get0414WarningAssetPaths(data))
529 {
530 var absolutePath = ToAbsolutePath(assetPath);
531 if (!File.Exists(absolutePath))
532 continue;
533
534 var original = File.ReadAllText(absolutePath, Encoding.UTF8);
535 var updated = shouldSuppress
536 ? Add0414WarningSuppression(original)
537 : Remove0414WarningSuppression(original);
538 if (string.Equals(original, updated, StringComparison.Ordinal))
539 continue;
540
541 File.WriteAllText(absolutePath, updated, new UTF8Encoding(false));
542 changedFileNames.Add(Path.GetFileName(assetPath));
543 }
544
545 if (changedFileNames.Count == 0)
546 return false;
547
548 var action = shouldSuppress ? "Added" : "Removed";
549 var direction = shouldSuppress ? "to" : "from";
550 Debug.Log($"[{nameof(YandexAssemblyUpdater)}]: {action} warning 414 suppression {direction} {string.Join(", ", changedFileNames)}.");
551 return true;
552 }
553
560 private static bool Is0414WarningSuppressionApplied(YandexData data, bool shouldSuppress)
561 {
562 if (data == null)
563 return true;
564
565 foreach (var assetPath in Get0414WarningAssetPaths(data))
566 {
567 var absolutePath = ToAbsolutePath(assetPath);
568 if (!File.Exists(absolutePath))
569 {
570 if (shouldSuppress)
571 return false;
572
573 continue;
574 }
575
576 var lines = File.ReadAllLines(absolutePath, Encoding.UTF8);
577 if (!shouldSuppress)
578 {
579 if (HasOwned0414WarningSuppression(lines))
580 return false;
581
582 continue;
583 }
584
585 var foundWarning67Directive = false;
586 for (var index = 0; index < lines.Length; index++)
587 {
588 var warningCodes = GetDisabledWarningCodes(lines[index]);
589 if (!warningCodes.Contains(WarningCode0067, StringComparer.Ordinal))
590 continue;
591
592 foundWarning67Directive = true;
593 if (!warningCodes.Contains(WarningCode0414, StringComparer.Ordinal))
594 return false;
595
596 var match = s_warningDisableRegex.Match(lines[index]);
597 var hasInlineMarker = Has0414WarningSuppressionMarker(match.Groups["comment"].Value);
598 var hasPreviousMarker = index > 0 && IsStandalone0414WarningSuppressionMarker(lines[index - 1]);
599 if (!hasInlineMarker && !hasPreviousMarker)
600 return false;
601 }
602
603 if (!foundWarning67Directive)
604 return false;
605 }
606
607 return true;
608 }
609
613 internal static void Validate0414WarningSuppressionPreference()
614 {
615 if (IsSdkUpdateRunning())
616 return;
617
618 if (!DefineSymbolsManager.HasDefineSymbol(DefineSymbolsManager.YandexMobileAds))
619 return;
620
621 var data = GetConfiguredYandexData(AdAggregatorResource.Instance);
622 EditorPrefs.SetBool(SuppressYandex0414WarningsPreferenceKey,
623 data != null && Is0414WarningSuppressionApplied(data, true));
624 }
625
631 private static string[] Get0414WarningAssetPaths(YandexData data)
632 {
633 var commonDirectory = $"{ToAssetsPath(data.AssetPath)}/Common";
634 return s_cs0414WarningFileNames
635 .Select(fileName => $"{commonDirectory}/{fileName}")
636 .ToArray();
637 }
638
644 private static string Add0414WarningSuppression(string source)
645 {
646 if (string.IsNullOrEmpty(source))
647 return source;
648
649 var newline = source.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
650 var lines = source.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList();
651 var changed = false;
652
653 for (var index = 0; index < lines.Count; index++)
654 {
655 var match = s_warningDisableRegex.Match(lines[index]);
656 if (!match.Success)
657 continue;
658
659 var warningCodes = match.Groups["codes"].Value
660 .Split(',')
661 .Select(code => code.Trim())
662 .ToArray();
663 if (!warningCodes.Contains(WarningCode0067, StringComparer.Ordinal))
664 {
665 continue;
666 }
667
668 var comment = match.Groups["comment"].Value;
669 var hasInlineMarker = Has0414WarningSuppressionMarker(comment);
670 var hasPreviousMarker = index > 0 && IsStandalone0414WarningSuppressionMarker(lines[index - 1]);
671 var hasWarning414 = warningCodes.Contains(WarningCode0414, StringComparer.Ordinal);
672 if (hasWarning414 && !hasInlineMarker && !hasPreviousMarker)
673 continue;
674
675 var updatedCodes = match.Groups["codes"].Value.TrimEnd();
676 if (!hasWarning414)
677 updatedCodes += $", {WarningCode0414}";
678
679 if (!hasInlineMarker)
680 comment = Append0414WarningSuppressionMarker(comment);
681
682 lines[index] = $"{match.Groups["prefix"].Value}{updatedCodes}{comment}";
683 if (hasPreviousMarker)
684 {
685 lines.RemoveAt(index - 1);
686 index--;
687 }
688
689 changed = true;
690 }
691
692 return changed ? string.Join(newline, lines) : source;
693 }
694
700 private static string Remove0414WarningSuppression(string source)
701 {
702 if (string.IsNullOrEmpty(source))
703 return source;
704
705 var newline = source.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
706 var lines = source.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList();
707 var changed = false;
708
709 for (var index = 0; index < lines.Count; index++)
710 {
711 var match = s_warningDisableRegex.Match(lines[index]);
712 if (!match.Success)
713 continue;
714
715 var comment = match.Groups["comment"].Value;
716 var hasInlineMarker = Has0414WarningSuppressionMarker(comment);
717 var hasPreviousMarker = index > 0 && IsStandalone0414WarningSuppressionMarker(lines[index - 1]);
718 if (!hasInlineMarker && !hasPreviousMarker)
719 continue;
720
721 var warningCodes = match.Groups["codes"].Value
722 .Split(',')
723 .Select(code => code.Trim())
724 .ToArray();
725 var updatedComment = hasInlineMarker
726 ? Remove0414WarningSuppressionMarker(comment)
727 : comment;
728 var updatedCodes = match.Groups["codes"].Value.TrimEnd();
729 if (warningCodes.Contains(WarningCode0067, StringComparer.Ordinal) &&
730 warningCodes.Contains(WarningCode0414, StringComparer.Ordinal))
731 {
732 updatedCodes = string.Join(
733 ", ",
734 warningCodes.Where(code => !string.Equals(code, WarningCode0414, StringComparison.Ordinal)));
735 }
736
737 lines[index] = $"{match.Groups["prefix"].Value}{updatedCodes}{updatedComment}";
738
739 if (hasPreviousMarker)
740 {
741 lines.RemoveAt(index - 1);
742 index--;
743 }
744
745 changed = true;
746 }
747
748 return changed ? string.Join(newline, lines) : source;
749 }
750
756 private static bool HasOwned0414WarningSuppression(string[] lines)
757 {
758 for (var index = 0; index < lines.Length; index++)
759 {
760 var warningCodes = GetDisabledWarningCodes(lines[index]);
761 if (!warningCodes.Contains(WarningCode0067, StringComparer.Ordinal) ||
762 !warningCodes.Contains(WarningCode0414, StringComparer.Ordinal))
763 continue;
764
765 var match = s_warningDisableRegex.Match(lines[index]);
766 if (Has0414WarningSuppressionMarker(match.Groups["comment"].Value) ||
767 index > 0 && IsStandalone0414WarningSuppressionMarker(lines[index - 1]))
768 {
769 return true;
770 }
771 }
772
773 return false;
774 }
775
781 private static bool Has0414WarningSuppressionMarker(string comment) =>
782 comment.TrimEnd().EndsWith(Warning414SuppressionMarker, StringComparison.Ordinal);
783
789 private static string Append0414WarningSuppressionMarker(string comment) =>
790 string.IsNullOrEmpty(comment)
791 ? $" {Warning414SuppressionMarker}"
792 : $"{comment.TrimEnd()} {Warning414SuppressionMarker}";
793
799 private static string Remove0414WarningSuppressionMarker(string comment)
800 {
801 var trimmedComment = comment.TrimEnd();
802 var markerIndex = trimmedComment.LastIndexOf(Warning414SuppressionMarker, StringComparison.Ordinal);
803 if (markerIndex < 0)
804 return comment;
805
806 var updatedComment = trimmedComment.Remove(markerIndex, Warning414SuppressionMarker.Length);
807 return string.IsNullOrWhiteSpace(updatedComment) ||
808 string.Equals(updatedComment.Trim(), "//", StringComparison.Ordinal)
809 ? string.Empty
810 : updatedComment.TrimEnd();
811 }
812
818 private static bool IsStandalone0414WarningSuppressionMarker(string line) =>
819 string.Equals(line.Trim(), Warning414SuppressionMarker, StringComparison.Ordinal);
820
826 private static string[] GetDisabledWarningCodes(string line)
827 {
828 var match = s_warningDisableRegex.Match(line);
829 return match.Success
830 ? match.Groups["codes"].Value.Split(',').Select(code => code.Trim()).ToArray()
831 : Array.Empty<string>();
832 }
833
834 #endregion
835
836 #region Enhanced editor dummies
837
838 private static bool ApplyOrRestoreEnhancedEditorDummies(bool useEnhancedEditorDummies) =>
839 YandexEditorDummyUpdater.ApplyOrRestore(yandexData, useEnhancedEditorDummies);
840
841 #endregion
842
843 #region Patch Yandex asmdef
844
848 private static bool UpdateAssemblyReferences()
849 {
850 if (yandexData.Asmdef == null)
851 throw new UnityException("YandexData.Asmdef is not assigned. Please reference SCG.Yandex.asmdef asset.");
852
853 var targetAsmdefRel = AssetDatabase.GetAssetPath(yandexData.Asmdef);
854 if (string.IsNullOrWhiteSpace(targetAsmdefRel))
855 throw new UnityException("Unable to resolve path to SCG.Yandex.asmdef via AssetDatabase.");
856
857 var targetAsmdefAbs = ToAbsolutePath(targetAsmdefRel);
858 if (!File.Exists(targetAsmdefAbs))
859 throw new UnityException($"The asmdef file was not found: {targetAsmdefRel}");
860
861 var original = File.ReadAllText(targetAsmdefAbs, Encoding.UTF8);
862 var updated = PatchDefineConstraint(original, yandexData.RemoveDefineConstraint);
863
864 if (string.Equals(original, updated, StringComparison.Ordinal))
865 return false;
866
867 File.WriteAllText(targetAsmdefAbs, updated, Encoding.UTF8);
868 Debug.Log($"[{nameof(YandexAssemblyUpdater)}]: Assembly references updated and define constraint removed successfully.");
869 return true;
870 }
871
877 private static string PatchDefineConstraint(string json, string defineToRemove)
878 {
879 if (string.IsNullOrWhiteSpace(defineToRemove))
880 return json;
881
882 var escaped = Regex.Escape(defineToRemove);
883 var pattern = $"\"\\b{escaped}\\b\",?\\s*";
884 var result = Regex.Replace(json, pattern, string.Empty);
885
886 // Normalize trailing list comma: ", ]" -> "\n ]"
887 result = Regex.Replace(result, @",\s*\‍]", "\n ]");
888
889 // Normalize empty defineConstraints array to multiline form for readability
890 result = Regex.Replace(result, @"""defineConstraints"":\s*\‍[\s*\‍]", @"""defineConstraints"": [
891 ]");
892
893 return result;
894 }
895
896 private static string AddDefineConstraint(string json, string defineToAdd)
897 {
898 if (string.IsNullOrWhiteSpace(defineToAdd) || json.Contains($"\"{defineToAdd}\"", StringComparison.Ordinal))
899 return json;
900
901 var emptyArrayPattern = @"""defineConstraints"":\s*\‍[\s*\‍]";
902 if (Regex.IsMatch(json, emptyArrayPattern))
903 {
904 return Regex.Replace(
905 json,
906 emptyArrayPattern,
907 $"\"defineConstraints\": [\n \"{defineToAdd}\"\n ]");
908 }
909
910 var arrayPattern = @"""defineConstraints"":\s*\‍[(?<content>[\s\S]*?)\‍]";
911 return Regex.Replace(
912 json,
913 arrayPattern,
914 match =>
915 {
916 var content = match.Groups["content"].Value;
917 var trimmed = content.Trim();
918 var separator = string.IsNullOrEmpty(trimmed) ? string.Empty : ",";
919 return $"\"defineConstraints\": [{content}{separator}\n \"{defineToAdd}\"\n ]";
920 },
921 RegexOptions.CultureInvariant);
922 }
923
924 #endregion
925
926 #region AdRevenue import worker guard
927
928 private static bool PatchAdRevenueAutoCollectionResolverGuard()
929 {
930 var changed = false;
931 foreach (var file in GetAdRevenueAutoCollectionResolverPaths())
932 {
933 var original = File.ReadAllText(file, Encoding.UTF8);
934 var updated = PatchAdRevenueAutoCollectionResolverGuard(original);
935 if (string.Equals(original, updated, StringComparison.Ordinal))
936 continue;
937
938 File.WriteAllText(file, updated, Encoding.UTF8);
939 changed = true;
940
941 var assetPath = ToAssetPathFromAbsolute(file);
942 if (!string.IsNullOrEmpty(assetPath))
943 AssetDatabase.ImportAsset(assetPath);
944
945 Debug.Log($"[{nameof(YandexAssemblyUpdater)}]: Updated import worker guard in {Path.GetFileName(file)}.");
946 }
947
948 return changed;
949 }
950
951 private static string[] GetAdRevenueAutoCollectionResolverPaths()
952 {
953 var dataPath = Application.dataPath.Replace('\\', '/');
954 return Directory.Exists(dataPath)
955 ? Directory.GetFiles(dataPath, AdRevenueAutoCollectionResolverFileName, SearchOption.AllDirectories)
956 .OrderBy(path => path, StringComparer.Ordinal)
957 .ToArray()
958 : Array.Empty<string>();
959 }
960
961 private static string PatchAdRevenueAutoCollectionResolverGuard(string source)
962 {
963 if (string.IsNullOrEmpty(source))
964 return source;
965
966 var newline = source.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
967 var lines = source.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList();
968 var changed = adRevenueAutoCollectionResolverGuardMethods.Aggregate(
969 false,
970 (current, signature) => current | PatchMethodImportWorkerGuard(lines, signature));
971
972 return changed ? string.Join(newline, lines) : source;
973 }
974
975 private static bool PatchMethodImportWorkerGuard(List<string> lines, string signature)
976 {
977 if (!TryFindMethodBlock(lines, signature, out _, out var openLine, out var endLine))
978 return false;
979
980 var changed = RemoveImportWorkerGuard(lines, openLine + 1, endLine);
981 if (!TryFindMethodBlock(lines, signature, out var methodLine, out openLine, out endLine))
982 return changed;
983
984 var contentStart = openLine + 1;
985 if (HasImportWorkerGuard(lines, contentStart, endLine))
986 return changed;
987
988 var indent = GetMethodBodyIndent(lines, methodLine, contentStart, endLine);
989 lines.InsertRange(contentStart, new[]
990 {
991 $"{indent}if ({ImportWorkerGuardCondition}) {{ // {ImportWorkerGuardComment}",
992 $"{indent} return;",
993 $"{indent}}}",
994 string.Empty
995 });
996
997 return true;
998 }
999
1000 private static bool TryFindMethodBlock(List<string> lines, string signature, out int methodLine, out int openLine, out int endLine)
1001 {
1002 methodLine = FindMethodLine(lines, signature);
1003 openLine = methodLine < 0 ? -1 : FindOpeningBraceLine(lines, methodLine);
1004 endLine = openLine < 0 ? -1 : FindMatchingClosingBrace(lines, openLine);
1005 return endLine > openLine;
1006 }
1007
1008 private static int FindMethodLine(List<string> lines, string signature) =>
1009 lines.FindIndex(line => line.TrimStart().StartsWith(signature, StringComparison.Ordinal));
1010
1011 private static int FindOpeningBraceLine(List<string> lines, int startLine)
1012 {
1013 for (var i = startLine; i < lines.Count; i++)
1014 {
1015 var trimmed = lines[i].TrimStart();
1016 if (i > startLine && trimmed.StartsWith("[", StringComparison.Ordinal))
1017 return -1;
1018
1019 if (lines[i].Contains("{", StringComparison.Ordinal))
1020 return i;
1021 }
1022
1023 return -1;
1024 }
1025
1026 private static bool RemoveImportWorkerGuard(List<string> lines, int startLine, int endLine)
1027 {
1028 var changed = false;
1029
1030 for (var i = endLine; i >= startLine; i--)
1031 {
1032 if (!lines[i].Contains(ImportWorkerGuardComment, StringComparison.Ordinal))
1033 continue;
1034
1035 var guardOpen = lines[i].Contains("{", StringComparison.Ordinal) ? i : FindOpeningBraceLine(lines, i);
1036 if (guardOpen < 0)
1037 continue;
1038
1039 var guardEnd = FindMatchingClosingBrace(lines, guardOpen);
1040 if (guardEnd < guardOpen || guardEnd > endLine)
1041 continue;
1042
1043 lines.RemoveRange(i, guardEnd - i + 1);
1044 if (i < lines.Count && string.IsNullOrWhiteSpace(lines[i]))
1045 lines.RemoveAt(i);
1046
1047 changed = true;
1048 }
1049
1050 return changed;
1051 }
1052
1053 private static bool HasImportWorkerGuard(List<string> lines, int startLine, int endLine)
1054 {
1055 for (var i = startLine; i <= endLine && i < lines.Count; i++)
1056 {
1057 if (lines[i].Contains(ImportWorkerGuardComment, StringComparison.Ordinal))
1058 return true;
1059 }
1060
1061 return false;
1062 }
1063
1064 private static string GetMethodBodyIndent(List<string> lines, int methodLine, int startLine, int endLine)
1065 {
1066 for (var i = startLine; i <= endLine && i < lines.Count; i++)
1067 {
1068 if (!string.IsNullOrWhiteSpace(lines[i]))
1069 return LeadingWhitespace(lines[i]);
1070 }
1071
1072 return LeadingWhitespace(lines[methodLine]) + " ";
1073 }
1074
1075 #endregion
1076
1077 #region iOS Fix
1078
1079 private static bool FixIosReferences(bool applyChanges = true)
1080 {
1081 if (AssetDatabase.IsAssetImportWorkerProcess())
1082 return false;
1083
1084 var assetsDir = ToAssetsPath(yandexData.AssetPath);
1085 if (!AssetDatabase.IsValidFolder(assetsDir))
1086 return false;
1087
1088 var path = ToAbsolutePath(assetsDir);
1089 var files = Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories);
1090 var changed = false;
1091
1092 foreach (var f in files)
1093 {
1094 var original = File.ReadAllText(f);
1095 var text = original;
1096
1097 text = WrapUsings(text);
1098 text = WrapIosMethodDefinitions(text);
1099 text = WrapIosIfContents(text);
1100 if (ReferenceEquals(text, original) || text == original) continue;
1101
1102 if (!applyChanges)
1103 return true;
1104
1105 File.WriteAllText(f, text);
1106 changed = true;
1107 }
1108
1109 if (changed)
1110 Debug.Log($"[{nameof(YandexAssemblyUpdater)}]: Assembly fixed.");
1111
1112 return changed;
1113 }
1114
1115 private static string WrapUsings(string src)
1116 {
1117 // Split preserving line endings simplicity
1118 var lines = src.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList();
1119 var modified = false;
1120
1121 for (var i = 0; i < lines.Count;)
1122 {
1123 var t = lines[i].Trim();
1124 if (!t.StartsWith(IosUsingStarts, StringComparison.Ordinal))
1125 {
1126 i++;
1127 continue;
1128 }
1129
1130 // Find contiguous block of "using UnityEditor.iOS.Xcode..."
1131 var start = i;
1132 var end = i;
1133 while (end + 1 < lines.Count && lines[end + 1].Trim().StartsWith(IosUsingStarts, StringComparison.Ordinal))
1134 end++;
1135
1136 // Check if already wrapped exactly around this block
1137 var prev = start - 1 >= 0 ? lines[start - 1].Trim() : string.Empty;
1138 var next = end + 1 < lines.Count ? lines[end + 1].Trim() : string.Empty;
1139 var already =
1140 prev.StartsWith("#if", StringComparison.Ordinal) && prev.Contains(IosWrappedComment) &&
1141 next.StartsWith("#endif", StringComparison.Ordinal) && next.Contains(IosWrappedComment);
1142
1143 if (!already)
1144 {
1145 var indent = LeadingWhitespace(lines[start]);
1146 lines.Insert(start, indent + IosFixIfDefine);
1147 end++; // block shifted by one
1148 lines.Insert(end + 1, indent + IosFixEndIfDefine);
1149 modified = true;
1150 i = end + 2; // skip wrapper
1151 }
1152 else
1153 {
1154 i = end + 1; // skip existing block
1155 }
1156 }
1157
1158 return modified ? string.Join("\n", lines) : src;
1159 }
1160
1161 private static string WrapIosIfContents(string src)
1162 {
1163 // Split to keep exact line structure; join with '\n' later.
1164 var lines = src.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList();
1165 var modified = false;
1166
1167 for (var i = 0; i < lines.Count; i++)
1168 {
1169 // Fast scan for the BuildTarget.iOS usage
1170 if (!lines[i].Contains(IosIfCondition))
1171 continue;
1172
1173 // Skip partial matches (e.g. NamedBuildTarget.iOS)
1174 var pos = lines[i].IndexOf(IosIfCondition, StringComparison.Ordinal);
1175 if (pos > 0 && char.IsLetterOrDigit(lines[i][pos - 1]))
1176 continue;
1177
1178 // Walk upwards to the actual 'if (...)' line that owns this condition
1179 var ifLine = i;
1180 while (ifLine >= 0 && !lines[ifLine].Contains("if"))
1181 ifLine--;
1182 if (ifLine < 0) continue;
1183
1184 // Skip preprocessor #if — only wrap C# if-statement bodies
1185 if (lines[ifLine].TrimStart().StartsWith("#", StringComparison.Ordinal))
1186 continue;
1187
1188 // Find the opening brace '{' for that 'if'
1189 var openLine = -1;
1190 for (var j = ifLine; j < lines.Count; j++)
1191 {
1192 // Stop on the first '{' after the 'if (...)'
1193 if (lines[j].Contains("{")) { openLine = j; break; }
1194 // Break if another 'if' appears before '{' – avoid nesting confusion
1195 if (j > ifLine && lines[j].Contains("if")) break;
1196 }
1197 if (openLine < 0) continue;
1198
1199 // Find the matching closing brace '}' for the block that starts at openLine
1200 var endLine = FindMatchingClosingBrace(lines, openLine);
1201 if (endLine <= openLine) continue;
1202
1203 // Calculate the *body* range (exclusive of braces)
1204 var contentStart = openLine + 1;
1205 var contentEnd = endLine - 1;
1206 if (contentEnd < contentStart) continue;
1207
1208 // Idempotency: if the insertion point already sits inside our #if/#endif region, skip.
1209 if (RegionNearHasMarker(lines, contentStart, IosWrappedComment))
1210 continue;
1211
1212 // Use the indentation of the first body line for our directives
1213 var indent = LeadingWhitespace(lines[contentStart]);
1214
1215 // Insert #if *before* the first body line
1216 lines.Insert(contentStart, indent + IosFixIfDefine);
1217
1218 lines.Insert(endLine + 1, indent + IosFixEndIfDefine);
1219
1220 modified = true;
1221
1222 // Jump beyond the just inserted #endif to avoid reprocessing this block
1223 i = endLine + 2;
1224 }
1225
1226 return modified ? string.Join("\n", lines) : src;
1227 }
1228
1229 private static string WrapIosMethodDefinitions(string src)
1230 {
1231 var lines = src.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList();
1232 var modified = false;
1233
1234 for (var i = 0; i < lines.Count; i++)
1235 {
1236 var trimmed = lines[i].Trim();
1237
1238 // If current position is already in our region, do nothing
1239 if (RegionNearHasMarker(lines, i, IosWrappedComment))
1240 continue;
1241
1242 foreach (var sig in iosMethodSignatures)
1243 {
1244 // Exact signature match (no regex – safer)
1245 if (!trimmed.Equals(sig, StringComparison.Ordinal))
1246 continue;
1247
1248 // Find '{' that opens the method body
1249 var openLine = -1;
1250 for (var j = i; j < lines.Count; j++)
1251 {
1252 if (lines[j].Contains("{")) { openLine = j; break; }
1253 // If attributes start on next lines, abort to avoid false detection
1254 if (j > i && lines[j].TrimStart().StartsWith("[", StringComparison.Ordinal)) break;
1255 }
1256 if (openLine < 0) break;
1257
1258 // Find matching closing '}' for the method body
1259 var endLine = FindMatchingClosingBrace(lines, openLine);
1260 if (endLine <= openLine) break;
1261
1262 var contentStart = openLine + 1;
1263 var body = lines.GetRange(contentStart, endLine - contentStart);
1264 body.RemoveAll(line => line.Trim().Equals(IosFixIfDefine, StringComparison.Ordinal) ||
1265 line.Trim().Equals(IosFixEndIfDefine, StringComparison.Ordinal));
1266 body.Insert(0, IosFixIfDefine);
1267 body.Add(IosFixEndIfDefine);
1268
1269 if (!lines.Skip(contentStart).Take(endLine - contentStart).SequenceEqual(body))
1270 {
1271 lines.RemoveRange(contentStart, endLine - contentStart);
1272 lines.InsertRange(contentStart, body);
1273 modified = true;
1274 }
1275
1276 i = contentStart + body.Count;
1277 break;
1278 }
1279 }
1280
1281 return modified ? string.Join("\n", lines) : src;
1282 }
1283
1284 // Returns index of the line that contains the matching closing brace for the block that opens at 'openLine'.
1285 private static int FindMatchingClosingBrace(List<string> lines, int openLine)
1286 {
1287 var depth = 0;
1288 for (var i = openLine; i < lines.Count; i++)
1289 {
1290 depth += CountChar(lines[i], '{');
1291 depth -= CountChar(lines[i], '}');
1292
1293 if (depth == 0)
1294 return i;
1295 }
1296 return -1;
1297 }
1298
1299 private static int CountChar(string s, char ch) => s.Count(t => t == ch);
1300
1301 private static string LeadingWhitespace(string s)
1302 {
1303 var i = 0;
1304 while (i < s.Length && char.IsWhiteSpace(s[i])) i++;
1305 return s[..i];
1306 }
1307
1308 private static bool RegionNearHasMarker(List<string> lines, int index, string marker)
1309 {
1310 // Determines whether 'index' is inside a #if ... // marker ... #endif region.
1311 var stack = new Stack<(int start, bool hasMarker)>();
1312
1313 for (var i = 0; i < lines.Count; i++)
1314 {
1315 var t = lines[i].Trim();
1316 if (t.StartsWith("#if"))
1317 {
1318 var has = t.Contains(marker);
1319 stack.Push((i, has));
1320 }
1321 else if (t.StartsWith("#endif"))
1322 {
1323 if (stack.Count == 0) continue;
1324 var (start, has) = stack.Pop();
1325 if (has && index >= start && index <= i)
1326 return true;
1327 }
1328 }
1329 return false;
1330 }
1331
1332 #endregion
1333
1334 #region Helpers
1335
1341 private static bool EnsureFolderExists(string assetsRelativePath)
1342 {
1343 assetsRelativePath = assetsRelativePath.Replace("\\", "/");
1344 if (AssetDatabase.IsValidFolder(assetsRelativePath))
1345 return false;
1346
1347 var segments = assetsRelativePath.Split('/');
1348 if (segments.Length == 0 || !string.Equals(segments[0], AssetsFolder, StringComparison.Ordinal))
1349 throw new UnityException($"Path must be {AssetsFolder}-relative: {assetsRelativePath}");
1350
1351 var current = AssetsFolder;
1352 for (var i = 1; i < segments.Length; i++)
1353 {
1354 var next = $"{current}/{segments[i]}";
1355 if (!AssetDatabase.IsValidFolder(next))
1356 {
1357 AssetDatabase.CreateFolder(current, segments[i]);
1358 }
1359 current = next;
1360 }
1361
1362 return !AssetDatabase.IsValidFolder(assetsRelativePath)
1363 ? throw new UnityException($"Unable to create folder: {assetsRelativePath}")
1364 : true;
1365 }
1366
1374 private static bool EnsureAsmdefExists(string assetsRelativeFile, string content, string createdLog)
1375 {
1376 if (string.IsNullOrEmpty(assetsRelativeFile))
1377 return false;
1378
1379 assetsRelativeFile = assetsRelativeFile.Replace("\\", "/");
1380 var absolute = ToAbsolutePath(assetsRelativeFile);
1381 if (File.Exists(absolute))
1382 return false;
1383
1384 var dir = Path.GetDirectoryName(absolute);
1385 if (!Directory.Exists(dir))
1386 Directory.CreateDirectory(dir!);
1387
1388 File.WriteAllText(absolute, content, Encoding.UTF8);
1389 Debug.Log(createdLog);
1390 AssetDatabase.ImportAsset(assetsRelativeFile);
1391 return true;
1392 }
1393
1398 private static string ToAssetsPath(string possiblyRelativeToAssetsFolder)
1399 {
1400 var path = possiblyRelativeToAssetsFolder.Replace("\\", "/");
1401 return path.StartsWith($"{AssetsFolder}/", StringComparison.Ordinal) ? path : $"{AssetsFolder}/{path}";
1402 }
1403
1409 private static string ToAssetPathFromAbsolute(string absolutePath)
1410 {
1411 var normalized = Path.GetFullPath(absolutePath).Replace('\\', '/');
1412 var dataPath = Application.dataPath.Replace('\\', '/');
1413 if (!normalized.StartsWith(dataPath + "/", StringComparison.Ordinal))
1414 return string.Empty;
1415
1416 var tail = normalized[dataPath.Length..].TrimStart('/');
1417 return string.IsNullOrEmpty(tail) ? AssetsFolder : $"{AssetsFolder}/{tail}";
1418 }
1419
1428 private static string ToAbsolutePath(string assetPath)
1429 {
1430 if (string.IsNullOrEmpty(assetPath))
1431 throw new UnityException("Empty asset path.");
1432
1433 var normalized = assetPath.Replace('\\', '/');
1434
1435 // "Assets" root (exact match or starts with "Assets/")
1436 if (string.Equals(normalized, AssetsFolder, StringComparison.Ordinal) ||
1437 normalized.StartsWith(AssetsFolder + "/", StringComparison.Ordinal))
1438 {
1439 var tail = normalized.Length == AssetsFolder.Length
1440 ? string.Empty
1441 : normalized[AssetsFolder.Length..].TrimStart('/');
1442
1443 var abs = Path.Combine(Application.dataPath, tail);
1444 return Path.GetFullPath(abs).Replace('\\', '/');
1445 }
1446
1447 // "Packages/<id>/..." (must strictly start with "Packages/")
1448 if (normalized.StartsWith(PackagesFolder + "/", StringComparison.Ordinal))
1449 {
1450 var pinfo = PackageInfo.FindForAssetPath(normalized)
1451 ?? throw new UnityException($"Unable to resolve package for '{assetPath}'.");
1452
1453 // Part inside the package folder
1454 var relInside = normalized.Length > pinfo.assetPath.Length
1455 ? normalized[pinfo.assetPath.Length..].TrimStart('/')
1456 : string.Empty;
1457
1458 var abs = string.IsNullOrEmpty(relInside)
1459 ? pinfo.resolvedPath
1460 : Path.Combine(pinfo.resolvedPath, relInside);
1461
1462 return Path.GetFullPath(abs).Replace('\\', '/');
1463 }
1464
1465 // Absolute path passthrough
1466 if (Path.IsPathRooted(normalized))
1467 return Path.GetFullPath(normalized).Replace('\\', '/');
1468
1469 // Project-relative fallback
1470 var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
1471 return Path.GetFullPath(Path.Combine(projectRoot, normalized)).Replace('\\', '/');
1472 }
1473
1474 #endregion
1475 }
1476}